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

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

Merge "Add SHIFT+Click selection support."

parents fc15b56b e63dce7f
Loading
Loading
Loading
Loading
+195 −13
Original line number Original line Diff line number Diff line
@@ -16,6 +16,7 @@


package com.android.documentsui;
package com.android.documentsui;


import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.Preconditions.checkState;
import static com.android.internal.util.Preconditions.checkState;


@@ -26,9 +27,12 @@ import android.util.Log;
import android.util.SparseBooleanArray;
import android.util.SparseBooleanArray;
import android.view.GestureDetector;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.GestureDetector.OnGestureListener;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View;


import com.android.internal.util.Preconditions;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.annotations.VisibleForTesting;


import java.util.ArrayList;
import java.util.ArrayList;
@@ -43,9 +47,11 @@ public final class MultiSelectManager {
    private static final boolean DEBUG = false;
    private static final boolean DEBUG = false;


    private final Selection mSelection = new Selection();
    private final Selection mSelection = new Selection();

    // Only created when selection is cleared.
    // Only created when selection is cleared.
    private Selection mIntermediateSelection;
    private Selection mIntermediateSelection;


    private Ranger mRanger;
    private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1);
    private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1);


    private Adapter<?> mAdapter;
    private Adapter<?> mAdapter;
@@ -213,6 +219,8 @@ public final class MultiSelectManager {
     * Clears the selection.
     * Clears the selection.
     */
     */
    public void clearSelection() {
    public void clearSelection() {
        mRanger = null;

        if (mSelection.isEmpty()) {
        if (mSelection.isEmpty()) {
            return;
            return;
        }
        }
@@ -238,7 +246,7 @@ public final class MultiSelectManager {
            return false;
            return false;
        }
        }


        return onSingleTapUp(mHelper.findEventPosition(e));
        return onSingleTapUp(mHelper.findEventPosition(e), e.getMetaState());
    }
    }


    /**
    /**
@@ -246,11 +254,12 @@ public final class MultiSelectManager {
     * can be mocked.
     * can be mocked.
     *
     *
     * @param position
     * @param position
     * @param metaState as returned from {@link MotionEvent#getMetaState()}.
     * @return true if the event was consumed.
     * @return true if the event was consumed.
     * @hide
     * @hide
     */
     */
    @VisibleForTesting
    @VisibleForTesting
    boolean onSingleTapUp(int position) {
    boolean onSingleTapUp(int position, int metaState) {
        if (mSelection.isEmpty()) {
        if (mSelection.isEmpty()) {
            return false;
            return false;
        }
        }
@@ -261,10 +270,18 @@ public final class MultiSelectManager {
            return true;
            return true;
        }
        }


        if (isShiftPressed(metaState) && mRanger != null) {
            mRanger.snapSelection(position);
        } else {
            toggleSelection(position);
            toggleSelection(position);
        }
        return true;
        return true;
    }
    }


    private static boolean isShiftPressed(int metaState) {
        return (metaState & KeyEvent.META_SHIFT_ON) != 0;
    }

    private void onLongPress(MotionEvent e) {
    private void onLongPress(MotionEvent e) {
        if (DEBUG) Log.d(TAG, "Handling long press event.");
        if (DEBUG) Log.d(TAG, "Handling long press event.");


@@ -273,7 +290,7 @@ public final class MultiSelectManager {
            if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event.");
            if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event.");
        }
        }


        toggleSelection(position);
        onLongPress(position);
    }
    }


    /**
    /**
@@ -292,22 +309,87 @@ public final class MultiSelectManager {
        toggleSelection(position);
        toggleSelection(position);
    }
    }


    private void toggleSelection(int position) {
    /**
     * Toggles the selection state at position. If an item does end up selected
     * a new Ranger (range selection manager) at that point is created.
     *
     * @param position
     * @return True if state changed.
     */
    private boolean toggleSelection(int position) {
        // Position may be special "no position" during certain
        // Position may be special "no position" during certain
        // transitional phases. If so, skip handling of the event.
        // transitional phases. If so, skip handling of the event.
        if (position == RecyclerView.NO_POSITION) {
        if (position == RecyclerView.NO_POSITION) {
            if (DEBUG) Log.d(TAG, "Ignoring toggle for element with no position.");
            if (DEBUG) Log.d(TAG, "Ignoring toggle for element with no position.");
            return;
            return false;
        }

        if (mSelection.contains(position)) {
            return attemptDeselect(position);
        } else {
            boolean selected = attemptSelect(position);
            // Here we're already in selection mode. In that case
            // When a simple click/tap (without SHIFT) creates causes
            // an item to be selected.
            // By recreating Ranger at this point, we allow the user to create
            // multiple separate contiguous ranges with SHIFT+Click & Click.
            if (selected) {
                mRanger = new Ranger(position);
            }
            return selected;
        }
    }

    /**
     * Try to select 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 inclusive
     * @param end inclusive
     * @param selected
     */
    private void updateRange(int begin, int end, boolean selected) {
        checkState(end >= begin);
        if (DEBUG) Log.i(TAG, String.format("Updating range begin=%d, end=%d, selected=%b.", begin, end, selected));
        for (int i = begin; i <= end; i++) {
            if (selected) {
                attemptSelect(i);
            } else {
                attemptDeselect(i);
            }
        }
    }

    /**
     * @param position
     * @return True if the update was applied.
     */
    private boolean attemptSelect(int position) {
        if (notifyBeforeItemStateChange(position, true)) {
            mSelection.add(position);
            notifyItemStateChanged(position, true);
            if (DEBUG) Log.d(TAG, "Selection after select: " + mSelection);
            return true;
        } else {
            if (DEBUG) Log.d(TAG, "Select cancelled by listener.");
            return false;
        }
    }
    }


        if (DEBUG) Log.d(TAG, "Handling long press on view: " + position);
    /**
        boolean nextState = !mSelection.contains(position);
     * @param position
        if (notifyBeforeItemStateChange(position, nextState)) {
     * @return True if the update was applied.
            boolean selected = mSelection.flip(position);
     */
            notifyItemStateChanged(position, selected);
    private boolean attemptDeselect(int position) {
            if (DEBUG) Log.d(TAG, "Selection after long press: " + mSelection);
        if (notifyBeforeItemStateChange(position, false)) {
            mSelection.remove(position);
            notifyItemStateChanged(position, false);
            if (DEBUG) Log.d(TAG, "Selection after deselect: " + mSelection);
            return true;
        } else {
        } else {
            Log.i(TAG, "Selection change cancelled by listener.");
            if (DEBUG) Log.d(TAG, "Select cancelled by listener.");
            return false;
        }
        }
    }
    }


@@ -335,6 +417,106 @@ public final class MultiSelectManager {
        mAdapter.notifyItemChanged(position);
        mAdapter.notifyItemChanged(position);
    }
    }


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

        final int mBegin;
        int mEnd = UNDEFINED;

        public Ranger(int begin) {
            if (DEBUG) Log.d(TAG, String.format("New Ranger(%d) created.", begin));
            mBegin = begin;
        }

        void snapSelection(int position) {
            checkState(mRanger != null);
            checkArgument(position != RecyclerView.NO_POSITION);

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

        private void establishRange(int position) {
            checkState(mRanger.mEnd == UNDEFINED);

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

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

            mEnd = position;
        }

        private void reviseRange(int position) {
            checkState(mEnd != UNDEFINED);
            checkState(mBegin != mEnd);

            if (position == mEnd) {
                if (DEBUG) Log.i(TAG, "Skipping no-op revision click on mEndRange.");
            }

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

            mEnd = position;
        }

        /**
         * Updates an existing ascending seleciton.
         * @param position
         */
        private void reviseAscendingRange(int position) {
            // Reducing or reversing the range....
            if (position < mEnd) {
                if (position < mBegin) {
                    updateRange(mBegin + 1, mEnd, false);
                    updateRange(position, mBegin -1, true);
                } else {
                    updateRange(position + 1, mEnd, false);
                }
            }

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

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

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

    /**
    /**
     * Object representing the current selection. Provides read only access
     * Object representing the current selection. Provides read only access
     * public access, and private write access.
     * public access, and private write access.
+78 −8
Original line number Original line Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.documentsui;
import static org.junit.Assert.*;
import static org.junit.Assert.*;


import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup;
@@ -78,34 +79,92 @@ public class MultiSelectManagerTest {


    @Test
    @Test
    public void singleTapUp_DoesNotSelectBeforeLongPress() {
    public void singleTapUp_DoesNotSelectBeforeLongPress() {
        mManager.onSingleTapUp(99);
        mManager.onSingleTapUp(99, 0);
        assertSelection();
        assertSelection();
    }
    }


    @Test
    @Test
    public void singleTapUp_UnselectsSelectedItem() {
    public void singleTapUp_UnselectsSelectedItem() {
        mManager.onLongPress(7);
        mManager.onLongPress(7);
        mManager.onSingleTapUp(7);
        mManager.onSingleTapUp(7, 0);
        assertSelection();
        assertSelection();
    }
    }


    @Test
    @Test
    public void singleTapUp_NoPositionClearsSelection() {
    public void singleTapUp_NoPositionClearsSelection() {
        mManager.onLongPress(7);
        mManager.onLongPress(7);
        mManager.onSingleTapUp(11);
        mManager.onSingleTapUp(11, 0);
        mManager.onSingleTapUp(RecyclerView.NO_POSITION);
        mManager.onSingleTapUp(RecyclerView.NO_POSITION, 0);
        assertSelection();
        assertSelection();
    }
    }


    @Test
    @Test
    public void singleTapUp_ExtendsSelection() {
    public void singleTapUp_ExtendsSelection() {
        mManager.onLongPress(99);
        mManager.onLongPress(99);
        mManager.onSingleTapUp(7);
        mManager.onSingleTapUp(7, 0);
        mManager.onSingleTapUp(13);
        mManager.onSingleTapUp(13, 0);
        mManager.onSingleTapUp(129899);
        mManager.onSingleTapUp(129899, 0);
        assertSelection(7, 99, 13, 129899);
        assertSelection(7, 99, 13, 129899);
    }
    }


    @Test
    public void singleTapUp_ShiftCreatesRangeSelection() {
        mManager.onLongPress(7);
        mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON);
        assertRangeSelection(7, 17);
    }

    @Test
    public void singleTapUp_ShiftCreatesRangeSeletion_Backwards() {
        mManager.onLongPress(17);
        mManager.onSingleTapUp(7, KeyEvent.META_SHIFT_ON);
        assertRangeSelection(7, 17);
    }

    @Test
    public void singleTapUp_SecondShiftClickExtendsSelection() {
        mManager.onLongPress(7);
        mManager.onSingleTapUp(11, KeyEvent.META_SHIFT_ON);
        mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON);
        assertRangeSelection(7, 17);
    }

    @Test
    public void singleTapUp_MultipleContiguousRangesSelected() {
        mManager.onLongPress(7);
        mManager.onSingleTapUp(11, KeyEvent.META_SHIFT_ON);
        mManager.onSingleTapUp(20, 0);
        mManager.onSingleTapUp(25, KeyEvent.META_SHIFT_ON);
        assertRangeSelected(7, 11);
        assertRangeSelected(20, 25);
        assertSelectionSize(11);
    }

    @Test
    public void singleTapUp_ShiftReducesSelectionRange_FromPreviousShiftClick() {
        mManager.onLongPress(7);
        mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON);
        mManager.onSingleTapUp(10, KeyEvent.META_SHIFT_ON);
        assertRangeSelection(7, 10);
    }

    @Test
    public void singleTapUp_ShiftReducesSelectionRange_FromPreviousShiftClick_Backwards() {
        mManager.onLongPress(17);
        mManager.onSingleTapUp(7, KeyEvent.META_SHIFT_ON);
        mManager.onSingleTapUp(14, KeyEvent.META_SHIFT_ON);
        assertRangeSelection(14, 17);
    }


    @Test
    public void singleTapUp_ShiftReversesSelectionDirection() {
        mManager.onLongPress(7);
        mManager.onSingleTapUp(17, KeyEvent.META_SHIFT_ON);
        mManager.onSingleTapUp(0, KeyEvent.META_SHIFT_ON);
        assertRangeSelection(0, 7);
    }

    private void assertSelected(int... expected) {
    private void assertSelected(int... expected) {
        for (int i = 0; i < expected.length; i++) {
        for (int i = 0; i < expected.length; i++) {
            Selection selection = mManager.getSelection();
            Selection selection = mManager.getSelection();
@@ -120,9 +179,20 @@ public class MultiSelectManagerTest {
        assertSelected(expected);
        assertSelected(expected);
    }
    }


    private void assertRangeSelected(int begin, int end) {
        for (int i = begin; i <= end; i++) {
            assertSelected(i);
        }
    }

    private void assertRangeSelection(int begin, int end) {
        assertSelectionSize(end - begin + 1);
        assertRangeSelected(begin, end);
    }

    private void assertSelectionSize(int expected) {
    private void assertSelectionSize(int expected) {
        Selection selection = mManager.getSelection();
        Selection selection = mManager.getSelection();
        assertEquals(expected, selection.size());
        assertEquals(selection.toString(), expected, selection.size());
    }
    }


    private static final class EventHelper implements RecyclerViewHelper {
    private static final class EventHelper implements RecyclerViewHelper {