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

Commit 182f2de1 authored by Steve McKay's avatar Steve McKay Committed by Android (Google) Code Review
Browse files

Merge "Decouple gesture detection."

parents 9203e72b 669ebe7f
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) {