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

Commit 63620cc6 authored by Ben Lin's avatar Ben Lin
Browse files

[multi-part] Gesture Multi-Select Code polish.

Using Range for multi-select instead of using own logic. However, this
CL removes the erase feature.

Bug: 30101739
Change-Id: If77bb784a669b88e45d49c5bc92a7f4e64aa55e2
parent 8b883320
Loading
Loading
Loading
Loading
+15 −50
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.support.v7.widget.RecyclerView;
import android.view.MotionEvent;
import android.view.View;

import com.android.documentsui.Events;
import com.android.documentsui.Events.InputEvent;
import com.android.documentsui.Events.MotionInputEvent;
import com.android.documentsui.dirlist.ViewAutoScroller.ScrollActionDelegate;
@@ -29,8 +30,6 @@ import com.android.documentsui.dirlist.ViewAutoScroller.ScrollDistanceDelegate;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import java.util.function.IntSupplier;

@@ -72,11 +71,9 @@ class GestureMultiSelectHelper {
    private final int mAutoScrollEdgeHeight;
    private final int mColumnCount;
    private final IntSupplier mHeight;
    private final Set<String> mCurrentSelectedIds = new HashSet<>();
    private int mLastGlidedItemPos = -1;
    private int mLastStartedItemPos = -1;
    private boolean mEnabled = false;
    private Point mLastStartedPoint;
    private Point mLastDownPoint;
    private Point mLastInterceptedPoint;
    private @SelectType int mType = TYPE_NONE;
    private @GestureSelectIntent int mUserIntent = TYPE_UNKNOWN;
@@ -152,7 +149,7 @@ class GestureMultiSelectHelper {

    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {

        if (!mEnabled) {
        if (!mEnabled || Events.isMouseEvent(e)) {
            return false;
        }

@@ -196,10 +193,9 @@ class GestureMultiSelectHelper {
    private boolean handleInterceptedDownEvent(RecyclerView rv, MotionEvent e) {
        View itemView = rv.findChildViewUnder(e.getX(), e.getY());
        try (InputEvent event = MotionInputEvent.obtain(e, rv)) {
            mLastStartedPoint = event.getOrigin();
            mLastDownPoint = event.getOrigin();
            if (itemView != null) {
                mLastStartedItemPos = rv.getChildAdapterPosition(itemView);
                mLastGlidedItemPos = mLastStartedItemPos;
                String modelId = mModelIdFinder.apply(mLastStartedItemPos);
                if (mSelectionMgr.getSelection().contains(modelId)) {
                    mType = TYPE_ERASE;
@@ -214,10 +210,7 @@ class GestureMultiSelectHelper {
    // Called when an ACTION_MOVE event is intercepted.
    private boolean handleInterceptedMoveEvent(RecyclerView rv, MotionEvent e) {
        if (shouldInterceptMoveEvent(rv, e)) {
            View itemView = rv.findChildViewUnder(e.getX(), e.getY());
            int pos = rv.getChildAdapterPosition(itemView);
            String modelId = mModelIdFinder.apply(pos);
            mCurrentSelectedIds.add(modelId);
            mSelectionMgr.startRangeSelection(mLastStartedItemPos);
            return true;
        }
        return false;
@@ -227,11 +220,10 @@ class GestureMultiSelectHelper {
    // Essentially, since this means all gesture movement is over, reset everything.
    private boolean handleUpEvent(RecyclerView rv, MotionEvent e) {
        mType = TYPE_NONE;
        mLastGlidedItemPos = -1;
        mLastStartedItemPos = -1;
        mLastStartedPoint = null;
        mLastDownPoint = null;
        mUserIntent = TYPE_UNKNOWN;
        mCurrentSelectedIds.clear();
        mSelectionMgr.getSelection().applyProvisionalSelection();
        return false;
    }

@@ -254,10 +246,8 @@ class GestureMultiSelectHelper {
            // item position.
            int lastGlidedItemPos = (bottomRight) ? rv.getAdapter().getItemCount() - 1
                    : rv.getChildAdapterPosition(rv.findChildViewUnder(e.getX(), e.getY()));
            if (lastGlidedItemPos != RecyclerView.NO_POSITION
                    && mLastGlidedItemPos != lastGlidedItemPos) {
                doGestureMultiSelect(mLastStartedItemPos, lastGlidedItemPos);
                mLastGlidedItemPos = lastGlidedItemPos;
            if (lastGlidedItemPos != RecyclerView.NO_POSITION) {
                doGestureMultiSelect(lastGlidedItemPos);
            }
            if (insideDragZone(rv)) {
                mDragScroller.run();
@@ -269,34 +259,9 @@ class GestureMultiSelectHelper {
     * @param startPos The adapter position of the start item.
     * @param endPos  The adapter position of the end item.
     */
    private void doGestureMultiSelect(int startPos, int endPos) {
        boolean selectionMode = (mType == TYPE_SELECTION);

        // First, reset everything that's currently selected/erased except the start item
        mCurrentSelectedIds.remove(mModelIdFinder.apply(startPos));
        mSelectionMgr.setItemsSelected(mCurrentSelectedIds, !selectionMode);

        // Then clear the set
        mCurrentSelectedIds.clear();

        // Add everything to be selected/erased
        if (startPos > endPos) {
            addItemsToModelIds(endPos, startPos);
        } else {
            addItemsToModelIds(startPos, endPos);
        }

        mSelectionMgr.setItemsSelected(mCurrentSelectedIds, selectionMode);
    }

    // Helper for {@code doGestureMultiSelect (int, int)}. Add all items from startPos <= i <=
    // endPos into mModelIds.
    private void addItemsToModelIds(int startPos, int endPos) {
        for (int i = startPos; i <= endPos; i++) {
            String modelId = mModelIdFinder.apply(i);
            if (modelId != null) {
                mCurrentSelectedIds.add(modelId);
            }
    private void doGestureMultiSelect(int endPos) {
        if (mType == TYPE_SELECTION) {
            mSelectionMgr.snapProvisionalRangeSelection(endPos);
        }
    }

@@ -313,8 +278,8 @@ class GestureMultiSelectHelper {
                return true;
            }

            int startItemPos = rv.getChildAdapterPosition(rv.findChildViewUnder(mLastStartedPoint.x,
                    mLastStartedPoint.y));
            int startItemPos = rv.getChildAdapterPosition(rv.findChildViewUnder(mLastDownPoint.x,
                    mLastDownPoint.y));
            int currentItemPos = rv
                    .getChildAdapterPosition(rv.findChildViewUnder(e.getX(), e.getY()));
            if (startItemPos == RecyclerView.NO_POSITION ||
@@ -326,7 +291,7 @@ class GestureMultiSelectHelper {
                return false;
            }

            if (mLastGlidedItemPos != currentItemPos) {
            if (startItemPos != currentItemPos) {
                int diff = Math.abs(startItemPos - currentItemPos);
                if (diff == 1 && mSelectionMgr.hasSelection()) {
                    mUserIntent = TYPE_SELECT;
+133 −73
Original line number Diff line number Diff line
@@ -54,6 +54,15 @@ public final class MultiSelectManager {
    public static final int MODE_MULTIPLE = 0;
    public static final int MODE_SINGLE = 1;

    @IntDef({
            RANGE_REGULAR,
            RANGE_PROVISIONAL
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface RangeType {}
    public static final int RANGE_REGULAR = 0;
    public static final int RANGE_PROVISIONAL = 1;

    private static final String TAG = "MultiSelectManager";

    private final Selection mSelection = new Selection();
@@ -222,15 +231,6 @@ public final class MultiSelectManager {
        }
    }

    void snapSelection(int position) {
        mRanger.snapSelection(position);

        // We're being lazy here notifying even when something might not have changed.
        // To make this more correct, we'd need to update the Ranger class to return
        // information about what has changed.
        notifySelectionChanged();
    }

    /**
     * Toggles selection on the item with the given model ID.
     *
@@ -262,28 +262,47 @@ public final class MultiSelectManager {
        setSelectionRangeBegin(pos);
    }

    void snapRangeSelection(int pos) {
        snapRangeSelection(pos, RANGE_REGULAR);
    }

    void snapProvisionalRangeSelection(int pos) {
        snapRangeSelection(pos, RANGE_PROVISIONAL);
    }

    /**
     * Sets the end point for the current range selection, started by a call to
     * {@link #startRangeSelection(int)}. This function should only be called when a range selection
     * is active (see {@link #isRangeSelectionActive()}. Items in the range [anchor, end] will be
     * selected.
     * selected or in provisional select, depending on the type supplied. Note that if the type is
     * provisional select, one should do {@link Selection#applyProvisionalSelection()} at some point
     * before calling on {@link #endRangeSelection()}.
     *
     * @param pos The new end position for the selection range.
     * @param type The type of selection the range should utilize.
     */
    void snapRangeSelection(int pos) {
    private void snapRangeSelection(int pos, @RangeType int type) {
        if (!isRangeSelectionActive()) {
            throw new IllegalStateException("Range start point not set.");
        }

        mRanger.snapSelection(pos);
        mRanger.snapSelection(pos, type);

        // We're being lazy here notifying even when something might not have changed.
        // To make this more correct, we'd need to update the Ranger class to return
        // information about what has changed.
        notifySelectionChanged();
    }

    /**
     * Stops an in-progress range selection.
     * Stops an in-progress range selection. All selection done with
     * {@link #snapRangeSelection(int, int)} with type RANGE_PROVISIONAL will be lost if
     * {@link Selection#applyProvisionalSelection()} is not called beforehand.
     */
    void endRangeSelection() {
        mRanger = null;
        // Clean up in case there was any leftover provisional selection
        mSelection.cancelProvisionalSelection();
    }

    /**
@@ -297,9 +316,6 @@ public final class MultiSelectManager {
     * Sets the magic location at which a selection range begins (the selection anchor). This value
     * is consulted when determining how to extend, and modify selection ranges. Calling this when a
     * range selection is active will reset the range selection.
     *
     * @throws IllegalStateException if {@code position} is not already be selected
     * @param position
     */
    void setSelectionRangeBegin(int position) {
        if (position == RecyclerView.NO_POSITION) {
@@ -307,38 +323,7 @@ public final class MultiSelectManager {
        }

        if (mSelection.contains(mAdapter.getModelId(position))) {
            mRanger = new Range(position);
        }
    }

    /**
     * Try to set selection state for all elements in range. Not that callbacks can cancel selection
     * of specific items, so some or even all items may not reflect the desired state after the
     * update is complete.
     *
     * @param begin Adapter position for range start (inclusive).
     * @param end Adapter position for range end (inclusive).
     * @param selected New selection state.
     */
    private void updateRange(int begin, int end, boolean selected) {
        assert(end >= begin);
        for (int i = begin; i <= end; i++) {
            String id = mAdapter.getModelId(i);
            if (id == null) {
                continue;
            }

            if (selected) {
                boolean canSelect = notifyBeforeItemStateChange(id, true);
                if (canSelect) {
                    if (mSingleSelect && hasSelection()) {
                        clearSelectionQuietly();
                    }
                    selectAndNotify(id);
                }
            } else {
                attemptDeselect(id);
            }
            mRanger = new Range(this::updateForRange, position);
        }
    }

@@ -425,50 +410,103 @@ public final class MultiSelectManager {
        }
    }

    private void updateForRange(int begin, int end, boolean selected, @RangeType int type) {
        switch (type) {
            case RANGE_REGULAR:
                updateForRegularRange(begin, end, selected);
                break;
            case RANGE_PROVISIONAL:
                updateForProvisionalRange(begin, end, selected);
                break;
            default:
                throw new IllegalArgumentException("Invalid range type: " + type);
        }
    }

    private void updateForRegularRange(int begin, int end, boolean selected) {
        assert(end >= begin);
        for (int i = begin; i <= end; i++) {
            String id = mAdapter.getModelId(i);
            if (id == null) {
                continue;
            }

            if (selected) {
                boolean canSelect = notifyBeforeItemStateChange(id, true);
                if (canSelect) {
                    if (mSingleSelect && hasSelection()) {
                        clearSelectionQuietly();
                    }
                    selectAndNotify(id);
                }
            } else {
                attemptDeselect(id);
            }
        }
    }

    private void updateForProvisionalRange(int begin, int end, boolean selected) {
        assert (end >= begin);
        for (int i = begin; i <= end; i++) {
            String id = mAdapter.getModelId(i);
            if (id == null) {
                continue;
            }
            if (selected) {
                mSelection.mProvisionalSelection.add(id);
            } else {
                mSelection.mProvisionalSelection.remove(id);
            }
            notifyItemStateChanged(id, selected);
        }
        notifySelectionChanged();
    }

    /**
     * Class providing support for managing range selections.
     */
    private final class Range {
    private static final class Range {
        private static final int UNDEFINED = -1;

        final int mBegin;
        int mEnd = UNDEFINED;
        private final RangeUpdater mUpdater;
        private final int mBegin;
        private int mEnd = UNDEFINED;

        public Range(int begin) {
        public Range(RangeUpdater updater, int begin) {
            if (DEBUG) Log.d(TAG, "New Ranger created beginning @ " + begin);
            mUpdater = updater;
            mBegin = begin;
        }

        private void snapSelection(int position) {
            assert(mRanger != null);
        private void snapSelection(int position, @RangeType int type) {
            assert(position != RecyclerView.NO_POSITION);

            if (mEnd == UNDEFINED || mEnd == mBegin) {
                // Reset mEnd so it can be established in establishRange.
                mEnd = UNDEFINED;
                establishRange(position);
                establishRange(position, type);
            } else {
                reviseRange(position);
                reviseRange(position, type);
            }
        }

        private void establishRange(int position) {
            assert(mRanger.mEnd == UNDEFINED);
        private void establishRange(int position, @RangeType int type) {
            assert(mEnd == UNDEFINED);

            if (position == mBegin) {
                mEnd = position;
            }

            if (position > mBegin) {
                updateRange(mBegin + 1, position, true);
                updateRange(mBegin + 1, position, true, type);
            } else if (position < mBegin) {
                updateRange(position, mBegin - 1, true);
                updateRange(position, mBegin - 1, true, type);
            }

            mEnd = position;
        }

        private void reviseRange(int position) {
        private void reviseRange(int position, @RangeType int type) {
            assert(mEnd != UNDEFINED);
            assert(mBegin != mEnd);

@@ -477,9 +515,9 @@ public final class MultiSelectManager {
            }

            if (mEnd > mBegin) {
                reviseAscendingRange(position);
                reviseAscendingRange(position, type);
            } else if (mEnd < mBegin) {
                reviseDescendingRange(position);
                reviseDescendingRange(position, type);
            }
            // the "else" case is covered by checkState at beginning of method.

@@ -490,39 +528,61 @@ public final class MultiSelectManager {
         * Updates an existing ascending seleciton.
         * @param position
         */
        private void reviseAscendingRange(int position) {
        private void reviseAscendingRange(int position, @RangeType int type) {
            // Reducing or reversing the range....
            if (position < mEnd) {
                if (position < mBegin) {
                    updateRange(mBegin + 1, mEnd, false);
                    updateRange(position, mBegin -1, true);
                    updateRange(mBegin + 1, mEnd, false, type);
                    updateRange(position, mBegin -1, true, type);
                } else {
                    updateRange(position + 1, mEnd, false);
                    updateRange(position + 1, mEnd, false, type);
                }
            }

            // Extending the range...
            else if (position > mEnd) {
                updateRange(mEnd + 1, position, true);
                updateRange(mEnd + 1, position, true, type);
            }
        }

        private void reviseDescendingRange(int position) {
        private void reviseDescendingRange(int position, @RangeType int type) {
            // Reducing or reversing the range....
            if (position > mEnd) {
                if (position > mBegin) {
                    updateRange(mEnd, mBegin - 1, false);
                    updateRange(mBegin + 1, position, true);
                    updateRange(mEnd, mBegin - 1, false, type);
                    updateRange(mBegin + 1, position, true, type);
                } else {
                    updateRange(mEnd, position - 1, false);
                    updateRange(mEnd, position - 1, false, type);
                }
            }

            // Extending the range...
            else if (position < mEnd) {
                updateRange(position, mEnd - 1, true);
                updateRange(position, mEnd - 1, true, type);
            }
        }

        /**
         * Try to set selection state for all elements in range. Not that callbacks can cancel
         * selection of specific items, so some or even all items may not reflect the desired state
         * after the update is complete.
         *
         * @param begin Adapter position for range start (inclusive).
         * @param end Adapter position for range end (inclusive).
         * @param selected New selection state.
         */
        private void updateRange(int begin, int end, boolean selected, @RangeType int type) {
            mUpdater.updateForRange(begin, end, selected, type);
        }

        /*
         * @see {@link MultiSelectManager#updateForRegularRange(int, int , boolean)} and {@link
         * MultiSelectManager#updateForProvisionalRange(int, int, boolean)}
         */
        @FunctionalInterface
        private interface RangeUpdater {
            void updateForRange(int begin, int end, boolean selected, @RangeType int type);
        }
    }

    /**
+2 −2
Original line number Diff line number Diff line
@@ -182,7 +182,7 @@ public final class UserInputHandler<T extends InputEvent>
    }

    private void extendSelectionRange(T event) {
        mSelectionMgr.snapSelection(event.getItemPosition());
        mSelectionMgr.snapRangeSelection(event.getItemPosition());
    }

    private final class TouchInputDelegate {
@@ -200,7 +200,7 @@ public final class UserInputHandler<T extends InputEvent>

            if (mSelectionMgr.hasSelection()) {
                if (isRangeExtension(event)) {
                    mSelectionMgr.snapSelection(event.getItemPosition());
                    mSelectionMgr.snapRangeSelection(event.getItemPosition());
                } else {
                    selectDocument(mDocFinder.apply(event));
                }
+43 −0
Original line number Diff line number Diff line
@@ -102,7 +102,50 @@ public class MultiSelectManagerTest extends AndroidTestCase {
        mSelection.assertSelectionSize(29);
        mSelection.assertRangeSelected(15, 27);
        mSelection.assertRangeSelected(42, 57);
    }

    public void testProvisionalRangeSelection() {
        mManager.startRangeSelection(13);
        mManager.snapProvisionalRangeSelection(15);
        mSelection.assertRangeSelection(13, 15);
        mManager.getSelection().applyProvisionalSelection();
        mManager.endRangeSelection();
        mSelection.assertSelectionSize(3);
    }

    public void testProvisionalRangeSelection_endEarly() {
        mManager.startRangeSelection(13);
        mManager.snapProvisionalRangeSelection(15);
        mSelection.assertRangeSelection(13, 15);

        mManager.endRangeSelection();
        // If we end range selection prematurely for provision selection, nothing should be selected
        // except the first item
        mSelection.assertSelectionSize(1);
    }

    public void testProvisionalRangeSelection_snapExpand() {
        mManager.startRangeSelection(13);
        mManager.snapProvisionalRangeSelection(15);
        mSelection.assertRangeSelection(13, 15);
        mManager.getSelection().applyProvisionalSelection();
        mManager.snapRangeSelection(18);
        mSelection.assertRangeSelection(13, 18);
    }

    public void testCombinationRangeSelection_IntersectsOldSelection() {
        mManager.startRangeSelection(13);
        mManager.snapRangeSelection(15);
        mSelection.assertRangeSelection(13, 15);

        mManager.startRangeSelection(11);
        mManager.snapProvisionalRangeSelection(18);
        mSelection.assertRangeSelected(11, 18);
        mManager.endRangeSelection();

        mSelection.assertRangeSelected(13, 15);
        mSelection.assertRangeSelected(11, 11);
        mSelection.assertSelectionSize(4);
    }

    public void testProvisionalSelection() {