在不覆盖 ViewGroup 或 View 方法的情况下拦截 click/touch 事件
Intercept click/touch event without overriding ViewGroup or View methods
有什么方法可以 intercept/decorate 查看触摸事件而不扩展 View
或包装在某些 ViewGroup
中(可以拦截子事件)?
假设我有 ExpandableListView
来处理项目点击事件。如果我在适配器 OnClickListener
或 OnTouchListener
中设置适配器返回的膨胀项目视图, ExpandableListView
不会执行相应的操作(组扩展),因为项目的侦听器使用了事件。
我不想使用ExpandableListView#setOnItemClickListener
的原因是,我想在不使用ExpandableListView
依赖的情况下装饰适配器中的点击事件。
我找到了解决这个问题的有效方法。
解决方案:在 OnTouchListener
中收集事件克隆,然后将它们分派到父视图。
private final Queue<MotionEntry> consumedEvents = new LinkedList<>();
private final AtomicBoolean isDispatching = new AtomicBoolean(false);
...
groupView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent e) {
// we don't want to handle re-dispatched event...
if (isDispatching.get()) {
return false;
}
// create clone as event might be changed by parent
MotionEvent clone = MotionEvent.obtain(e);
MotionEntry entry = new MotionEntry(v, clone);
consumedEvents.add(entry);
// consume ACTION_DOWN in order to receive subsequent motion events
// like ACTION_MOVE, ACTION_CANCEL/ACTION_UP...
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
return true;
}
// we do not want to handle canceled motion...
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
consumedEvents.clear();
return false;
}
// at this moment we have intercepted whole motion
// = re-dispatch to parent in order to apply default handling...
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
dispatchEvents();
}
return true;
}
});
...
派送方式:
private void dispatchEvents() {
isDispatching.set(true);
while (!consumedEvents.isEmpty()) {
MotionEntry entry = consumedEvents.poll();
ViewGroup parent = (ViewGroup) entry.view.getParent();
if (parent == null || entry.view.getVisibility() != View.VISIBLE) {
continue; // skip dispatching to detached/invisible view
}
// make position relative to parent...
entry.event.offsetLocation(entry.view.getLeft(), entry.view.getTop());
entry.event.setSource(PARENT_DISPATCHER);
parent.dispatchTouchEvent(entry.event);
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
clickListener.onClick(entry.view);
}
}
isDispatching.set(false);
}
帮手class
private class MotionEntry {
private final View view;
private final MotionEvent event;
public MotionEntry(View view, MotionEvent event) {
this.view = view;
this.event = event;
}
}
有什么方法可以 intercept/decorate 查看触摸事件而不扩展 View
或包装在某些 ViewGroup
中(可以拦截子事件)?
假设我有 ExpandableListView
来处理项目点击事件。如果我在适配器 OnClickListener
或 OnTouchListener
中设置适配器返回的膨胀项目视图, ExpandableListView
不会执行相应的操作(组扩展),因为项目的侦听器使用了事件。
我不想使用ExpandableListView#setOnItemClickListener
的原因是,我想在不使用ExpandableListView
依赖的情况下装饰适配器中的点击事件。
我找到了解决这个问题的有效方法。
解决方案:在 OnTouchListener
中收集事件克隆,然后将它们分派到父视图。
private final Queue<MotionEntry> consumedEvents = new LinkedList<>();
private final AtomicBoolean isDispatching = new AtomicBoolean(false);
...
groupView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent e) {
// we don't want to handle re-dispatched event...
if (isDispatching.get()) {
return false;
}
// create clone as event might be changed by parent
MotionEvent clone = MotionEvent.obtain(e);
MotionEntry entry = new MotionEntry(v, clone);
consumedEvents.add(entry);
// consume ACTION_DOWN in order to receive subsequent motion events
// like ACTION_MOVE, ACTION_CANCEL/ACTION_UP...
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
return true;
}
// we do not want to handle canceled motion...
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
consumedEvents.clear();
return false;
}
// at this moment we have intercepted whole motion
// = re-dispatch to parent in order to apply default handling...
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
dispatchEvents();
}
return true;
}
});
...
派送方式:
private void dispatchEvents() {
isDispatching.set(true);
while (!consumedEvents.isEmpty()) {
MotionEntry entry = consumedEvents.poll();
ViewGroup parent = (ViewGroup) entry.view.getParent();
if (parent == null || entry.view.getVisibility() != View.VISIBLE) {
continue; // skip dispatching to detached/invisible view
}
// make position relative to parent...
entry.event.offsetLocation(entry.view.getLeft(), entry.view.getTop());
entry.event.setSource(PARENT_DISPATCHER);
parent.dispatchTouchEvent(entry.event);
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
clickListener.onClick(entry.view);
}
}
isDispatching.set(false);
}
帮手class
private class MotionEntry {
private final View view;
private final MotionEvent event;
public MotionEntry(View view, MotionEvent event) {
this.view = view;
this.event = event;
}
}