Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 669ebe7f authored by Steve McKay's avatar Steve McKay
Browse files

Decouple gesture detection.

DirectoryFragment now uses an independent gesture detector.
Move early event-handling logic out of listener into the handler class.

Change-Id: Ie8a01b1507c8a8aa74355ead38feb7b802029540
parent 8f8b5d58
Loading
Loading
Loading
Loading
+20 −2
Original line number Diff line number Diff line
@@ -65,6 +65,7 @@ import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.LayoutManager;
import android.support.v7.widget.RecyclerView.OnItemTouchListener;
import android.support.v7.widget.RecyclerView.RecyclerListener;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.text.TextUtils;
@@ -78,7 +79,6 @@ import android.util.TypedValue;
import android.view.ActionMode;
import android.view.DragEvent;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@@ -97,6 +97,7 @@ import com.android.documentsui.RecentsProvider.StateColumns;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.RootInfo;

import com.google.common.collect.Lists;

import java.util.ArrayList;
@@ -299,12 +300,29 @@ public class DirectoryFragment extends Fragment {
                    }
                };

        final GestureDetector detector = new GestureDetector(this.getContext(), listener);
        detector.setOnDoubleTapListener(listener);

        mRecView.addOnItemTouchListener(
                new OnItemTouchListener() {
                    @Override
                    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
                        detector.onTouchEvent(e);
                        return false;
                    }

                    @Override
                    public void onTouchEvent(RecyclerView rv, MotionEvent e) {}

                    @Override
                    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
                });

        // TODO: instead of inserting the view into the constructor, extract listener-creation code
        // and set the listener on the view after the fact.  Then the view doesn't need to be passed
        // into the selection manager.
        mSelectionManager = new MultiSelectManager(
                mRecView,
                listener,
                state.allowMultiple
                    ? MultiSelectManager.MODE_MULTIPLE
                    : MultiSelectManager.MODE_SINGLE);
+39 −192
Original line number Diff line number Diff line
@@ -37,8 +37,6 @@ import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import android.view.GestureDetector;
import android.view.GestureDetector.OnDoubleTapListener;
import android.view.GestureDetector.OnGestureListener;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
@@ -46,8 +44,6 @@ import android.view.View;
import com.android.documentsui.Events.InputEvent;
import com.android.documentsui.Events.MotionInputEvent;

import com.google.android.collect.Lists;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -78,30 +74,21 @@ public final class MultiSelectManager implements View.OnKeyListener {
    private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1);

    private Adapter<?> mAdapter;
    private ItemFinder mHelper;
    private boolean mSingleSelect;

    @Nullable private BandController mBandManager;

    /**
     * @param recyclerView
     * @param gestureDelegate Option delegate gesture listener.
     * @param mode Selection mode
     * @template A gestureDelegate that implements both {@link OnGestureListener}
     *     and {@link OnDoubleTapListener}
     */
    public <L extends OnGestureListener & OnDoubleTapListener> MultiSelectManager(
            final RecyclerView recyclerView, L gestureDelegate, int mode) {

        this(
                recyclerView.getAdapter(),
                new RuntimeItemFinder(recyclerView),
                mode);
    public MultiSelectManager(final RecyclerView recyclerView, int mode) {
        this(recyclerView.getAdapter(), mode);

        mEnvironment = new RuntimeSelectionEnvironment(recyclerView);

        if (mode == MODE_MULTIPLE) {
            mBandManager = new BandController(mHelper);
            mBandManager = new BandController();
        }

        GestureDetector.SimpleOnGestureListener listener =
@@ -118,15 +105,8 @@ public final class MultiSelectManager implements View.OnKeyListener {
                    }
                };

        CompositeOnGestureListener compositeListener =
                new CompositeOnGestureListener(
                        Lists.<OnGestureListener>newArrayList(listener, gestureDelegate),
                        Lists.<OnDoubleTapListener>newArrayList(listener, gestureDelegate));

        final GestureDetector detector =
                new GestureDetector(recyclerView.getContext(), compositeListener);

        detector.setOnDoubleTapListener(compositeListener);
        final GestureDetector detector = new GestureDetector(recyclerView.getContext(), listener);
        detector.setOnDoubleTapListener(listener);

        recyclerView.addOnItemTouchListener(
                new RecyclerView.OnItemTouchListener() {
@@ -134,37 +114,15 @@ public final class MultiSelectManager implements View.OnKeyListener {
                    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
                        detector.onTouchEvent(e);

                        if (mBandManager == null) {
                            return false;
                        }

                        // b/23793622 notes the fact that we *never* receiver ACTION_DOWN
                        // events in onTouchEvent. Where it not for this issue, we'd
                        // push start handling down into handleInputEvent.
                        if (mBandManager.shouldStart(e)) {
                            // endBandSelect is handled in handleInputEvent.
                            mBandManager.startBandSelect(
                                    new Point((int) e.getX(), (int) e.getY()));
                        } else if (mBandManager.isActive()
                                && Events.isMouseEvent(e)
                                && Events.isActionUp(e)) {
                            // Same issue here w b/23793622. The ACTION_UP event
                            // is only evert dispatched to onTouchEvent when
                            // there is some associated motion. If a user taps
                            // mouse, but doesn't move, then band select gets
                            // started BUT not ended. Causing phantom
                            // bands to appear when the user later clicks to start
                            // band select.
                            mBandManager.handleInputEvent(
                                    new MotionInputEvent(e, recyclerView));
                        if (mBandManager != null) {
                            return mBandManager.handleEvent(new MotionInputEvent(e, recyclerView));
                        }

                        return mBandManager.isActive();
                        return false;
                    }

                    @Override
                    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
                        mBandManager.handleInputEvent(
                        mBandManager.processInputEvent(
                                new MotionInputEvent(e, recyclerView));
                    }
                    @Override
@@ -177,13 +135,11 @@ public final class MultiSelectManager implements View.OnKeyListener {
     * @hide
     */
    @VisibleForTesting
    MultiSelectManager(Adapter<?> adapter, ItemFinder helper, int mode) {
    MultiSelectManager(Adapter<?> adapter, int mode) {
        checkNotNull(adapter, "'adapter' cannot be null.");
        checkNotNull(helper, "'helper' cannot be null.");

        mSingleSelect = mode == MODE_SINGLE;

        mHelper = helper;
        mAdapter = adapter;

        mAdapter.registerAdapterDataObserver(
@@ -873,32 +829,6 @@ public final class MultiSelectManager implements View.OnKeyListener {
        }
    }

    /**
     * Provides functionality for MultiSelectManager. Exists primarily to tests that are
     * fully isolated from RecyclerView.
     */
    interface ItemFinder {
        int findItemPosition(MotionEvent e);
    }

    /** ItemFinder implementation backed by good ol' RecyclerView. */
    private static final class RuntimeItemFinder implements ItemFinder {

        private final RecyclerView mView;

        RuntimeItemFinder(RecyclerView view) {
            mView = view;
        }

        @Override
        public int findItemPosition(MotionEvent e) {
            View view = mView.findChildViewUnder(e.getX(), e.getY());
            return view != null
                    ? mView.getChildAdapterPosition(view)
                    : RecyclerView.NO_POSITION;
        }
    }

    /**
     * Provides functionality for BandController. Exists primarily to tests that are
     * fully isolated from RecyclerView.
@@ -1095,110 +1025,6 @@ public final class MultiSelectManager implements View.OnKeyListener {
        public void onSelectionChanged();
    }

    /**
     * A composite {@code OnGestureDetector} that allows us to delegate unhandled
     * events to an outside party (presumably DirectoryFragment).
     * @template A gestureDelegate that implements both {@link OnGestureListener}
     *     and {@link OnDoubleTapListener}
     */
    private static final class CompositeOnGestureListener
            implements OnGestureListener, OnDoubleTapListener {

        private List<OnGestureListener> mGestureListeners;
        private List<OnDoubleTapListener> mTapListeners;

        public CompositeOnGestureListener(
                List<OnGestureListener> gestureListeners,
                List<OnDoubleTapListener> tapListeners) {
            mGestureListeners = gestureListeners;
            mTapListeners = tapListeners;
        }

        @Override
        public boolean onDown(MotionEvent e) {
            for (OnGestureListener l : mGestureListeners) {
                if (l.onDown(e)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public void onShowPress(MotionEvent e) {
            for (OnGestureListener l : mGestureListeners) {
                l.onShowPress(e);
            }
        }

        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            for (OnGestureListener l : mGestureListeners) {
                if (l.onSingleTapUp(e)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            for (OnGestureListener l : mGestureListeners) {
                if (l.onScroll(e1, e2, distanceX, distanceY)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            for (OnGestureListener l : mGestureListeners) {
                l.onLongPress(e);
            }
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            for (OnGestureListener l : mGestureListeners) {
                if (l.onFling(e1, e2, velocityX, velocityY)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            for (OnDoubleTapListener listener : mTapListeners) {
                if (listener.onSingleTapConfirmed(e)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            for (OnDoubleTapListener listener : mTapListeners) {
                if (listener.onDoubleTap(e)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            for (OnDoubleTapListener listener : mTapListeners) {
                if (listener.onDoubleTapEvent(e)) {
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * Provides mouse driven band-select support when used in conjunction with {@link RecyclerView}
     * and {@link MultiSelectManager}. This class is responsible for rendering the band select
@@ -1209,7 +1035,6 @@ public final class MultiSelectManager implements View.OnKeyListener {

        private static final int NOT_SET = -1;

        private final ItemFinder mItemFinder;
        private final Runnable mModelBuilder;

        @Nullable private Rect mBounds;
@@ -1222,8 +1047,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
        private long mScrollStartTime = NOT_SET;
        private final Runnable mViewScroller = new ViewScroller();

        public BandController(ItemFinder finder) {
            mItemFinder = finder;
        public BandController() {
            mEnvironment.addOnScrollListener(this);

            mModelBuilder = new Runnable() {
@@ -1235,6 +1059,29 @@ public final class MultiSelectManager implements View.OnKeyListener {
            };
        }

        public boolean handleEvent(MotionInputEvent e) {
            // b/23793622 notes the fact that we *never* receive ACTION_DOWN
            // events in onTouchEvent. Where it not for this issue, we'd
            // push start handling down into handleInputEvent.
            if (mBandManager.shouldStart(e)) {
                // endBandSelect is handled in handleInputEvent.
                mBandManager.startBandSelect(e.getOrigin());
            } else if (mBandManager.isActive()
                    && e.isMouseEvent()
                    && e.isActionUp()) {
                // Same issue here w b/23793622. The ACTION_UP event
                // is only evert dispatched to onTouchEvent when
                // there is some associated motion. If a user taps
                // mouse, but doesn't move, then band select gets
                // started BUT not ended. Causing phantom
                // bands to appear when the user later clicks to start
                // band select.
                mBandManager.processInputEvent(e);
            }

            return isActive();
        }

        private boolean isActive() {
            return mModel != null;
        }
@@ -1253,12 +1100,12 @@ public final class MultiSelectManager implements View.OnKeyListener {
            }
        }

        boolean shouldStart(MotionEvent e) {
        boolean shouldStart(MotionInputEvent e) {
            return !isActive()
                    && Events.isMouseEvent(e)  // a mouse
                    && Events.isActionDown(e)  // the initial button press
                    && e.isMouseEvent()  // a mouse
                    && e.isActionDown()  // the initial button press
                    && mAdapter.getItemCount() > 0
                    && mItemFinder.findItemPosition(e) == RecyclerView.NO_ID;  // in empty space
                    && e.getItemPosition() == RecyclerView.NO_ID;  // in empty space
        }

        boolean shouldStop(InputEvent input) {
@@ -1271,7 +1118,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
         * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
         * @param input
         */
        private void handleInputEvent(InputEvent input) {
        private void processInputEvent(InputEvent input) {
            checkArgument(input.isMouseEvent());

            if (shouldStop(input)) {
+0 −2
Original line number Diff line number Diff line
@@ -22,10 +22,8 @@ import android.content.ContextWrapper;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.provider.DocumentsContract.Document;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.RecyclerView;
import android.test.AndroidTestCase;
import android.test.MoreAsserts;
import android.test.mock.MockContentResolver;
import android.view.ViewGroup;

+4 −23
Original line number Diff line number Diff line
@@ -16,13 +16,9 @@

package com.android.documentsui;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import android.support.v7.widget.RecyclerView;
import android.test.AndroidTestCase;
import android.util.SparseBooleanArray;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

@@ -51,13 +47,11 @@ public class MultiSelectManagerTest extends AndroidTestCase {
    private MultiSelectManager mManager;
    private TestAdapter mAdapter;
    private TestCallback mCallback;
    private EventHelper mEventHelper;

    public void setUp() throws Exception {
        mAdapter = new TestAdapter(items);
        mCallback = new TestCallback();
        mEventHelper = new EventHelper();
        mManager = new MultiSelectManager(mAdapter, mEventHelper, MultiSelectManager.MODE_MULTIPLE);
        mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_MULTIPLE);
        mManager.addCallback(mCallback);
    }

@@ -175,7 +169,7 @@ public class MultiSelectManagerTest extends AndroidTestCase {
    }

    public void testSingleSelectMode() {
        mManager = new MultiSelectManager(mAdapter, mEventHelper, MultiSelectManager.MODE_SINGLE);
        mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE);
        mManager.addCallback(mCallback);
        longPress(20);
        tap(13);
@@ -183,7 +177,7 @@ public class MultiSelectManagerTest extends AndroidTestCase {
    }

    public void testSingleSelectMode_ShiftTap() {
        mManager = new MultiSelectManager(mAdapter, mEventHelper, MultiSelectManager.MODE_SINGLE);
        mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE);
        mManager.addCallback(mCallback);
        longPress(13);
        shiftTap(20);
@@ -269,26 +263,13 @@ public class MultiSelectManagerTest extends AndroidTestCase {
        assertEquals(selection.toString(), expected, selection.size());
    }

    private static final class EventHelper implements MultiSelectManager.ItemFinder {

        @Override
        public int findItemPosition(MotionEvent e) {
            throw new UnsupportedOperationException();
        }
    }

    private static final class TestCallback implements MultiSelectManager.Callback {

        Set<Integer> ignored = new HashSet<>();
        private int mLastChangedPosition;
        private boolean mLastChangedSelected;
        private boolean mSelectionChanged = false;

        @Override
        public void onItemStateChanged(int position, boolean selected) {
            this.mLastChangedPosition = position;
            this.mLastChangedSelected = selected;
        }
        public void onItemStateChanged(int position, boolean selected) {}

        @Override
        public boolean onBeforeItemStateChange(int position, boolean selected) {