Intercept click/touch event without overriding ViewGroup or View methods

2.8k views Asked by At

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.

1

There are 1 answers

1
matoni On BEST ANSWER

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;
    }
}