Is there any way how can I intercept/decorate view's touch event without extending View
or wrapping in some ViewGroup
(which can intercept child events)?
Suppose I have ExpandableListView
which handles item click events. If I set in adapter OnClickListener
or OnTouchListener
on inflated item view returned by adapter, ExpandableListView
does not perform corresponding action (group expanding) as event was consumed by item's listener.
The reason why I do not want to use ExpandableListView#setOnItemClickListener
is, that I want to decorate click event in adapter without using ExpandableListView
dependency.
I found a working solution for this problem.
Solution: collecting event clones in OnTouchListener
and then dispatching them to parent view.
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;
}
});
...
Dispatch method:
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);
}
Helper class
private class MotionEntry {
private final View view;
private final MotionEvent event;
public MotionEntry(View view, MotionEvent event) {
this.view = view;
this.event = event;
}
}