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

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

Merge "Add support for single-select mode."

parents e789fc02 dbec47a4
Loading
Loading
Loading
Loading
+11 −11
Original line number Diff line number Diff line
@@ -85,7 +85,6 @@ import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

@@ -153,7 +152,6 @@ public class DirectoryFragment extends Fragment {
    // These are lazily initialized.
    private LinearLayoutManager mListLayout;
    private GridLayoutManager mGridLayout;
    private OnLayoutChangeListener mRecyclerLayoutListener;
    private int mColumnCount = 1;  // This will get updated when layout changes.

    public static void showNormal(FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
@@ -294,7 +292,13 @@ public class DirectoryFragment extends Fragment {
                    }
                };

        mSelectionManager = new MultiSelectManager(mRecView, listener);
        mSelectionManager = new MultiSelectManager(
                mRecView,
                listener,
                state.allowMultiple
                    ? MultiSelectManager.MODE_MULTIPLE
                    : MultiSelectManager.MODE_SINGLE);

        mSelectionManager.addCallback(new SelectionModeListener());

        mType = getArguments().getInt(EXTRA_TYPE);
@@ -431,7 +435,7 @@ public class DirectoryFragment extends Fragment {
    }

    private boolean onSingleTapUp(MotionEvent e) {
        if (!Events.isMouseEvent(e)) {
        if (Events.isTouchEvent(e) && mSelectionManager.getSelection().isEmpty()) {
            int position = getEventAdapterPosition(e);
            if (position != RecyclerView.NO_POSITION) {
                return handleViewItem(position);
@@ -531,13 +535,6 @@ public class DirectoryFragment extends Fragment {

        updateLayout(state.derivedMode);

        final int choiceMode;
        if (state.allowMultiple) {
            choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL;
        } else {
            choiceMode = ListView.CHOICE_MODE_NONE;
        }

        final int thumbSize = getResources().getDimensionPixelSize(R.dimen.icon_size);
        mThumbSize = new Point(thumbSize, thumbSize);
        mRecView.setAdapter(mAdapter);
@@ -622,7 +619,10 @@ public class DirectoryFragment extends Fragment {
            if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) {
                mNoDeleteCount += selected ? 1 : -1;
            }
        }

        @Override
        public void onSelectionChanged() {
            mSelectionManager.getSelection(mSelected);
            if (mSelected.size() > 0) {
                if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
+16 −1
Original line number Diff line number Diff line
@@ -28,7 +28,14 @@ final class Events {
     * Returns true if event was triggered by a mouse.
     */
    static boolean isMouseEvent(MotionEvent e) {
        return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
        return isMouseType(e.getToolType(0));
    }

    /**
     * Returns true if event was triggered by a finger or stylus touch.
     */
    static boolean isTouchEvent(MotionEvent e) {
        return isTouchType(e.getToolType(0));
    }

    /**
@@ -38,6 +45,14 @@ final class Events {
        return toolType == MotionEvent.TOOL_TYPE_MOUSE;
    }

    /**
     * Returns true if event was triggered by a finger or stylus touch.
     */
    static boolean isTouchType(int toolType) {
        return toolType == MotionEvent.TOOL_TYPE_FINGER
                || toolType == MotionEvent.TOOL_TYPE_STYLUS;
    }

    /**
     * Returns true if the shift is pressed.
     */
+104 −45
Original line number Diff line number Diff line
@@ -37,10 +37,18 @@ import java.util.ArrayList;
import java.util.List;

/**
 * MultiSelectManager adds traditional multi-item selection support to RecyclerView.
 * MultiSelectManager provides support traditional multi-item selection support to RecyclerView.
 * Additionally it can be configured to restrict selection to a single element, @see
 * #setSelectMode.
 */
public final class MultiSelectManager {

    /** Selection mode for multiple select. **/
    public static final int MODE_MULTIPLE = 0;

    /** Selection mode for multiple select. **/
    public static final int MODE_SINGLE = 1;

    private static final String TAG = "MultiSelectManager";
    private static final boolean DEBUG = false;

@@ -54,15 +62,18 @@ public final class MultiSelectManager {

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

    /**
     * @param recyclerView
     * @param gestureDelegate Option delage 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) {
            final RecyclerView recyclerView, L gestureDelegate, int mode) {

        this(
                recyclerView.getAdapter(),
                new RecyclerViewHelper() {
@@ -73,7 +84,8 @@ public final class MultiSelectManager {
                                ? recyclerView.getChildAdapterPosition(view)
                                : RecyclerView.NO_POSITION;
                    }
                });
                },
                mode);

        GestureDetector.SimpleOnGestureListener listener =
                new GestureDetector.SimpleOnGestureListener() {
@@ -110,15 +122,15 @@ public final class MultiSelectManager {

    /**
     * Constructs a new instance with {@code adapter} and {@code helper}.
     * @param adapter
     * @param helper
     * @hide
     */
    @VisibleForTesting
    MultiSelectManager(Adapter<?> adapter, RecyclerViewHelper helper) {
    MultiSelectManager(Adapter<?> adapter, RecyclerViewHelper helper, int mode) {
        checkNotNull(adapter, "'adapter' cannot be null.");
        checkNotNull(helper, "'helper' cannot be null.");

        mSingleSelect = mode == MODE_SINGLE;

        mHelper = helper;
        mAdapter = adapter;

@@ -196,34 +208,44 @@ public final class MultiSelectManager {
     * @return True if the selection state of the item changed.
     */
    public boolean setItemSelected(int position, boolean selected) {
        boolean changed = (selected)
                ? mSelection.add(position)
                : mSelection.remove(position);

        if (changed) {
            notifyItemStateChanged(position, true);
        if (mSingleSelect && !mSelection.isEmpty()) {
            clearSelectionQuietly();
        }
        return changed;
        return setItemsSelected(position, 1, selected);
    }

    /**
     * @param position
     * @param length
     * @param selected
     * Sets the selected state of the specified items. Note that the callback will NOT
     * be consulted to see if an item can be selected.
     *
     * @return True if the selection state of any of the items changed.
     */
    public boolean setItemsSelected(int position, int length, boolean selected) {
        boolean changed = false;
        for (int i = position; i < position + length; i++) {
            changed |= setItemSelected(i, selected);
            boolean itemChanged = selected ? mSelection.add(i) : mSelection.remove(i);
            if (itemChanged) {
                notifyItemStateChanged(i, selected);
            }
            changed |= itemChanged;
        }

        notifySelectionChanged();
        return changed;
    }

    /**
     * Clears the selection.
     * Clears the selection and notifies (even if nothing changes).
     */
    public void clearSelection() {
        clearSelectionQuietly();
        notifySelectionChanged();
    }

    /**
     * Clears the selection, without notifying anyone.
     */
    private void clearSelectionQuietly() {
        mRanger = null;

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

        toggleSelection(position);
        if (toggleSelection(position)) {
            notifySelectionChanged();
        }
    }

    /**
@@ -309,6 +333,10 @@ public final class MultiSelectManager {
            toggleSelection(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();
        return false;
    }

@@ -327,20 +355,29 @@ public final class MultiSelectManager {
            return false;
        }

        boolean changed = false;
        if (mSelection.contains(position)) {
            return attemptDeselect(position);
            changed = attemptDeselect(position);
        } else {
            boolean selected = attemptSelect(position);
            boolean canSelect = notifyBeforeItemStateChange(position, true);
            if (!canSelect) {
                return false;
            }
            if (mSingleSelect && !mSelection.isEmpty()) {
                clearSelectionQuietly();
            }

            // 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) {
            selectAndNotify(position);
            setSelectionFocusBegin(position);
            changed = true;
        }
            return selected;
        }

        return changed;
    }

    /**
@@ -367,10 +404,15 @@ public final class MultiSelectManager {
     */
    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);
                boolean canSelect = notifyBeforeItemStateChange(i, true);
                if (canSelect) {
                    if (mSingleSelect && !mSelection.isEmpty()) {
                        clearSelectionQuietly();
                    }
                    selectAndNotify(i);
                }
            } else {
                attemptDeselect(i);
            }
@@ -381,16 +423,12 @@ public final class MultiSelectManager {
     * @param position
     * @return True if the update was applied.
     */
    private boolean attemptSelect(int position) {
        if (notifyBeforeItemStateChange(position, true)) {
            mSelection.add(position);
    private boolean selectAndNotify(int position) {
        boolean changed = mSelection.add(position);
        if (changed) {
            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;
        }
        return changed;
    }

    /**
@@ -420,10 +458,8 @@ public final class MultiSelectManager {
    }

    /**
     * Notifies registered listeners when a selection changes.
     *
     * @param position
     * @param selected
     * Notifies registered listeners when the selection status of a single item
     * (identified by {@code position}) changes.
     */
    private void notifyItemStateChanged(int position, boolean selected) {
        int lastListener = mCallbacks.size() - 1;
@@ -433,6 +469,19 @@ public final class MultiSelectManager {
        mAdapter.notifyItemChanged(position);
    }

    /**
     * Notifies registered listeners when the selection has changed. This
     * notification should be sent only once a full series of changes
     * is complete, e.g. clearingSelection, or updating the single
     * selection from one item to another.
     */
    private void notifySelectionChanged() {
        int lastListener = mCallbacks.size() - 1;
        for (int i = lastListener; i > -1; i--) {
            mCallbacks.get(i).onSelectionChanged();
        }
    }

    /**
     * Class providing support for managing range selections.
     */
@@ -443,7 +492,7 @@ public final class MultiSelectManager {
        int mEnd = UNDEFINED;

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

@@ -680,8 +729,10 @@ public final class MultiSelectManager {
            }

            StringBuilder buffer = new StringBuilder(mSelection.size() * 28);
            buffer.append(String.format("{size=%d, ", mSelection.size()));
            buffer.append("items=[");
            buffer.append("{size=")
                    .append(mSelection.size())
                    .append(", ")
                    .append("items=[");
            for (int i=0; i < mSelection.size(); i++) {
                if (i > 0) {
                    buffer.append(", ");
@@ -726,11 +777,19 @@ public final class MultiSelectManager {
        public void onItemStateChanged(int position, boolean selected);

        /**
         * @param position
         * @param selected
         * @return false to cancel the change.
         * Called prior to an item changing state. Callbacks can cancel
         * the change at {@code position} by returning {@code false}.
         *
         * @param position Adapter position of the item that was checked or unchecked
         * @param selected <code>true</code> if the item is to be selected, <code>false</code>
         *                if the item is to be unselected.
         */
        public boolean onBeforeItemStateChange(int position, boolean selected);

        /**
         * Called immediately after completion of any set of changes.
         */
        public void onSelectionChanged();
    }

    /**
+22 −1
Original line number Diff line number Diff line
@@ -60,7 +60,7 @@ public class MultiSelectManagerTest {
        mAdapter = new TestAdapter(items);
        mCallback = new TestCallback();
        mEventHelper = new EventHelper();
        mManager = new MultiSelectManager(mAdapter, mEventHelper);
        mManager = new MultiSelectManager(mAdapter, mEventHelper, MultiSelectManager.MODE_MULTIPLE);
        mManager.addCallback(mCallback);
    }

@@ -188,6 +188,24 @@ public class MultiSelectManagerTest {
        assertRangeSelection(0, 7);
    }

    @Test
    public void singleSelectMode() {
        mManager = new MultiSelectManager(mAdapter, mEventHelper, MultiSelectManager.MODE_SINGLE);
        mManager.addCallback(mCallback);
        tap(20);
        tap(13);
        assertSelection(13);
    }

    @Test
    public void singleSelectMode_ShiftTap() {
        mManager = new MultiSelectManager(mAdapter, mEventHelper, MultiSelectManager.MODE_SINGLE);
        mManager.addCallback(mCallback);
        tap(13);
        shiftTap(20);
        assertSelection(20);
    }

    private void tap(int position) {
        mManager.onSingleTapUp(position, 0, MotionEvent.TOOL_TYPE_MOUSE);
    }
@@ -257,6 +275,9 @@ public class MultiSelectManagerTest {
        public boolean onBeforeItemStateChange(int position, boolean selected) {
            return !ignored.contains(position);
        }

        @Override
        public void onSelectionChanged() {}
    }

    private static final class TestHolder extends RecyclerView.ViewHolder {