diff --git a/FAQ.md b/FAQ.md
index a7e0939e1d..cb7cc83cc4 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -23,7 +23,7 @@ For authorizing:
## Known problems
-A [bug in Android](https://issuetracker.google.com/issues/78495471) lets FairEmail occasionally crash on long pressing or swiping.
+~~A [bug in Android](https://issuetracker.google.com/issues/78495471) lets FairEmail occasionally crash on long pressing or swiping.~~
## Planned features
diff --git a/app/build.gradle b/app/build.gradle
index 69bdc89220..7cc7963e3d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -123,7 +123,7 @@ dependencies {
// https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview
implementation "androidx.recyclerview:recyclerview:$recyclerview_version"
- implementation "androidx.recyclerview:recyclerview-selection:$recyclerview_version"
+ //implementation "androidx.recyclerview:recyclerview-selection:$recyclerview_version"
// https://mvnrepository.com/artifact/androidx.coordinatorlayout/coordinatorlayout
implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorlayout_version"
diff --git a/app/src/main/java/androidx/recyclerview/selection/AutoScroller.java b/app/src/main/java/androidx/recyclerview/selection/AutoScroller.java
new file mode 100644
index 0000000000..c3112c44b6
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/AutoScroller.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.graphics.Point;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Provides support for auto-scrolling a view.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class AutoScroller {
+
+ /**
+ * Resets state of the scroller. Call this when the user activity that is driving
+ * auto-scrolling is done.
+ */
+ public abstract void reset();
+
+ /**
+ * Processes a new input location.
+ * @param location
+ */
+ public abstract void scroll(@NonNull Point location);
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/BandPredicate.java b/app/src/main/java/androidx/recyclerview/selection/BandPredicate.java
new file mode 100644
index 0000000000..1ee3d73801
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/BandPredicate.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Provides a means of controlling when and where band selection can be initiated.
+ *
+ *
+ * Two default implementations are provided: {@link EmptyArea}, and {@link NonDraggableArea}.
+ *
+ * @see SelectionTracker.Builder#withBandPredicate(BandPredicate)
+ */
+public abstract class BandPredicate {
+
+ /**
+ * @return true if band selection can be initiated in response to the {@link MotionEvent}.
+ */
+ public abstract boolean canInitiate(MotionEvent e);
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ static boolean hasSupportedLayoutManager(@NonNull RecyclerView recyclerView) {
+ RecyclerView.LayoutManager lm = recyclerView.getLayoutManager();
+ return lm instanceof GridLayoutManager
+ || lm instanceof LinearLayoutManager;
+ }
+
+ /**
+ * A BandPredicate that allows initiation of band selection only in areas of RecyclerView
+ * that map to {@link RecyclerView#NO_POSITION}. In most cases, this will be the empty areas
+ * between views.
+ *
+ *
+ * Use this implementation to permit band selection only in empty areas
+ * surrounding view items. But be advised that if there is no empy area around
+ * view items, band selection cannot be initiated.
+ */
+ public static final class EmptyArea extends BandPredicate {
+
+ private final RecyclerView mRecyclerView;
+
+ /**
+ * @param recyclerView the owner RecyclerView
+ */
+ public EmptyArea(@NonNull RecyclerView recyclerView) {
+ checkArgument(recyclerView != null);
+
+ mRecyclerView = recyclerView;
+ }
+
+ @Override
+ public boolean canInitiate(@NonNull MotionEvent e) {
+ if (!hasSupportedLayoutManager(mRecyclerView)
+ || mRecyclerView.hasPendingAdapterUpdates()) {
+ return false;
+ }
+
+ View itemView = mRecyclerView.findChildViewUnder(e.getX(), e.getY());
+ int position = itemView != null
+ ? mRecyclerView.getChildAdapterPosition(itemView)
+ : RecyclerView.NO_POSITION;
+
+ return position == RecyclerView.NO_POSITION;
+ }
+ }
+
+ /**
+ * A BandPredicate that allows initiation of band selection in any area that is not
+ * draggable as determined by consulting
+ * {@link ItemDetailsLookup.ItemDetails#inDragRegion(MotionEvent)}. By default empty
+ * areas (those with a position that maps to {@link RecyclerView#NO_POSITION}
+ * are considered non-draggable.
+ *
+ *
+ * Use this implementation in order to permit band selection in
+ * otherwise empty areas of a View. This is useful especially in
+ * list layouts where there is no empty space surrounding the list items,
+ * and individual list items may contain extra white space (like
+ * in a list of varying length words).
+ *
+ * @see ItemDetailsLookup.ItemDetails#inDragRegion(MotionEvent)
+ */
+ public static final class NonDraggableArea extends BandPredicate {
+
+ private final RecyclerView mRecyclerView;
+ private final ItemDetailsLookup mDetailsLookup;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param recyclerView the owner RecyclerView
+ * @param detailsLookup provides access to item details.
+ */
+ public NonDraggableArea(
+ @NonNull RecyclerView recyclerView, @NonNull ItemDetailsLookup detailsLookup) {
+
+ checkArgument(recyclerView != null);
+ checkArgument(detailsLookup != null);
+
+ mRecyclerView = recyclerView;
+ mDetailsLookup = detailsLookup;
+ }
+
+ @Override
+ public boolean canInitiate(@NonNull MotionEvent e) {
+ if (!hasSupportedLayoutManager(mRecyclerView)
+ || mRecyclerView.hasPendingAdapterUpdates()) {
+ return false;
+ }
+
+ @Nullable ItemDetailsLookup.ItemDetails details = mDetailsLookup.getItemDetails(e);
+ return (details == null) || !details.inDragRegion(e);
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/BandSelectionHelper.java b/app/src/main/java/androidx/recyclerview/selection/BandSelectionHelper.java
new file mode 100644
index 0000000000..495b63f2d0
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/BandSelectionHelper.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+import static androidx.core.util.Preconditions.checkState;
+import static androidx.recyclerview.selection.Shared.VERBOSE;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
+import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
+
+import java.util.Set;
+
+/**
+ * Provides mouse driven band-selection support when used in conjunction with a {@link RecyclerView}
+ * instance. This class is responsible for rendering a band overlay and manipulating selection
+ * status of the items it intersects with.
+ *
+ *
+ * Given the recycling nature of RecyclerView items that have scrolled off-screen would not
+ * be selectable with a band that itself was partially rendered off-screen. To address this,
+ * BandSelectionController builds a model of the list/grid information presented by RecyclerView as
+ * the user interacts with items using their pointer (and the band). Selectable items that intersect
+ * with the band, both on and off screen, are selected on pointer up.
+ *
+ * @see SelectionTracker.Builder#withPointerTooltypes(int...) for details on the specific
+ * tooltypes routed to this helper.
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+class BandSelectionHelper implements OnItemTouchListener {
+
+ static final String TAG = "BandSelectionHelper";
+ static final boolean DEBUG = false;
+
+ private final BandHost mHost;
+ private final ItemKeyProvider mKeyProvider;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final SelectionTracker mSelectionTracker;
+ private final BandPredicate mBandPredicate;
+ private final FocusDelegate mFocusDelegate;
+ private final OperationMonitor mLock;
+ private final AutoScroller mScroller;
+ private final GridModel.SelectionObserver mGridObserver;
+
+ private @Nullable Point mCurrentPosition;
+ private @Nullable Point mOrigin;
+ private @Nullable GridModel mModel;
+
+ /**
+ * See {@link BandSelectionHelper#create}.
+ */
+ BandSelectionHelper(
+ @NonNull BandHost host,
+ @NonNull AutoScroller scroller,
+ @NonNull ItemKeyProvider keyProvider,
+ @NonNull SelectionTracker selectionTracker,
+ @NonNull BandPredicate bandPredicate,
+ @NonNull FocusDelegate focusDelegate,
+ @NonNull OperationMonitor lock) {
+
+ checkArgument(host != null);
+ checkArgument(scroller != null);
+ checkArgument(keyProvider != null);
+ checkArgument(selectionTracker != null);
+ checkArgument(bandPredicate != null);
+ checkArgument(focusDelegate != null);
+ checkArgument(lock != null);
+
+ mHost = host;
+ mKeyProvider = keyProvider;
+ mSelectionTracker = selectionTracker;
+ mBandPredicate = bandPredicate;
+ mFocusDelegate = focusDelegate;
+ mLock = lock;
+
+ mHost.addOnScrollListener(
+ new OnScrollListener() {
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ BandSelectionHelper.this.onScrolled(recyclerView, dx, dy);
+ }
+ });
+
+ mScroller = scroller;
+
+ mGridObserver = new GridModel.SelectionObserver() {
+ @Override
+ public void onSelectionChanged(Set updatedSelection) {
+ mSelectionTracker.setProvisionalSelection(updatedSelection);
+ }
+ };
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @return new BandSelectionHelper instance.
+ */
+ static BandSelectionHelper create(
+ @NonNull RecyclerView recyclerView,
+ @NonNull AutoScroller scroller,
+ @DrawableRes int bandOverlayId,
+ @NonNull ItemKeyProvider keyProvider,
+ @NonNull SelectionTracker selectionTracker,
+ @NonNull SelectionPredicate selectionPredicate,
+ @NonNull BandPredicate bandPredicate,
+ @NonNull FocusDelegate focusDelegate,
+ @NonNull OperationMonitor lock) {
+
+ return new BandSelectionHelper<>(
+ new DefaultBandHost<>(recyclerView, bandOverlayId, keyProvider, selectionPredicate),
+ scroller,
+ keyProvider,
+ selectionTracker,
+ bandPredicate,
+ focusDelegate,
+ lock);
+ }
+
+ @VisibleForTesting
+ boolean isActive() {
+ boolean active = mModel != null;
+ if (DEBUG && active) {
+ mLock.checkStarted();
+ }
+ return active;
+ }
+
+ /**
+ * Clients must call reset when there are any material changes to the layout of items
+ * in RecyclerView.
+ */
+ void reset() {
+ if (!isActive()) {
+ return;
+ }
+
+ mHost.hideBand();
+ if (mModel != null) {
+ mModel.stopCapturing();
+ mModel.onDestroy();
+ }
+
+ mModel = null;
+ mOrigin = null;
+
+ mScroller.reset();
+ mLock.stop();
+ }
+
+ @VisibleForTesting
+ boolean shouldStart(@NonNull MotionEvent e) {
+ // b/30146357 && b/23793622. onInterceptTouchEvent does not dispatch events to onTouchEvent
+ // unless the event is != ACTION_DOWN. Thus, we need to actually start band selection when
+ // mouse moves.
+ return MotionEvents.isPrimaryMouseButtonPressed(e)
+ && MotionEvents.isActionMove(e)
+ && mBandPredicate.canInitiate(e)
+ && !isActive();
+ }
+
+ @VisibleForTesting
+ boolean shouldStop(@NonNull MotionEvent e) {
+ return isActive()
+ && (MotionEvents.isActionUp(e)
+ || MotionEvents.isActionPointerUp(e)
+ || MotionEvents.isActionCancel(e));
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
+ if (shouldStart(e)) {
+ startBandSelect(e);
+ } else if (shouldStop(e)) {
+ endBandSelect();
+ }
+
+ return isActive();
+ }
+
+ /**
+ * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
+ */
+ @Override
+ public void onTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
+ if (shouldStop(e)) {
+ endBandSelect();
+ return;
+ }
+
+ // We shouldn't get any events in this method when band select is not active,
+ // but it turns some guests show up late to the party.
+ // Probably happening when a re-layout is happening to the ReyclerView (ie. Pull-To-Refresh)
+ if (!isActive()) {
+ return;
+ }
+
+ if (DEBUG) {
+ checkArgument(MotionEvents.isActionMove(e));
+ checkState(mModel != null);
+ }
+
+ mCurrentPosition = MotionEvents.getOrigin(e);
+
+ mModel.resizeSelection(mCurrentPosition);
+
+ resizeBand();
+ mScroller.scroll(mCurrentPosition);
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ }
+
+ /**
+ * Starts band select by adding the drawable to the RecyclerView's overlay.
+ */
+ private void startBandSelect(@NonNull MotionEvent e) {
+ checkState(!isActive());
+
+ if (!MotionEvents.isCtrlKeyPressed(e)) {
+ mSelectionTracker.clearSelection();
+ }
+
+ Point origin = MotionEvents.getOrigin(e);
+ if (DEBUG) Log.d(TAG, "Starting band select @ " + origin);
+
+ mModel = mHost.createGridModel();
+ mModel.addOnSelectionChangedListener(mGridObserver);
+
+ mLock.start();
+ mFocusDelegate.clearFocus();
+ mOrigin = origin;
+ // NOTE: Pay heed that resizeBand modifies the y coordinates
+ // in onScrolled. Not sure if model expects this. If not
+ // it should be defending against this.
+ mModel.startCapturing(mOrigin);
+ }
+
+ /**
+ * Resizes the band select rectangle by using the origin and the current pointer position as
+ * two opposite corners of the selection.
+ */
+ private void resizeBand() {
+ Rect bounds = new Rect(Math.min(mOrigin.x, mCurrentPosition.x),
+ Math.min(mOrigin.y, mCurrentPosition.y),
+ Math.max(mOrigin.x, mCurrentPosition.x),
+ Math.max(mOrigin.y, mCurrentPosition.y));
+
+ if (VERBOSE) Log.v(TAG, "Resizing band! " + bounds);
+ mHost.showBand(bounds);
+ }
+
+ /**
+ * Ends band select by removing the overlay.
+ */
+ private void endBandSelect() {
+ if (DEBUG) {
+ Log.d(TAG, "Ending band select.");
+ checkState(mModel != null);
+ }
+
+ // TODO: Currently when a band select operation ends outside
+ // of an item (e.g. in the empty area between items),
+ // getPositionNearestOrigin may return an unselected item.
+ // Since the point of this code is to establish the
+ // anchor point for subsequent range operations (SHIFT+CLICK)
+ // we really want to do a better job figuring out the last
+ // item selected (and nearest to the cursor).
+ int firstSelected = mModel.getPositionNearestOrigin();
+ if (firstSelected != GridModel.NOT_SET
+ && mSelectionTracker.isSelected(mKeyProvider.getKey(firstSelected))) {
+ // Establish the band selection point as range anchor. This
+ // allows touch and keyboard based selection activities
+ // to be based on the band selection anchor point.
+ mSelectionTracker.anchorRange(firstSelected);
+ }
+
+ mSelectionTracker.mergeProvisionalSelection();
+ reset();
+ }
+
+ /**
+ * @see OnScrollListener
+ */
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ if (!isActive()) {
+ return;
+ }
+
+ // Adjust the y-coordinate of the origin the opposite number of pixels so that the
+ // origin remains in the same place relative to the view's items.
+ mOrigin.y -= dy;
+ resizeBand();
+ }
+
+ /**
+ * Provides functionality for BandController. Exists primarily to tests that are
+ * fully isolated from RecyclerView.
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+ abstract static class BandHost {
+
+ /**
+ * Returns a new GridModel instance.
+ */
+ abstract GridModel createGridModel();
+
+ /**
+ * Show the band covering the bounds.
+ *
+ * @param bounds The boundaries of the band to show.
+ */
+ abstract void showBand(@NonNull Rect bounds);
+
+ /**
+ * Hide the band.
+ */
+ abstract void hideBand();
+
+ /**
+ * Add a listener to be notified on scroll events.
+ *
+ * @param listener
+ */
+ abstract void addOnScrollListener(@NonNull OnScrollListener listener);
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/DefaultBandHost.java b/app/src/main/java/androidx/recyclerview/selection/DefaultBandHost.java
new file mode 100644
index 0000000000..bb1a034de5
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/DefaultBandHost.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
+import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
+
+/**
+ * RecyclerView backed {@link BandSelectionHelper.BandHost}.
+ */
+final class DefaultBandHost extends GridModel.GridHost {
+
+ private static final Rect NILL_RECT = new Rect(0, 0, 0, 0);
+
+ private final RecyclerView mRecyclerView;
+ private final Drawable mBand;
+ private final ItemKeyProvider mKeyProvider;
+ private final SelectionPredicate mSelectionPredicate;
+
+ DefaultBandHost(
+ @NonNull RecyclerView recyclerView,
+ @DrawableRes int bandOverlayId,
+ @NonNull ItemKeyProvider keyProvider,
+ @NonNull SelectionPredicate selectionPredicate) {
+
+ checkArgument(recyclerView != null);
+
+ mRecyclerView = recyclerView;
+ mBand = mRecyclerView.getContext().getResources().getDrawable(bandOverlayId);
+
+ checkArgument(mBand != null);
+ checkArgument(keyProvider != null);
+ checkArgument(selectionPredicate != null);
+
+ mKeyProvider = keyProvider;
+ mSelectionPredicate = selectionPredicate;
+
+ mRecyclerView.addItemDecoration(
+ new ItemDecoration() {
+ @Override
+ public void onDrawOver(
+ Canvas canvas,
+ RecyclerView unusedParent,
+ RecyclerView.State unusedState) {
+ DefaultBandHost.this.onDrawBand(canvas);
+ }
+ });
+ }
+
+ @Override
+ GridModel createGridModel() {
+ return new GridModel<>(this, mKeyProvider, mSelectionPredicate);
+ }
+
+ @Override
+ int getAdapterPositionAt(int index) {
+ return mRecyclerView.getChildAdapterPosition(mRecyclerView.getChildAt(index));
+ }
+
+ @Override
+ void addOnScrollListener(@NonNull OnScrollListener listener) {
+ mRecyclerView.addOnScrollListener(listener);
+ }
+
+ @Override
+ void removeOnScrollListener(@NonNull OnScrollListener listener) {
+ mRecyclerView.removeOnScrollListener(listener);
+ }
+
+ @Override
+ Point createAbsolutePoint(@NonNull Point relativePoint) {
+ return new Point(relativePoint.x + mRecyclerView.computeHorizontalScrollOffset(),
+ relativePoint.y + mRecyclerView.computeVerticalScrollOffset());
+ }
+
+ @Override
+ Rect getAbsoluteRectForChildViewAt(int index) {
+ final View child = mRecyclerView.getChildAt(index);
+ final Rect childRect = new Rect();
+ child.getHitRect(childRect);
+ childRect.left += mRecyclerView.computeHorizontalScrollOffset();
+ childRect.right += mRecyclerView.computeHorizontalScrollOffset();
+ childRect.top += mRecyclerView.computeVerticalScrollOffset();
+ childRect.bottom += mRecyclerView.computeVerticalScrollOffset();
+ return childRect;
+ }
+
+ @Override
+ int getVisibleChildCount() {
+ return mRecyclerView.getChildCount();
+ }
+
+ @Override
+ int getColumnCount() {
+ RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+ if (layoutManager instanceof GridLayoutManager) {
+ return ((GridLayoutManager) layoutManager).getSpanCount();
+ }
+
+ // Otherwise, it is a list with 1 column.
+ return 1;
+ }
+
+ @Override
+ void showBand(@NonNull Rect rect) {
+ mBand.setBounds(rect);
+ // TODO: mRecyclerView.invalidateItemDecorations() should work, but it isn't currently.
+ // NOTE: That without invalidating rv, the band only gets updated
+ // when the pointer moves off a the item view into "NO_POSITION" territory.
+ mRecyclerView.invalidate();
+ }
+
+ @Override
+ void hideBand() {
+ mBand.setBounds(NILL_RECT);
+ // TODO: mRecyclerView.invalidateItemDecorations() should work, but it isn't currently.
+ mRecyclerView.invalidate();
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ void onDrawBand(@NonNull Canvas c) {
+ mBand.draw(c);
+ }
+
+ @Override
+ boolean hasView(int pos) {
+ return mRecyclerView.findViewHolderForAdapterPosition(pos) != null;
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/DefaultSelectionTracker.java b/app/src/main/java/androidx/recyclerview/selection/DefaultSelectionTracker.java
new file mode 100644
index 0000000000..014d2c64fa
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/DefaultSelectionTracker.java
@@ -0,0 +1,589 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.core.util.Preconditions.checkArgument;
+import static androidx.core.util.Preconditions.checkState;
+import static androidx.recyclerview.selection.Shared.DEBUG;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.recyclerview.selection.Range.RangeType;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * {@link SelectionTracker} providing support for traditional multi-item selection on top
+ * of {@link RecyclerView}.
+ *
+ *
+ * The class supports running in a single-select mode, which can be enabled using
+ * {@link SelectionPredicate#canSelectMultiple()}.
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public class DefaultSelectionTracker extends SelectionTracker {
+
+ private static final String TAG = "DefaultSelectionTracker";
+ private static final String EXTRA_SELECTION_PREFIX = "androidx.recyclerview.selection";
+
+ private final Selection mSelection = new Selection<>();
+ private final List mObservers = new ArrayList<>(1);
+ private final ItemKeyProvider mKeyProvider;
+ private final SelectionPredicate mSelectionPredicate;
+ private final StorageStrategy mStorage;
+ private final RangeCallbacks mRangeCallbacks;
+ private final AdapterObserver mAdapterObserver;
+ private final boolean mSingleSelect;
+ private final String mSelectionId;
+
+ private @Nullable Range mRange;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param selectionId A unique string identifying this selection in the context
+ * of the activity or fragment.
+ * @param keyProvider client supplied class providing access to stable ids.
+ * @param selectionPredicate A predicate allowing the client to disallow selection
+ * @param storage Strategy for storing typed selection in bundle.
+ */
+ public DefaultSelectionTracker(
+ @NonNull String selectionId,
+ @NonNull ItemKeyProvider keyProvider,
+ @NonNull SelectionPredicate selectionPredicate,
+ @NonNull StorageStrategy storage) {
+
+ checkArgument(selectionId != null);
+ checkArgument(!selectionId.trim().isEmpty());
+ checkArgument(keyProvider != null);
+ checkArgument(selectionPredicate != null);
+ checkArgument(storage != null);
+
+ mSelectionId = selectionId;
+ mKeyProvider = keyProvider;
+ mSelectionPredicate = selectionPredicate;
+ mStorage = storage;
+
+ mRangeCallbacks = new RangeCallbacks();
+
+ mSingleSelect = !selectionPredicate.canSelectMultiple();
+
+ mAdapterObserver = new AdapterObserver(this);
+ }
+
+ @Override
+ public void addObserver(@NonNull SelectionObserver callback) {
+ checkArgument(callback != null);
+ mObservers.add(callback);
+ }
+
+ @Override
+ public boolean hasSelection() {
+ return !mSelection.isEmpty();
+ }
+
+ @Override
+ public Selection getSelection() {
+ return mSelection;
+ }
+
+ @Override
+ public void copySelection(@NonNull MutableSelection dest) {
+ dest.copyFrom(mSelection);
+ }
+
+ @Override
+ public boolean isSelected(@Nullable K key) {
+ return mSelection.contains(key);
+ }
+
+ @Override
+ protected void restoreSelection(@NonNull Selection other) {
+ checkArgument(other != null);
+ setItemsSelectedQuietly(other.mSelection, true);
+ // NOTE: We intentionally don't restore provisional selection. It's provisional.
+ notifySelectionRestored();
+ }
+
+ @Override
+ public boolean setItemsSelected(@NonNull Iterable keys, boolean selected) {
+ boolean changed = setItemsSelectedQuietly(keys, selected);
+ notifySelectionChanged();
+ return changed;
+ }
+
+ private boolean setItemsSelectedQuietly(@NonNull Iterable keys, boolean selected) {
+ boolean changed = false;
+ for (K key: keys) {
+ boolean itemChanged = selected
+ ? canSetState(key, true) && mSelection.add(key)
+ : canSetState(key, false) && mSelection.remove(key);
+ if (itemChanged) {
+ notifyItemStateChanged(key, selected);
+ }
+ changed |= itemChanged;
+ }
+ return changed;
+ }
+
+ @Override
+ public boolean clearSelection() {
+ if (!hasSelection()) {
+ return false;
+ }
+
+ clearProvisionalSelection();
+ clearPrimarySelection();
+ return true;
+ }
+
+ private void clearPrimarySelection() {
+ if (!hasSelection()) {
+ return;
+ }
+
+ Selection prev = clearSelectionQuietly();
+ notifySelectionCleared(prev);
+ notifySelectionChanged();
+ }
+
+ /**
+ * Clears the selection, without notifying selection listeners.
+ * Returns items in previous selection. Callers are responsible for notifying
+ * listeners about changes.
+ */
+ private Selection clearSelectionQuietly() {
+ mRange = null;
+
+ MutableSelection prevSelection = new MutableSelection();
+ if (hasSelection()) {
+ copySelection(prevSelection);
+ mSelection.clear();
+ }
+
+ return prevSelection;
+ }
+
+ @Override
+ public boolean select(@NonNull K key) {
+ checkArgument(key != null);
+
+ if (mSelection.contains(key)) {
+ return false;
+ }
+
+ if (!canSetState(key, true)) {
+ if (DEBUG) Log.d(TAG, "Select cancelled by selection predicate test.");
+ return false;
+ }
+
+ // Enforce single selection policy.
+ if (mSingleSelect && hasSelection()) {
+ Selection prev = clearSelectionQuietly();
+ notifySelectionCleared(prev);
+ }
+
+ mSelection.add(key);
+ notifyItemStateChanged(key, true);
+ notifySelectionChanged();
+
+ return true;
+ }
+
+ @Override
+ public boolean deselect(@NonNull K key) {
+ checkArgument(key != null);
+
+ if (mSelection.contains(key)) {
+ if (!canSetState(key, false)) {
+ if (DEBUG) Log.d(TAG, "Deselect cancelled by selection predicate test.");
+ return false;
+ }
+ mSelection.remove(key);
+ notifyItemStateChanged(key, false);
+ notifySelectionChanged();
+ if (mSelection.isEmpty() && isRangeActive()) {
+ // if there's nothing in the selection and there is an active ranger it results
+ // in unexpected behavior when the user tries to start range selection: the item
+ // which the ranger 'thinks' is the already selected anchor becomes unselectable
+ endRange();
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void startRange(int position) {
+ if (mSelection.contains(mKeyProvider.getKey(position))
+ || select(mKeyProvider.getKey(position))) {
+ anchorRange(position);
+ }
+ }
+
+ @Override
+ public void extendRange(int position) {
+ extendRange(position, Range.TYPE_PRIMARY);
+ }
+
+ @Override
+ public void endRange() {
+ mRange = null;
+ // Clean up in case there was any leftover provisional selection
+ clearProvisionalSelection();
+ }
+
+ @Override
+ public void anchorRange(int position) {
+ checkArgument(position != RecyclerView.NO_POSITION);
+ checkArgument(mSelection.contains(mKeyProvider.getKey(position)));
+
+ mRange = new Range(position, mRangeCallbacks);
+ }
+
+ @Override
+ public void extendProvisionalRange(int position) {
+ if (mSingleSelect) {
+ return;
+ }
+
+ if (DEBUG) Log.i(TAG, "Extending provision range to position: " + position);
+ checkState(isRangeActive(), "Range start point not set.");
+ extendRange(position, Range.TYPE_PROVISIONAL);
+ }
+
+ /**
+ * Sets the end point for the current range selection, started by a call to
+ * {@link #startRange(int)}. This function should only be called when a range selection
+ * is active (see {@link #isRangeActive()}. Items in the range [anchor, end] will be
+ * selected or in provisional select, depending on the type supplied. Note that if the type is
+ * provisional selection, one should do {@link #mergeProvisionalSelection()} at some
+ * point before calling on {@link #endRange()}.
+ *
+ * @param position The new end position for the selection range.
+ * @param type The type of selection the range should utilize.
+ */
+ private void extendRange(int position, @RangeType int type) {
+ checkState(isRangeActive(), "Range start point not set.");
+
+ mRange.extendRange(position, 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();
+ }
+
+ @Override
+ public void setProvisionalSelection(@NonNull Set newSelection) {
+ if (mSingleSelect) {
+ return;
+ }
+
+ Map delta = mSelection.setProvisionalSelection(newSelection);
+ for (Map.Entry entry: delta.entrySet()) {
+ notifyItemStateChanged(entry.getKey(), entry.getValue());
+ }
+
+ notifySelectionChanged();
+ }
+
+ @Override
+ public void mergeProvisionalSelection() {
+ mSelection.mergeProvisionalSelection();
+
+ // Note, that for almost all functional purposes, merging a provisional selection
+ // into a the primary selection doesn't change the selection, just an internal
+ // representation of it. But there are some nuanced areas cases where
+ // that isn't true. equality for 1. So, we notify regardless.
+
+ notifySelectionChanged();
+ }
+
+ @Override
+ public void clearProvisionalSelection() {
+ for (K key : mSelection.mProvisionalSelection) {
+ notifyItemStateChanged(key, false);
+ }
+ mSelection.clearProvisionalSelection();
+ }
+
+ @Override
+ public boolean isRangeActive() {
+ return mRange != null;
+ }
+
+ private boolean canSetState(@NonNull K key, boolean nextState) {
+ return mSelectionPredicate.canSetStateForKey(key, nextState);
+ }
+
+ @Override
+ AdapterDataObserver getAdapterDataObserver() {
+ return mAdapterObserver;
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ void onDataSetChanged() {
+ mSelection.clearProvisionalSelection();
+
+ notifySelectionRefresh();
+
+ List toRemove = null;
+ for (K key : mSelection) {
+ // If the underlying data set has changed, before restoring
+ // selection we must re-verify that it can be selected.
+ // Why? Because if the dataset has changed, then maybe the
+ // selectability of an item has changed.
+ if (!canSetState(key, true)) {
+ if (toRemove == null) {
+ toRemove = new ArrayList<>();
+ }
+ toRemove.add(key);
+ } else {
+ int lastListener = mObservers.size() - 1;
+ for (int i = lastListener; i >= 0; i--) {
+ mObservers.get(i).onItemStateChanged(key, true);
+ }
+ }
+
+ }
+
+ if (toRemove != null) {
+ for (K key : toRemove) {
+ deselect(key);
+ }
+ }
+
+ notifySelectionChanged();
+ }
+
+ /**
+ * Notifies registered listeners when the selection status of a single item
+ * (identified by {@code position}) changes.
+ */
+ private void notifyItemStateChanged(@NonNull K key, boolean selected) {
+ checkArgument(key != null);
+
+ int lastListenerIndex = mObservers.size() - 1;
+ for (int i = lastListenerIndex; i >= 0; i--) {
+ mObservers.get(i).onItemStateChanged(key, selected);
+ }
+ }
+
+ private void notifySelectionCleared(@NonNull Selection selection) {
+ for (K key: selection.mSelection) {
+ notifyItemStateChanged(key, false);
+ }
+ for (K key: selection.mProvisionalSelection) {
+ notifyItemStateChanged(key, false);
+ }
+ }
+
+ /**
+ * 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 lastListenerIndex = mObservers.size() - 1;
+ for (int i = lastListenerIndex; i >= 0; i--) {
+ mObservers.get(i).onSelectionChanged();
+ }
+ }
+
+ private void notifySelectionRestored() {
+ int lastListenerIndex = mObservers.size() - 1;
+ for (int i = lastListenerIndex; i >= 0; i--) {
+ mObservers.get(i).onSelectionRestored();
+ }
+ }
+
+ private void notifySelectionRefresh() {
+ int lastListenerIndex = mObservers.size() - 1;
+ for (int i = lastListenerIndex; i >= 0; i--) {
+ mObservers.get(i).onSelectionRefresh();
+ }
+ }
+
+ private void updateForRange(int begin, int end, boolean selected, @RangeType int type) {
+ switch (type) {
+ case Range.TYPE_PRIMARY:
+ updateForRegularRange(begin, end, selected);
+ break;
+ case Range.TYPE_PROVISIONAL:
+ updateForProvisionalRange(begin, end, selected);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid range type: " + type);
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ void updateForRegularRange(int begin, int end, boolean selected) {
+ checkArgument(end >= begin);
+
+ for (int i = begin; i <= end; i++) {
+ K key = mKeyProvider.getKey(i);
+ if (key == null) {
+ continue;
+ }
+
+ if (selected) {
+ select(key);
+ } else {
+ deselect(key);
+ }
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ void updateForProvisionalRange(int begin, int end, boolean selected) {
+ checkArgument(end >= begin);
+
+ for (int i = begin; i <= end; i++) {
+ K key = mKeyProvider.getKey(i);
+ if (key == null) {
+ continue;
+ }
+
+ boolean changedState = false;
+ if (selected) {
+ boolean canSelect = canSetState(key, true);
+ if (canSelect && !mSelection.mSelection.contains(key)) {
+ mSelection.mProvisionalSelection.add(key);
+ changedState = true;
+ }
+ } else {
+ mSelection.mProvisionalSelection.remove(key);
+ changedState = true;
+ }
+
+ // Only notify item callbacks when something's state is actually changed in provisional
+ // selection.
+ if (changedState) {
+ notifyItemStateChanged(key, selected);
+ }
+ }
+
+ notifySelectionChanged();
+ }
+
+ @VisibleForTesting
+ String getInstanceStateKey() {
+ return EXTRA_SELECTION_PREFIX + ":" + mSelectionId;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public final void onSaveInstanceState(@NonNull Bundle state) {
+ if (mSelection.isEmpty()) {
+ return;
+ }
+
+ state.putBundle(getInstanceStateKey(), mStorage.asBundle(mSelection));
+ }
+
+ @Override
+ public final void onRestoreInstanceState(@Nullable Bundle state) {
+ if (state == null) {
+ return;
+ }
+
+ @Nullable Bundle selectionState = state.getBundle(getInstanceStateKey());
+ if (selectionState == null) {
+ return;
+ }
+
+ Selection selection = mStorage.asSelection(selectionState);
+ if (selection != null && !selection.isEmpty()) {
+ restoreSelection(selection);
+ }
+ }
+
+ private final class RangeCallbacks extends Range.Callbacks {
+ RangeCallbacks() {
+ }
+
+ @Override
+ void updateForRange(int begin, int end, boolean selected, int type) {
+ switch (type) {
+ case Range.TYPE_PRIMARY:
+ updateForRegularRange(begin, end, selected);
+ break;
+ case Range.TYPE_PROVISIONAL:
+ updateForProvisionalRange(begin, end, selected);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid range type: " + type);
+ }
+ }
+ }
+
+ private static final class AdapterObserver extends AdapterDataObserver {
+
+ private final DefaultSelectionTracker> mSelectionTracker;
+
+ AdapterObserver(@NonNull DefaultSelectionTracker> selectionTracker) {
+ checkArgument(selectionTracker != null);
+ mSelectionTracker = selectionTracker;
+ }
+
+ @Override
+ public void onChanged() {
+ mSelectionTracker.onDataSetChanged();
+ }
+
+ @Override
+ public void onItemRangeChanged(int startPosition, int itemCount, @Nullable Object payload) {
+ if (!SelectionTracker.SELECTION_CHANGED_MARKER.equals(payload)) {
+ mSelectionTracker.onDataSetChanged();
+ }
+ }
+
+ @Override
+ public void onItemRangeInserted(int startPosition, int itemCount) {
+ mSelectionTracker.endRange();
+ }
+
+ @Override
+ public void onItemRangeRemoved(int startPosition, int itemCount) {
+ mSelectionTracker.endRange();
+ }
+
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ mSelectionTracker.endRange();
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/EventBridge.java b/app/src/main/java/androidx/recyclerview/selection/EventBridge.java
new file mode 100644
index 0000000000..6c43f7e09a
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/EventBridge.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
+import static androidx.core.util.Preconditions.checkArgument;
+import static androidx.recyclerview.selection.Shared.VERBOSE;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Provides the necessary glue to notify RecyclerView when selection data changes,
+ * and to notify SelectionTracker when the underlying RecyclerView.Adapter data changes.
+ *
+ * This strict decoupling is necessary to permit a single SelectionTracker to work
+ * with multiple RecyclerView instances. This may be necessary when multiple
+ * different views of data are presented to the user.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
+public class EventBridge {
+
+ private static final String TAG = "EventsRelays";
+
+ /**
+ * Installs the event bridge for on the supplied adapter/helper.
+ *
+ * @param adapter
+ * @param selectionTracker
+ * @param keyProvider
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+ public static void install(
+ @NonNull RecyclerView.Adapter> adapter,
+ @NonNull SelectionTracker selectionTracker,
+ @NonNull ItemKeyProvider keyProvider) {
+
+ // setup bridges to relay selection and adapter events
+ new TrackerToAdapterBridge<>(selectionTracker, keyProvider, adapter);
+ adapter.registerAdapterDataObserver(selectionTracker.getAdapterDataObserver());
+ }
+
+ private static final class TrackerToAdapterBridge
+ extends SelectionTracker.SelectionObserver {
+
+ private final ItemKeyProvider mKeyProvider;
+ private final RecyclerView.Adapter> mAdapter;
+
+ TrackerToAdapterBridge(
+ @NonNull SelectionTracker selectionTracker,
+ @NonNull ItemKeyProvider keyProvider,
+ @NonNull RecyclerView.Adapter> adapter) {
+
+ selectionTracker.addObserver(this);
+
+ checkArgument(keyProvider != null);
+ checkArgument(adapter != null);
+
+ mKeyProvider = keyProvider;
+ mAdapter = adapter;
+ }
+
+ /**
+ * Called when state of an item has been changed.
+ */
+ @Override
+ public void onItemStateChanged(@NonNull K key, boolean selected) {
+ int position = mKeyProvider.getPosition(key);
+ if (VERBOSE) Log.v(TAG, "ITEM " + key + " CHANGED at pos: " + position);
+
+ if (position < 0) {
+ Log.w(TAG, "Item change notification received for unknown item: " + key);
+ return;
+ }
+
+ mAdapter.notifyItemChanged(position, SelectionTracker.SELECTION_CHANGED_MARKER);
+ }
+ }
+
+ private EventBridge() {
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/FocusDelegate.java b/app/src/main/java/androidx/recyclerview/selection/FocusDelegate.java
new file mode 100644
index 0000000000..ca69c85214
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/FocusDelegate.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Override methods in this class to provide application specific behaviors
+ * related to focusing item.
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+public abstract class FocusDelegate {
+
+ static FocusDelegate dummy() {
+ return new FocusDelegate() {
+ @Override
+ public void focusItem(@NonNull ItemDetails item) {
+ }
+
+ @Override
+ public boolean hasFocusedItem() {
+ return false;
+ }
+
+ @Override
+ public int getFocusedPosition() {
+ return RecyclerView.NO_POSITION;
+ }
+
+ @Override
+ public void clearFocus() {
+ }
+ };
+ }
+
+ /**
+ * If environment supports focus, focus {@code item}.
+ */
+ public abstract void focusItem(@NonNull ItemDetails item);
+
+ /**
+ * @return true if there is a focused item.
+ */
+ public abstract boolean hasFocusedItem();
+
+ /**
+ * @return the position of the currently focused item, if any.
+ */
+ public abstract int getFocusedPosition();
+
+ /**
+ * If the environment supports focus and something is focused, unfocus it.
+ */
+ public abstract void clearFocus();
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/GestureRouter.java b/app/src/main/java/androidx/recyclerview/selection/GestureRouter.java
new file mode 100644
index 0000000000..797f32d991
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/GestureRouter.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+
+import android.view.GestureDetector.OnDoubleTapListener;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * GestureRouter is responsible for routing gestures detected by a GestureDetector
+ * to registered handlers. The primary function is to divide events by tool-type
+ * allowing handlers to cleanly implement tool-type specific policies.
+ *
+ * @param listener type. Must extend OnGestureListener & OnDoubleTapListener.
+ */
+final class GestureRouter
+ implements OnGestureListener, OnDoubleTapListener {
+
+ private final ToolHandlerRegistry mDelegates;
+
+ GestureRouter(@NonNull T defaultDelegate) {
+ checkArgument(defaultDelegate != null);
+ mDelegates = new ToolHandlerRegistry<>(defaultDelegate);
+ }
+
+ GestureRouter() {
+ this((T) new SimpleOnGestureListener());
+ }
+
+ /**
+ * @param toolType
+ * @param delegate the delegate, or null to unregister.
+ */
+ public void register(int toolType, @Nullable T delegate) {
+ mDelegates.set(toolType, delegate);
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(@NonNull MotionEvent e) {
+ return mDelegates.get(e).onSingleTapConfirmed(e);
+ }
+
+ @Override
+ public boolean onDoubleTap(@NonNull MotionEvent e) {
+ return mDelegates.get(e).onDoubleTap(e);
+ }
+
+ @Override
+ public boolean onDoubleTapEvent(@NonNull MotionEvent e) {
+ return mDelegates.get(e).onDoubleTapEvent(e);
+ }
+
+ @Override
+ public boolean onDown(@NonNull MotionEvent e) {
+ return mDelegates.get(e).onDown(e);
+ }
+
+ @Override
+ public void onShowPress(@NonNull MotionEvent e) {
+ mDelegates.get(e).onShowPress(e);
+ }
+
+ @Override
+ public boolean onSingleTapUp(@NonNull MotionEvent e) {
+ return mDelegates.get(e).onSingleTapUp(e);
+ }
+
+ @Override
+ public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2,
+ float distanceX, float distanceY) {
+ return mDelegates.get(e2).onScroll(e1, e2, distanceX, distanceY);
+ }
+
+ @Override
+ public void onLongPress(@NonNull MotionEvent e) {
+ mDelegates.get(e).onLongPress(e);
+ }
+
+ @Override
+ public boolean onFling(@NonNull MotionEvent e1, @NonNull MotionEvent e2,
+ float velocityX, float velocityY) {
+ return mDelegates.get(e2).onFling(e1, e2, velocityX, velocityY);
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/GestureSelectionHelper.java b/app/src/main/java/androidx/recyclerview/selection/GestureSelectionHelper.java
new file mode 100644
index 0000000000..f127576102
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/GestureSelectionHelper.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+import static androidx.core.util.Preconditions.checkState;
+
+import android.graphics.Point;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.view.ViewCompat;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
+
+/**
+ * GestureSelectionHelper provides logic that interprets a combination
+ * of motions and gestures in order to provide gesture driven selection support
+ * when used in conjunction with RecyclerView and other classes in the ReyclerView
+ * selection support package.
+ */
+final class GestureSelectionHelper implements OnItemTouchListener {
+
+ private static final String TAG = "GestureSelectionHelper";
+
+ private final SelectionTracker> mSelectionMgr;
+ private final ItemDetailsLookup> mDetailsLookup;
+ private final AutoScroller mScroller;
+ private final ViewDelegate mView;
+ private final OperationMonitor mLock;
+
+ private int mLastStartedItemPos = RecyclerView.NO_POSITION;
+ private boolean mStarted = false;
+
+ /**
+ * See {@link GestureSelectionHelper#create} for convenience
+ * method.
+ */
+ GestureSelectionHelper(
+ @NonNull SelectionTracker> selectionTracker,
+ @NonNull ItemDetailsLookup> detailsLookup,
+ @NonNull ViewDelegate view,
+ @NonNull AutoScroller scroller,
+ @NonNull OperationMonitor lock) {
+
+ checkArgument(selectionTracker != null);
+ checkArgument(detailsLookup != null);
+ checkArgument(view != null);
+ checkArgument(scroller != null);
+ checkArgument(lock != null);
+
+ mSelectionMgr = selectionTracker;
+ mDetailsLookup = detailsLookup;
+ mView = view;
+ mScroller = scroller;
+ mLock = lock;
+ }
+
+ /**
+ * Explicitly kicks off a gesture multi-select.
+ */
+ void start() {
+ checkState(!mStarted);
+ // See: b/70518185. It appears start() is being called via onLongPress
+ // even though we never received an intial handleInterceptedDownEvent
+ // where we would usually initialize mLastStartedItemPos.
+ if (mLastStartedItemPos == RecyclerView.NO_POSITION) {
+ Log.w(TAG, "Illegal state. Can't start without valid mLastStartedItemPos.");
+ return;
+ }
+
+ // Partner code in MotionInputHandler ensures items
+ // are selected and range established prior to
+ // start being called.
+ // Verify the truth of that statement here
+ // to make the implicit coupling less of a time bomb.
+ checkState(mSelectionMgr.isRangeActive());
+
+ mLock.checkStopped();
+
+ mStarted = true;
+ mLock.start();
+ }
+
+ @Override
+ /** @hide */
+ public boolean onInterceptTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
+ if (MotionEvents.isMouseEvent(e)) {
+ if (Shared.DEBUG) Log.w(TAG, "Unexpected Mouse event. Check configuration.");
+ }
+
+ // TODO(b/109808552): It seems that mLastStartedItemPos should likely be set as a method
+ // parameter in start().
+ if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ if (mDetailsLookup.getItemDetails(e) != null) {
+ mLastStartedItemPos = mView.getItemUnder(e);
+ }
+ }
+
+ // See handleTouch(MotionEvent) javadoc for explanation as to why this is correct.
+ return handleTouch(e);
+ }
+
+ @Override
+ /** @hide */
+ public void onTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
+ // See handleTouch(MotionEvent) javadoc for explanation as to why this is correct.
+ handleTouch(e);
+ }
+
+ /**
+ * If selection has started, will handle all appropriate types of MotionEvents and will return
+ * true if this OnItemTouchListener should start intercepting the rest of the MotionEvents.
+ *
+ * This code, and the fact that this method is used by both OnInterceptTouchEvent and
+ * OnTouchEvent, is correct and valid because:
+ *
+ * - MotionEvents that aren't ACTION_DOWN are only ever passed to either onInterceptTouchEvent
+ * or onTouchEvent; never to both. The MotionEvents we are handling in this method are not
+ * ACTION_DOWN, and therefore, its appropriate that both the onInterceptTouchEvent and
+ * onTouchEvent code paths cross this method.
+ *
- This method returns true when we want to intercept MotionEvents. OnInterceptTouchEvent
+ * uses that information to determine its own return, and OnMotionEvent doesn't have a return
+ * so this methods return value is irrelevant to it.
+ *
+ */
+ private boolean handleTouch(MotionEvent e) {
+ if (!mStarted) {
+ return false;
+ }
+
+ switch (e.getActionMasked()) {
+ case MotionEvent.ACTION_MOVE:
+ handleMoveEvent(e);
+ return true;
+ case MotionEvent.ACTION_UP:
+ handleUpEvent();
+ return true;
+ case MotionEvent.ACTION_CANCEL:
+ handleCancelEvent();
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ /** @hide */
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ }
+
+ // Called when ACTION_UP event is to be handled.
+ // Essentially, since this means all gesture movement is over, reset everything and apply
+ // provisional selection.
+ private void handleUpEvent() {
+ mSelectionMgr.mergeProvisionalSelection();
+ endSelection();
+ if (mLastStartedItemPos != RecyclerView.NO_POSITION) {
+ mSelectionMgr.startRange(mLastStartedItemPos);
+ }
+ }
+
+ // Called when ACTION_CANCEL event is to be handled.
+ // This means this gesture selection is aborted, so reset everything and abandon provisional
+ // selection.
+ private void handleCancelEvent() {
+ mSelectionMgr.clearProvisionalSelection();
+ endSelection();
+ }
+
+ private void endSelection() {
+ checkState(mStarted);
+
+ mLastStartedItemPos = RecyclerView.NO_POSITION;
+ mStarted = false;
+ mScroller.reset();
+ mLock.stop();
+ }
+
+ // Call when an intercepted ACTION_MOVE event is passed down.
+ // At this point, we are sure user wants to gesture multi-select.
+ private void handleMoveEvent(@NonNull MotionEvent e) {
+ Point lastInterceptedPoint = MotionEvents.getOrigin(e);
+
+ int lastGlidedItemPos = mView.getLastGlidedItemPosition(e);
+ if (lastGlidedItemPos != RecyclerView.NO_POSITION) {
+ extendSelection(lastGlidedItemPos);
+ }
+
+ mScroller.scroll(lastInterceptedPoint);
+ }
+
+ // It's possible for events to go over the top/bottom of the RecyclerView.
+ // We want to get a Y-coordinate within the RecyclerView so we can find the childView underneath
+ // correctly.
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ static float getInboundY(float max, float y) {
+ if (y < 0f) {
+ return 0f;
+ } else if (y > max) {
+ return max;
+ }
+ return y;
+ }
+
+ /* Given the end position, select everything in-between.
+ * @param endPos The adapter position of the end item.
+ */
+ private void extendSelection(int endPos) {
+ mSelectionMgr.extendProvisionalRange(endPos);
+ }
+
+ /**
+ * Returns a new instance of GestureSelectionHelper.
+ */
+ static GestureSelectionHelper create(
+ @NonNull SelectionTracker> selectionMgr,
+ @NonNull ItemDetailsLookup> detailsLookup,
+ @NonNull RecyclerView recyclerView,
+ @NonNull AutoScroller scroller,
+ @NonNull OperationMonitor lock) {
+
+ return new GestureSelectionHelper(
+ selectionMgr,
+ detailsLookup,
+ new RecyclerViewDelegate(recyclerView),
+ scroller,
+ lock);
+ }
+
+ @VisibleForTesting
+ abstract static class ViewDelegate {
+ abstract int getHeight();
+
+ abstract int getItemUnder(@NonNull MotionEvent e);
+
+ abstract int getLastGlidedItemPosition(@NonNull MotionEvent e);
+ }
+
+ @VisibleForTesting
+ static final class RecyclerViewDelegate extends ViewDelegate {
+
+ private final RecyclerView mRecyclerView;
+
+ RecyclerViewDelegate(@NonNull RecyclerView recyclerView) {
+ checkArgument(recyclerView != null);
+ mRecyclerView = recyclerView;
+ }
+
+ @Override
+ int getHeight() {
+ return mRecyclerView.getHeight();
+ }
+
+ @Override
+ int getItemUnder(@NonNull MotionEvent e) {
+ View child = mRecyclerView.findChildViewUnder(e.getX(), e.getY());
+ return child != null
+ ? mRecyclerView.getChildAdapterPosition(child)
+ : RecyclerView.NO_POSITION;
+ }
+
+ @Override
+ int getLastGlidedItemPosition(@NonNull MotionEvent e) {
+ // If user has moved his pointer to the bottom-right empty pane (ie. to the right of the
+ // last item of the recycler view), we would want to set that as the currentItemPos
+ View lastItem = mRecyclerView.getLayoutManager()
+ .getChildAt(mRecyclerView.getLayoutManager().getChildCount() - 1);
+ int direction = ViewCompat.getLayoutDirection(mRecyclerView);
+ final boolean pastLastItem = isPastLastItem(lastItem.getTop(),
+ lastItem.getLeft(),
+ lastItem.getRight(),
+ e,
+ direction);
+
+ // Since views get attached & detached from RecyclerView,
+ // {@link LayoutManager#getChildCount} can return a different number from the actual
+ // number
+ // of items in the adapter. Using the adapter is the for sure way to get the actual last
+ // item position.
+ final float inboundY = getInboundY(mRecyclerView.getHeight(), e.getY());
+ return (pastLastItem) ? mRecyclerView.getAdapter().getItemCount() - 1
+ : mRecyclerView.getChildAdapterPosition(
+ mRecyclerView.findChildViewUnder(e.getX(), inboundY));
+ }
+
+ /*
+ * Check to see if MotionEvent if past a particular item, i.e. to the right or to the bottom
+ * of the item.
+ * For RTL, it would to be to the left or to the bottom of the item.
+ */
+ @VisibleForTesting
+ static boolean isPastLastItem(
+ int top, int left, int right, @NonNull MotionEvent e, int direction) {
+ if (direction == View.LAYOUT_DIRECTION_LTR) {
+ return e.getX() > right && e.getY() > top;
+ } else {
+ return e.getX() < left && e.getY() > top;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/GridModel.java b/app/src/main/java/androidx/recyclerview/selection/GridModel.java
new file mode 100644
index 0000000000..406ae618ff
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/GridModel.java
@@ -0,0 +1,795 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Provides a band selection item model for views within a RecyclerView. This class queries the
+ * RecyclerView to determine where its items are placed; then, once band selection is underway,
+ * it alerts listeners of which items are covered by the selections.
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+final class GridModel {
+
+ // Magical value indicating that a value has not been previously set. primitive null :)
+ static final int NOT_SET = -1;
+
+ // Enum values used to determine the corner at which the origin is located within the
+ private static final int UPPER = 0x00;
+ private static final int LOWER = 0x01;
+ private static final int LEFT = 0x00;
+ private static final int RIGHT = 0x02;
+ private static final int UPPER_LEFT = UPPER | LEFT;
+ private static final int UPPER_RIGHT = UPPER | RIGHT;
+ private static final int LOWER_LEFT = LOWER | LEFT;
+ private static final int LOWER_RIGHT = LOWER | RIGHT;
+
+ private final GridHost mHost;
+ private final ItemKeyProvider mKeyProvider;
+ private final SelectionPredicate mSelectionPredicate;
+
+ private final List mOnSelectionChangedListeners = new ArrayList<>();
+
+ // Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed
+ // by their y-offset. For example, if the first column of the view starts at an x-value of 5,
+ // mColumns.get(5) would return an array of positions in that column. Within that array, the
+ // value for key y is the adapter position for the item whose y-offset is y.
+ private final SparseArray mColumns = new SparseArray<>();
+
+ // List of limits along the x-axis (columns).
+ // This list is sorted from furthest left to furthest right.
+ private final List mColumnBounds = new ArrayList<>();
+
+ // List of limits along the y-axis (rows). Note that this list only contains items which
+ // have been in the viewport.
+ private final List mRowBounds = new ArrayList<>();
+
+ // The adapter positions which have been recorded so far.
+ private final SparseBooleanArray mKnownPositions = new SparseBooleanArray();
+
+ // Array passed to registered OnSelectionChangedListeners. One array is created and reused
+ // throughout the lifetime of the object.
+ private final Set mSelection = new HashSet<>();
+
+ // The current pointer (in absolute positioning from the top of the view).
+ private Point mPointer;
+
+ // The bounds of the band selection.
+ private RelativePoint mRelOrigin;
+ private RelativePoint mRelPointer;
+
+ private boolean mIsActive;
+
+ // Tracks where the band select originated from. This is used to determine where selections
+ // should expand from when Shift+click is used.
+ private int mPositionNearestOrigin = NOT_SET;
+
+ private final OnScrollListener mScrollListener;
+
+ GridModel(
+ GridHost host,
+ ItemKeyProvider keyProvider,
+ SelectionPredicate selectionPredicate) {
+
+ checkArgument(host != null);
+ checkArgument(keyProvider != null);
+ checkArgument(selectionPredicate != null);
+
+ mHost = host;
+ mKeyProvider = keyProvider;
+ mSelectionPredicate = selectionPredicate;
+
+ mScrollListener = new OnScrollListener() {
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ GridModel.this.onScrolled(recyclerView, dx, dy);
+ }
+ };
+
+ mHost.addOnScrollListener(mScrollListener);
+ }
+
+ /**
+ * Start a band select operation at the given point.
+ *
+ * @param relativeOrigin The origin of the band select operation, relative to the viewport.
+ * For example, if the view is scrolled to the bottom, the top-left of
+ * the
+ * viewport
+ * would have a relative origin of (0, 0), even though its absolute point
+ * has a higher
+ * y-value.
+ */
+ void startCapturing(Point relativeOrigin) {
+ recordVisibleChildren();
+ if (isEmpty()) {
+ // The selection band logic works only if there is at least one visible child.
+ return;
+ }
+
+ mIsActive = true;
+ mPointer = mHost.createAbsolutePoint(relativeOrigin);
+ mRelOrigin = createRelativePoint(mPointer);
+ mRelPointer = createRelativePoint(mPointer);
+ computeCurrentSelection();
+ notifySelectionChanged();
+ }
+
+ /**
+ * Ends the band selection.
+ */
+ void stopCapturing() {
+ mIsActive = false;
+ }
+
+ /**
+ * Resizes the selection by adjusting the pointer (i.e., the corner of the selection
+ * opposite the origin.
+ *
+ * @param relativePointer The pointer (opposite of the origin) of the band select operation,
+ * relative to the viewport. For example, if the view is scrolled to the
+ * bottom, the
+ * top-left of the viewport would have a relative origin of (0, 0), even
+ * though its
+ * absolute point has a higher y-value.
+ */
+ void resizeSelection(Point relativePointer) {
+ mPointer = mHost.createAbsolutePoint(relativePointer);
+ updateModel();
+ }
+
+ /**
+ * @return The adapter position for the item nearest the origin corresponding to the latest
+ * band select operation, or NOT_SET if the selection did not cover any items.
+ */
+ int getPositionNearestOrigin() {
+ return mPositionNearestOrigin;
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ if (!mIsActive) {
+ return;
+ }
+
+ mPointer.x += dx;
+ mPointer.y += dy;
+ recordVisibleChildren();
+ updateModel();
+ }
+
+ /**
+ * Queries the view for all children and records their location metadata.
+ */
+ private void recordVisibleChildren() {
+ for (int i = 0; i < mHost.getVisibleChildCount(); i++) {
+ int adapterPosition = mHost.getAdapterPositionAt(i);
+ // Sometimes the view is not attached, as we notify the multi selection manager
+ // synchronously, while views are attached asynchronously. As a result items which
+ // are in the adapter may not actually have a corresponding view (yet).
+ if (mHost.hasView(adapterPosition)
+ && mSelectionPredicate.canSetStateAtPosition(adapterPosition, true)
+ && !mKnownPositions.get(adapterPosition)) {
+ mKnownPositions.put(adapterPosition, true);
+ recordItemData(mHost.getAbsoluteRectForChildViewAt(i), adapterPosition);
+ }
+ }
+ }
+
+ /**
+ * Checks if there are any recorded children.
+ */
+ private boolean isEmpty() {
+ return mColumnBounds.size() == 0 || mRowBounds.size() == 0;
+ }
+
+ /**
+ * Updates the limits lists and column map with the given item metadata.
+ *
+ * @param absoluteChildRect The absolute rectangle for the child view being processed.
+ * @param adapterPosition The position of the child view being processed.
+ */
+ private void recordItemData(Rect absoluteChildRect, int adapterPosition) {
+ if (mColumnBounds.size() != mHost.getColumnCount()) {
+ // If not all x-limits have been recorded, record this one.
+ recordLimits(
+ mColumnBounds, new Limits(absoluteChildRect.left, absoluteChildRect.right));
+ }
+
+ recordLimits(mRowBounds, new Limits(absoluteChildRect.top, absoluteChildRect.bottom));
+
+ SparseIntArray columnList = mColumns.get(absoluteChildRect.left);
+ if (columnList == null) {
+ columnList = new SparseIntArray();
+ mColumns.put(absoluteChildRect.left, columnList);
+ }
+ columnList.put(absoluteChildRect.top, adapterPosition);
+ }
+
+ /**
+ * Ensures limits exists within the sorted list limitsList, and adds it to the list if it
+ * does not exist.
+ */
+ private void recordLimits(List limitsList, Limits limits) {
+ int index = Collections.binarySearch(limitsList, limits);
+ if (index < 0) {
+ limitsList.add(~index, limits);
+ }
+ }
+
+ /**
+ * Handles a moved pointer; this function determines whether the pointer movement resulted
+ * in a selection change and, if it has, notifies listeners of this change.
+ */
+ private void updateModel() {
+ RelativePoint old = mRelPointer;
+ mRelPointer = createRelativePoint(mPointer);
+ if (old != null && mRelPointer.equals(old)) {
+ return;
+ }
+
+ computeCurrentSelection();
+ notifySelectionChanged();
+ }
+
+ /**
+ * Computes the currently-selected items.
+ */
+ private void computeCurrentSelection() {
+ if (areItemsCoveredByBand(mRelPointer, mRelOrigin)) {
+ updateSelection(computeBounds());
+ } else {
+ mSelection.clear();
+ mPositionNearestOrigin = NOT_SET;
+ }
+ }
+
+ /**
+ * Notifies all listeners of a selection change. Note that this function simply passes
+ * mSelection, so computeCurrentSelection() should be called before this
+ * function.
+ */
+ private void notifySelectionChanged() {
+ for (SelectionObserver listener : mOnSelectionChangedListeners) {
+ listener.onSelectionChanged(mSelection);
+ }
+ }
+
+ /**
+ * @param rect Rectangle including all covered items.
+ */
+ private void updateSelection(Rect rect) {
+ int columnStart =
+ Collections.binarySearch(mColumnBounds, new Limits(rect.left, rect.left));
+
+ checkArgument(columnStart >= 0, "Rect doesn't intesect any known column.");
+
+ int columnEnd = columnStart;
+
+ for (int i = columnStart; i < mColumnBounds.size()
+ && mColumnBounds.get(i).lowerLimit <= rect.right; i++) {
+ columnEnd = i;
+ }
+
+ int rowStart = Collections.binarySearch(mRowBounds, new Limits(rect.top, rect.top));
+ if (rowStart < 0) {
+ mPositionNearestOrigin = NOT_SET;
+ return;
+ }
+
+ int rowEnd = rowStart;
+ for (int i = rowStart; i < mRowBounds.size()
+ && mRowBounds.get(i).lowerLimit <= rect.bottom; i++) {
+ rowEnd = i;
+ }
+
+ updateSelection(columnStart, columnEnd, rowStart, rowEnd);
+ }
+
+ /**
+ * Computes the selection given the previously-computed start- and end-indices for each
+ * row and column.
+ */
+ private void updateSelection(
+ int columnStartIndex, int columnEndIndex, int rowStartIndex, int rowEndIndex) {
+
+ if (BandSelectionHelper.DEBUG) {
+ Log.d(BandSelectionHelper.TAG, String.format(
+ "updateSelection: %d, %d, %d, %d",
+ columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex));
+ }
+
+ mSelection.clear();
+ for (int column = columnStartIndex; column <= columnEndIndex; column++) {
+ SparseIntArray items = mColumns.get(mColumnBounds.get(column).lowerLimit);
+ for (int row = rowStartIndex; row <= rowEndIndex; row++) {
+ // The default return value for SparseIntArray.get is 0, which is a valid
+ // position. Use a sentry value to prevent erroneously selecting item 0.
+ final int rowKey = mRowBounds.get(row).lowerLimit;
+ int position = items.get(rowKey, NOT_SET);
+ if (position != NOT_SET) {
+ K key = mKeyProvider.getKey(position);
+ if (key != null) {
+ // The adapter inserts items for UI layout purposes that aren't
+ // associated with files. Those will have a null model ID.
+ // Don't select them.
+ if (canSelect(key)) {
+ mSelection.add(key);
+ }
+ }
+ if (isPossiblePositionNearestOrigin(column, columnStartIndex, columnEndIndex,
+ row, rowStartIndex, rowEndIndex)) {
+ // If this is the position nearest the origin, record it now so that it
+ // can be returned by endSelection() later.
+ mPositionNearestOrigin = position;
+ }
+ }
+ }
+ }
+ }
+
+ private boolean canSelect(K key) {
+ return mSelectionPredicate.canSetStateForKey(key, true);
+ }
+
+ /**
+ * @return Returns true if the position is the nearest to the origin, or, in the case of the
+ * lower-right corner, whether it is possible that the position is the nearest to the
+ * origin. See comment below for reasoning for this special case.
+ */
+ private boolean isPossiblePositionNearestOrigin(int columnIndex, int columnStartIndex,
+ int columnEndIndex, int rowIndex, int rowStartIndex, int rowEndIndex) {
+ int corner = computeCornerNearestOrigin();
+ switch (corner) {
+ case UPPER_LEFT:
+ return columnIndex == columnStartIndex && rowIndex == rowStartIndex;
+ case UPPER_RIGHT:
+ return columnIndex == columnEndIndex && rowIndex == rowStartIndex;
+ case LOWER_LEFT:
+ return columnIndex == columnStartIndex && rowIndex == rowEndIndex;
+ case LOWER_RIGHT:
+ // Note that in some cases, the last row will not have as many items as there
+ // are columns (e.g., if there are 4 items and 3 columns, the second row will
+ // only have one item in the first column). This function is invoked for each
+ // position from left to right, so return true for any position in the bottom
+ // row and only the right-most position in the bottom row will be recorded.
+ return rowIndex == rowEndIndex;
+ default:
+ throw new RuntimeException("Invalid corner type.");
+ }
+ }
+
+ /**
+ * Listener for changes in which items have been band selected.
+ */
+ public abstract static class SelectionObserver {
+ abstract void onSelectionChanged(Set updatedSelection);
+ }
+
+ void addOnSelectionChangedListener(SelectionObserver listener) {
+ mOnSelectionChangedListeners.add(listener);
+ }
+
+ /**
+ * Called when {@link BandSelectionHelper} is finished with a GridModel.
+ */
+ void onDestroy() {
+ mOnSelectionChangedListeners.clear();
+ // Cleanup listeners to prevent memory leaks.
+ mHost.removeOnScrollListener(mScrollListener);
+ }
+
+ /**
+ * Limits of a view item. For example, if an item's left side is at x-value 5 and its right side
+ * is at x-value 10, the limits would be from 5 to 10. Used to record the left- and right sides
+ * of item columns and the top- and bottom sides of item rows so that it can be determined
+ * whether the pointer is located within the bounds of an item.
+ */
+ private static class Limits implements Comparable {
+ public int lowerLimit;
+ public int upperLimit;
+
+ Limits(int lowerLimit, int upperLimit) {
+ this.lowerLimit = lowerLimit;
+ this.upperLimit = upperLimit;
+ }
+
+ @Override
+ public int compareTo(Limits other) {
+ return lowerLimit - other.lowerLimit;
+ }
+
+ @Override
+ public int hashCode() {
+ return lowerLimit ^ upperLimit;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Limits)) {
+ return false;
+ }
+
+ return ((Limits) other).lowerLimit == lowerLimit
+ && ((Limits) other).upperLimit == upperLimit;
+ }
+
+ @Override
+ public String toString() {
+ return "(" + lowerLimit + ", " + upperLimit + ")";
+ }
+ }
+
+ /**
+ * The location of a coordinate relative to items. This class represents a general area of the
+ * view as it relates to band selection rather than an explicit point. For example, two
+ * different points within an item are considered to have the same "location" because band
+ * selection originating within the item would select the same items no matter which point
+ * was used. Same goes for points between items as well as those at the very beginning or end
+ * of the view.
+ *
+ * Tracking a coordinate (e.g., an x-value) as a CoordinateLocation instead of as an int has the
+ * advantage of tying the value to the Limits of items along that axis. This allows easy
+ * selection of items within those Limits as opposed to a search through every item to see if a
+ * given coordinate value falls within those Limits.
+ */
+ private static class RelativeCoordinate
+ implements Comparable {
+ /**
+ * Location describing points after the last known item.
+ */
+ static final int AFTER_LAST_ITEM = 0;
+
+ /**
+ * Location describing points before the first known item.
+ */
+ static final int BEFORE_FIRST_ITEM = 1;
+
+ /**
+ * Location describing points between two items.
+ */
+ static final int BETWEEN_TWO_ITEMS = 2;
+
+ /**
+ * Location describing points within the limits of one item.
+ */
+ static final int WITHIN_LIMITS = 3;
+
+ /**
+ * The type of this coordinate, which is one of AFTER_LAST_ITEM, BEFORE_FIRST_ITEM,
+ * BETWEEN_TWO_ITEMS, or WITHIN_LIMITS.
+ */
+ public final int type;
+
+ /**
+ * The limits before the coordinate; only populated when type == WITHIN_LIMITS or type ==
+ * BETWEEN_TWO_ITEMS.
+ */
+ public Limits limitsBeforeCoordinate;
+
+ /**
+ * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS.
+ */
+ public Limits limitsAfterCoordinate;
+
+ // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM.
+ public Limits mFirstKnownItem;
+ // Limits of the last known item; only populated when type == AFTER_LAST_ITEM.
+ public Limits mLastKnownItem;
+
+ /**
+ * @param limitsList The sorted limits list for the coordinate type. If this
+ * CoordinateLocation is an x-value, mXLimitsList should be passed;
+ * otherwise,
+ * mYLimitsList should be pased.
+ * @param value The coordinate value.
+ */
+ RelativeCoordinate(List limitsList, int value) {
+ int index = Collections.binarySearch(limitsList, new Limits(value, value));
+
+ if (index >= 0) {
+ this.type = WITHIN_LIMITS;
+ this.limitsBeforeCoordinate = limitsList.get(index);
+ } else if (~index == 0) {
+ this.type = BEFORE_FIRST_ITEM;
+ this.mFirstKnownItem = limitsList.get(0);
+ } else if (~index == limitsList.size()) {
+ Limits lastLimits = limitsList.get(limitsList.size() - 1);
+ if (lastLimits.lowerLimit <= value && value <= lastLimits.upperLimit) {
+ this.type = WITHIN_LIMITS;
+ this.limitsBeforeCoordinate = lastLimits;
+ } else {
+ this.type = AFTER_LAST_ITEM;
+ this.mLastKnownItem = lastLimits;
+ }
+ } else {
+ Limits limitsBeforeIndex = limitsList.get(~index - 1);
+ if (limitsBeforeIndex.lowerLimit <= value
+ && value <= limitsBeforeIndex.upperLimit) {
+ this.type = WITHIN_LIMITS;
+ this.limitsBeforeCoordinate = limitsList.get(~index - 1);
+ } else {
+ this.type = BETWEEN_TWO_ITEMS;
+ this.limitsBeforeCoordinate = limitsList.get(~index - 1);
+ this.limitsAfterCoordinate = limitsList.get(~index);
+ }
+ }
+ }
+
+ int toComparisonValue() {
+ if (type == BEFORE_FIRST_ITEM) {
+ return mFirstKnownItem.lowerLimit - 1;
+ } else if (type == AFTER_LAST_ITEM) {
+ return mLastKnownItem.upperLimit + 1;
+ } else if (type == BETWEEN_TWO_ITEMS) {
+ return limitsBeforeCoordinate.upperLimit + 1;
+ } else {
+ return limitsBeforeCoordinate.lowerLimit;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return mFirstKnownItem.lowerLimit
+ ^ mLastKnownItem.upperLimit
+ ^ limitsBeforeCoordinate.upperLimit
+ ^ limitsBeforeCoordinate.lowerLimit;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof RelativeCoordinate)) {
+ return false;
+ }
+
+ RelativeCoordinate otherCoordinate = (RelativeCoordinate) other;
+ return toComparisonValue() == otherCoordinate.toComparisonValue();
+ }
+
+ @Override
+ public int compareTo(RelativeCoordinate other) {
+ return toComparisonValue() - other.toComparisonValue();
+ }
+ }
+
+ RelativePoint createRelativePoint(Point point) {
+ return new RelativePoint(
+ new RelativeCoordinate(mColumnBounds, point.x),
+ new RelativeCoordinate(mRowBounds, point.y));
+ }
+
+ /**
+ * The location of a point relative to the Limits of nearby items; consists of both an x- and
+ * y-RelativeCoordinateLocation.
+ */
+ private static class RelativePoint {
+
+ final RelativeCoordinate mX;
+ final RelativeCoordinate mY;
+
+ RelativePoint(
+ @NonNull List columnLimits,
+ @NonNull List rowLimits, Point point) {
+
+ this.mX = new RelativeCoordinate(columnLimits, point.x);
+ this.mY = new RelativeCoordinate(rowLimits, point.y);
+ }
+
+ RelativePoint(@NonNull RelativeCoordinate x, @NonNull RelativeCoordinate y) {
+ this.mX = x;
+ this.mY = y;
+ }
+
+ @Override
+ public int hashCode() {
+ return mX.toComparisonValue() ^ mY.toComparisonValue();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (!(other instanceof RelativePoint)) {
+ return false;
+ }
+
+ RelativePoint otherPoint = (RelativePoint) other;
+ return mX.equals(otherPoint.mX) && mY.equals(otherPoint.mY);
+ }
+ }
+
+ /**
+ * Generates a rectangle which contains the items selected by the pointer and origin.
+ *
+ * @return The rectangle, or null if no items were selected.
+ */
+ private Rect computeBounds() {
+ Rect rect = new Rect();
+ rect.left = getCoordinateValue(
+ min(mRelOrigin.mX, mRelPointer.mX),
+ mColumnBounds,
+ true);
+ rect.right = getCoordinateValue(
+ max(mRelOrigin.mX, mRelPointer.mX),
+ mColumnBounds,
+ false);
+ rect.top = getCoordinateValue(
+ min(mRelOrigin.mY, mRelPointer.mY),
+ mRowBounds,
+ true);
+ rect.bottom = getCoordinateValue(
+ max(mRelOrigin.mY, mRelPointer.mY),
+ mRowBounds,
+ false);
+ return rect;
+ }
+
+ /**
+ * Computes the corner of the selection nearest the origin.
+ */
+ private int computeCornerNearestOrigin() {
+ int cornerValue = 0;
+
+ if (mRelOrigin.mY.equals(min(mRelOrigin.mY, mRelPointer.mY))) {
+ cornerValue |= UPPER;
+ } else {
+ cornerValue |= LOWER;
+ }
+
+ if (mRelOrigin.mX.equals(min(mRelOrigin.mX, mRelPointer.mX))) {
+ cornerValue |= LEFT;
+ } else {
+ cornerValue |= RIGHT;
+ }
+
+ return cornerValue;
+ }
+
+ private RelativeCoordinate min(
+ @NonNull RelativeCoordinate first, @NonNull RelativeCoordinate second) {
+ return first.compareTo(second) < 0 ? first : second;
+ }
+
+ private RelativeCoordinate max(
+ @NonNull RelativeCoordinate first, @NonNull RelativeCoordinate second) {
+ return first.compareTo(second) > 0 ? first : second;
+ }
+
+ /**
+ * @return The absolute coordinate (i.e., the x- or y-value) of the given relative
+ * coordinate.
+ */
+ private int getCoordinateValue(
+ @NonNull RelativeCoordinate coordinate,
+ @NonNull List limitsList,
+ boolean isStartOfRange) {
+
+ switch (coordinate.type) {
+ case RelativeCoordinate.BEFORE_FIRST_ITEM:
+ return limitsList.get(0).lowerLimit;
+ case RelativeCoordinate.AFTER_LAST_ITEM:
+ return limitsList.get(limitsList.size() - 1).upperLimit;
+ case RelativeCoordinate.BETWEEN_TWO_ITEMS:
+ if (isStartOfRange) {
+ return coordinate.limitsAfterCoordinate.lowerLimit;
+ } else {
+ return coordinate.limitsBeforeCoordinate.upperLimit;
+ }
+ case RelativeCoordinate.WITHIN_LIMITS:
+ return coordinate.limitsBeforeCoordinate.lowerLimit;
+ }
+
+ throw new RuntimeException("Invalid coordinate value.");
+ }
+
+ private boolean areItemsCoveredByBand(
+ @NonNull RelativePoint first, @NonNull RelativePoint second) {
+
+ return doesCoordinateLocationCoverItems(first.mX, second.mX)
+ && doesCoordinateLocationCoverItems(first.mY, second.mY);
+ }
+
+ private boolean doesCoordinateLocationCoverItems(
+ @NonNull RelativeCoordinate pointerCoordinate,
+ @NonNull RelativeCoordinate originCoordinate) {
+
+ if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM
+ && originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) {
+ return false;
+ }
+
+ if (pointerCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM
+ && originCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM) {
+ return false;
+ }
+
+ if (pointerCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS
+ && originCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS
+ && pointerCoordinate.limitsBeforeCoordinate.equals(
+ originCoordinate.limitsBeforeCoordinate)
+ && pointerCoordinate.limitsAfterCoordinate.equals(
+ originCoordinate.limitsAfterCoordinate)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Provides functionality for BandController. Exists primarily to tests that are
+ * fully isolated from RecyclerView.
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+ abstract static class GridHost extends BandSelectionHelper.BandHost {
+
+ /**
+ * Remove the listener.
+ *
+ * @param listener
+ */
+ abstract void removeOnScrollListener(@NonNull OnScrollListener listener);
+
+ /**
+ * @param relativePoint for which to create absolute point.
+ * @return absolute point.
+ */
+ abstract Point createAbsolutePoint(@NonNull Point relativePoint);
+
+ /**
+ * @param index index of child.
+ * @return rectangle describing child at {@code index}.
+ */
+ abstract Rect getAbsoluteRectForChildViewAt(int index);
+
+ /**
+ * @param index index of child.
+ * @return child adapter position for the child at {@code index}
+ */
+ abstract int getAdapterPositionAt(int index);
+
+ /** @return column count. */
+ abstract int getColumnCount();
+
+ /** @return number of children visible in the view. */
+ abstract int getVisibleChildCount();
+
+ /**
+ * @return true if the item at adapter position is attached to a view.
+ */
+ abstract boolean hasView(int adapterPosition);
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/ItemDetailsLookup.java b/app/src/main/java/androidx/recyclerview/selection/ItemDetailsLookup.java
new file mode 100644
index 0000000000..9cb6d34551
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/ItemDetailsLookup.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * The Selection library calls {@link #getItemDetails(MotionEvent)} when it needs
+ * access to information about the area and/or {@link ItemDetails} under a {@link MotionEvent}.
+ * Your implementation must negotiate
+ * {@link RecyclerView.ViewHolder ViewHolder} lookup with the
+ * corresponding RecyclerView instance, and the subsequent conversion of the ViewHolder
+ * instance to an {@link ItemDetails} instance.
+ *
+ *
+ * Example
+ *
+ * final class MyDetailsLookup extends ItemDetailsLookup {
+ *
+ * private final RecyclerView mRecyclerView;
+ *
+ * MyDetailsLookup(RecyclerView recyclerView) {
+ * mRecyclerView = recyclerView;
+ * }
+ *
+ * public ItemDetails getItemDetails(MotionEvent e) {
+ * View view = mRecView.findChildViewUnder(e.getX(), e.getY());
+ * if (view != null) {
+ * ViewHolder holder = mRecView.getChildViewHolder(view);
+ * if (holder instanceof MyHolder) {
+ * return ((MyHolder) holder).getItemDetails();
+ * }
+ * }
+ * return null;
+ * }
+ *}
+ *
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+public abstract class ItemDetailsLookup {
+
+ /**
+ * @return true if there is an item at the event coordinates.
+ */
+ final boolean overItem(@NonNull MotionEvent e) {
+ return getItemPosition(e) != RecyclerView.NO_POSITION;
+ }
+
+ /**
+ * @return true if there is an item w/ a stable ID at the event coordinates.
+ */
+ final boolean overItemWithSelectionKey(@NonNull MotionEvent e) {
+ return overItem(e) && hasSelectionKey(getItemDetails(e));
+ }
+
+ /**
+ * @return true if the event coordinates are in an area of the item
+ * that can result in dragging the item. List items frequently have a white
+ * area that is not draggable allowing band selection to be initiated
+ * in that area.
+ */
+ final boolean inItemDragRegion(@NonNull MotionEvent e) {
+ return overItem(e) && getItemDetails(e).inDragRegion(e);
+ }
+
+ /**
+ * @return true if the event coordinates are in a "selection hot spot"
+ * region of an item. Contact in these regions result in immediate
+ * selection, even when there is no existing selection.
+ */
+ final boolean inItemSelectRegion(@NonNull MotionEvent e) {
+ return overItem(e) && getItemDetails(e).inSelectionHotspot(e);
+ }
+
+ /**
+ * @return the adapter position of the item at the event coordinates.
+ */
+ final int getItemPosition(@NonNull MotionEvent e) {
+ @Nullable ItemDetails> item = getItemDetails(e);
+ return item != null
+ ? item.getPosition()
+ : RecyclerView.NO_POSITION;
+ }
+
+ private static boolean hasSelectionKey(@Nullable ItemDetails> item) {
+ return item != null && item.getSelectionKey() != null;
+ }
+
+ private static boolean hasPosition(@Nullable ItemDetails> item) {
+ return item != null && item.getPosition() != RecyclerView.NO_POSITION;
+ }
+
+ /**
+ * @return the ItemDetails for the item under the event, or null.
+ */
+ public abstract @Nullable ItemDetails getItemDetails(@NonNull MotionEvent e);
+
+ /**
+ * An ItemDetails implementation provides the selection library with access to information
+ * about a specific RecyclerView item. This class is a key component in controling
+ * the behaviors of the selection library in the context of a specific activity.
+ *
+ *
+ * Selection Hotspot
+ *
+ *
+ * This is an optional feature identifying an area within a view that
+ * is single-tap to select. Ordinarily a single tap on an item when there is no
+ * existing selection will result in that item being activated. If the tap
+ * occurs within the "selection hotspot" the item will instead be selected.
+ *
+ *
+ * See {@link OnItemActivatedListener} for details on handling item activation.
+ *
+ *
+ * Drag Region
+ *
+ *
+ * The selection library provides support for mouse driven band selection. The "lasso"
+ * typically associated with mouse selection can be started only in an empty
+ * area of the RecyclerView (an area where the item position == RecyclerView#NO_POSITION,
+ * or where RecyclerView#findChildViewUnder returns null). But in many instances
+ * the item views presented by RecyclerView will contain areas that may be perceived
+ * by the user as being empty. The user may expect to be able to initiate band
+ * selection in these empty areas.
+ *
+ *
+ * The "drag region" concept exists in large part to accommodate this user expectation.
+ * Drag region is the content in an item view that the user doesn't otherwise
+ * perceive to be empty or part of the background of recycler view.
+ *
+ * Take for example a traditional single column layout where
+ * the view layout width is "match_parent":
+ *
+ * -------------------------------------------------------
+ * | [icon] A string label. ...empty space... |
+ * -------------------------------------------------------
+ * < --- drag region --> < --treated as background-->
+ *
+ *
+ *
+ * Further more, within a drag region, a mouse click and drag will immediately
+ * initiate drag and drop (if supported by your configuration).
+ *
+ *
+ * As user expectations around touch and mouse input differ substantially,
+ * "drag region" has no effect on handling of touch input.
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+ public abstract static class ItemDetails {
+
+ /**
+ * Returns the adapter position of the item. See
+ * {@link RecyclerView.ViewHolder#getAdapterPosition() ViewHolder.getAdapterPosition}
+ *
+ * @return the position of an item.
+ */
+ public abstract int getPosition();
+
+ /**
+ * @return true if the item has a selection key.
+ */
+ public boolean hasSelectionKey() {
+ return getSelectionKey() != null;
+ }
+
+ /**
+ * @return the selection key of an item.
+ */
+ public abstract @Nullable K getSelectionKey();
+
+ /**
+ * Areas are often included in a view that behave similar to checkboxes, such
+ * as the icon to the left of an email message. "selection
+ * hotspot" provides a mechanism to identify such regions, and for the
+ * library to directly translate taps in these regions into a change
+ * in selection state.
+ *
+ * @return true if the event is in an area of the item that should be
+ * directly interpreted as a user wishing to select the item. This
+ * is useful for checkboxes and other UI affordances focused on enabling
+ * selection.
+ */
+ public boolean inSelectionHotspot(@NonNull MotionEvent e) {
+ return false;
+ }
+
+ /**
+ * "Item Drag Region" identifies areas of an item that are not considered when the library
+ * evaluates whether or not to initiate band-selection for mouse input. The drag region
+ * will usually correspond to an area of an item that represents user visible content.
+ * Mouse driven band selection operations are only ever initiated in non-drag-regions.
+ * This is a consideration as many layouts may not include empty space between
+ * RecyclerView items where band selection can be initiated.
+ *
+ *
+ * For example. You may present a single column list of contact names in a
+ * RecyclerView instance in which the individual view items expand to fill all
+ * available space.
+ * But within the expanded view item after the contact name there may be empty space that a
+ * user would reasonably expect to initiate band selection. When a MotionEvent occurs
+ * in such an area, you should return identify this as NOT in a drag region.
+ *
+ *
+ * Further more, within a drag region, a mouse click and drag will immediately
+ * initiate drag and drop (if supported by your configuration).
+ *
+ * @return true if the item is in an area of the item that can result in dragging
+ * the item. List items frequently have a white area that is not draggable allowing
+ * mouse driven band selection to be initiated in that area.
+ */
+ public boolean inDragRegion(@NonNull MotionEvent e) {
+ return false;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ return (obj instanceof ItemDetails)
+ && isEqualTo((ItemDetails) obj);
+ }
+
+ private boolean isEqualTo(@NonNull ItemDetails other) {
+ K key = getSelectionKey();
+ boolean sameKeys = false;
+ if (key == null) {
+ sameKeys = other.getSelectionKey() == null;
+ } else {
+ sameKeys = key.equals(other.getSelectionKey());
+ }
+ return sameKeys && this.getPosition() == other.getPosition();
+ }
+
+ @Override
+ public int hashCode() {
+ return getPosition() >>> 8;
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/ItemKeyProvider.java b/app/src/main/java/androidx/recyclerview/selection/ItemKeyProvider.java
new file mode 100644
index 0000000000..a216b2466c
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/ItemKeyProvider.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Provides selection library access to stable selection keys identifying items
+ * presented by a {@link RecyclerView RecyclerView} instance.
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+public abstract class ItemKeyProvider {
+
+ /**
+ * Provides access to all data, regardless of whether it is bound to a view or not.
+ * Key providers with this access type enjoy support for enhanced features like:
+ * SHIFT+click range selection, and band selection.
+ */
+ public static final int SCOPE_MAPPED = 0;
+
+ /**
+ * Provides access to cached data based for items that were recently bound in the view.
+ * Employing this provider will result in a reduced feature-set, as some
+ * features like SHIFT+click range selection and band selection are dependent
+ * on mapped access.
+ */
+ public static final int SCOPE_CACHED = 1;
+
+ @IntDef({
+ SCOPE_MAPPED,
+ SCOPE_CACHED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Scope {}
+
+ private final @Scope int mScope;
+
+ /**
+ * Creates a new provider with the given scope.
+ *
+ * @param scope Scope can't be changed at runtime.
+ */
+ protected ItemKeyProvider(@Scope int scope) {
+ checkArgument(scope == SCOPE_MAPPED || scope == SCOPE_CACHED);
+
+ mScope = scope;
+ }
+
+ final boolean hasAccess(@Scope int scope) {
+ return scope == mScope;
+ }
+
+ /**
+ * @return The selection key at the given adapter position, or null.
+ */
+ public abstract @Nullable K getKey(int position);
+
+ /**
+ * @return the position corresponding to the selection key, or RecyclerView.NO_POSITION.
+ */
+ public abstract int getPosition(@NonNull K key);
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/MotionEvents.java b/app/src/main/java/androidx/recyclerview/selection/MotionEvents.java
new file mode 100644
index 0000000000..bc47a76ccf
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/MotionEvents.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import android.graphics.Point;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Utility methods for working with {@link MotionEvent} instances.
+ */
+final class MotionEvents {
+
+ private MotionEvents() {}
+
+ static boolean isMouseEvent(@NonNull MotionEvent e) {
+ return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
+ }
+
+ static boolean isTouchEvent(@NonNull MotionEvent e) {
+ return e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER;
+ }
+
+ static boolean isActionMove(@NonNull MotionEvent e) {
+ return e.getActionMasked() == MotionEvent.ACTION_MOVE;
+ }
+
+ static boolean isActionDown(@NonNull MotionEvent e) {
+ return e.getActionMasked() == MotionEvent.ACTION_DOWN;
+ }
+
+ static boolean isActionUp(@NonNull MotionEvent e) {
+ return e.getActionMasked() == MotionEvent.ACTION_UP;
+ }
+
+ static boolean isActionPointerUp(@NonNull MotionEvent e) {
+ return e.getActionMasked() == MotionEvent.ACTION_POINTER_UP;
+ }
+
+ @SuppressWarnings("unused")
+ static boolean isActionPointerDown(@NonNull MotionEvent e) {
+ return e.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN;
+ }
+
+ static boolean isActionCancel(@NonNull MotionEvent e) {
+ return e.getActionMasked() == MotionEvent.ACTION_CANCEL;
+ }
+
+ static Point getOrigin(@NonNull MotionEvent e) {
+ return new Point((int) e.getX(), (int) e.getY());
+ }
+
+ static boolean isPrimaryMouseButtonPressed(@NonNull MotionEvent e) {
+ return isButtonPressed(e, MotionEvent.BUTTON_PRIMARY);
+ }
+
+ static boolean isSecondaryMouseButtonPressed(@NonNull MotionEvent e) {
+ return isButtonPressed(e, MotionEvent.BUTTON_SECONDARY);
+ }
+
+ static boolean isTertiaryMouseButtonPressed(@NonNull MotionEvent e) {
+ return isButtonPressed(e, MotionEvent.BUTTON_TERTIARY);
+ }
+
+ // NOTE: Can replace this with MotionEvent.isButtonPressed once targeting 21 or higher.
+ private static boolean isButtonPressed(MotionEvent e, int button) {
+ if (button == 0) {
+ return false;
+ }
+ return (e.getButtonState() & button) == button;
+ }
+
+ static boolean isShiftKeyPressed(@NonNull MotionEvent e) {
+ return hasBit(e.getMetaState(), KeyEvent.META_SHIFT_ON);
+ }
+
+ static boolean isCtrlKeyPressed(@NonNull MotionEvent e) {
+ return hasBit(e.getMetaState(), KeyEvent.META_CTRL_ON);
+ }
+
+ static boolean isAltKeyPressed(@NonNull MotionEvent e) {
+ return hasBit(e.getMetaState(), KeyEvent.META_ALT_ON);
+ }
+
+ static boolean isTouchpadScroll(@NonNull MotionEvent e) {
+ // Touchpad inputs are treated as mouse inputs, and when scrolling, there are no buttons
+ // returned.
+ return isMouseEvent(e) && isActionMove(e) && e.getButtonState() == 0;
+ }
+
+ /**
+ * Returns true if the event is a drag event (which is presumbaly, but not
+ * explicitly required to be a mouse event).
+ * @param e
+ */
+ static boolean isPointerDragEvent(MotionEvent e) {
+ return isPrimaryMouseButtonPressed(e)
+ && isActionMove(e);
+ }
+
+ private static boolean hasBit(int metaState, int bit) {
+ return (metaState & bit) != 0;
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/MotionInputHandler.java b/app/src/main/java/androidx/recyclerview/selection/MotionInputHandler.java
new file mode 100644
index 0000000000..a7eb208358
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/MotionInputHandler.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+import static androidx.core.util.Preconditions.checkState;
+
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Base class for handlers that can be registered w/ {@link GestureRouter}.
+ */
+abstract class MotionInputHandler extends SimpleOnGestureListener {
+
+ protected final SelectionTracker mSelectionTracker;
+
+ private final ItemKeyProvider mKeyProvider;
+ private final FocusDelegate mFocusDelegate;
+
+ MotionInputHandler(
+ @NonNull SelectionTracker selectionTracker,
+ @NonNull ItemKeyProvider keyProvider,
+ @NonNull FocusDelegate focusDelegate) {
+
+ checkArgument(selectionTracker != null);
+ checkArgument(keyProvider != null);
+ checkArgument(focusDelegate != null);
+
+ mSelectionTracker = selectionTracker;
+ mKeyProvider = keyProvider;
+ mFocusDelegate = focusDelegate;
+ }
+
+ final boolean selectItem(@NonNull ItemDetails details) {
+ checkArgument(details != null);
+ checkArgument(hasPosition(details));
+ checkArgument(hasSelectionKey(details));
+
+ if (mSelectionTracker.select(details.getSelectionKey())) {
+ mSelectionTracker.anchorRange(details.getPosition());
+ }
+
+ // we set the focus on this doc so it will be the origin for keyboard events or shift+clicks
+ // if there is only a single item selected, otherwise clear focus
+ if (mSelectionTracker.getSelection().size() == 1) {
+ mFocusDelegate.focusItem(details);
+ } else {
+ mFocusDelegate.clearFocus();
+ }
+ return true;
+ }
+
+ protected final boolean focusItem(@NonNull ItemDetails details) {
+ checkArgument(details != null);
+ checkArgument(hasSelectionKey(details));
+
+ mSelectionTracker.clearSelection();
+ mFocusDelegate.focusItem(details);
+ return true;
+ }
+
+ protected final void extendSelectionRange(@NonNull ItemDetails details) {
+ checkState(mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED));
+ checkArgument(hasPosition(details));
+ checkArgument(hasSelectionKey(details));
+
+ mSelectionTracker.extendRange(details.getPosition());
+ mFocusDelegate.focusItem(details);
+ }
+
+ final boolean isRangeExtension(@NonNull MotionEvent e) {
+ return MotionEvents.isShiftKeyPressed(e)
+ && mSelectionTracker.isRangeActive()
+ // Without full corpus access we can't reliably implement range
+ // as a user can scroll *anywhere* then SHIFT+click.
+ && mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED);
+ }
+
+ boolean shouldClearSelection(@NonNull MotionEvent e, @NonNull ItemDetails item) {
+ return !MotionEvents.isCtrlKeyPressed(e)
+ && !item.inSelectionHotspot(e)
+ && !mSelectionTracker.isSelected(item.getSelectionKey());
+ }
+
+ static boolean hasSelectionKey(@Nullable ItemDetails> item) {
+ return item != null && item.getSelectionKey() != null;
+ }
+
+ static boolean hasPosition(@Nullable ItemDetails> item) {
+ return item != null && item.getPosition() != RecyclerView.NO_POSITION;
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/MouseInputHandler.java b/app/src/main/java/androidx/recyclerview/selection/MouseInputHandler.java
new file mode 100644
index 0000000000..ca45137a00
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/MouseInputHandler.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+import static androidx.core.util.Preconditions.checkState;
+import static androidx.recyclerview.selection.Shared.DEBUG;
+import static androidx.recyclerview.selection.Shared.VERBOSE;
+
+import android.util.Log;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * A MotionInputHandler that provides the high-level glue for mouse driven selection. This
+ * class works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper}
+ * to implement the primary policies around mouse input.
+ */
+final class MouseInputHandler extends MotionInputHandler {
+
+ private static final String TAG = "MouseInputDelegate";
+
+ private final ItemDetailsLookup mDetailsLookup;
+ private final OnContextClickListener mOnContextClickListener;
+ private final OnItemActivatedListener mOnItemActivatedListener;
+ private final FocusDelegate mFocusDelegate;
+
+ // The event has been handled in onSingleTapUp
+ private boolean mHandledTapUp;
+ // true when the previous event has consumed a right click motion event
+ private boolean mHandledOnDown;
+
+ MouseInputHandler(
+ @NonNull SelectionTracker selectionTracker,
+ @NonNull ItemKeyProvider keyProvider,
+ @NonNull ItemDetailsLookup detailsLookup,
+ @NonNull OnContextClickListener onContextClickListener,
+ @NonNull OnItemActivatedListener onItemActivatedListener,
+ @NonNull FocusDelegate focusDelegate) {
+
+ super(selectionTracker, keyProvider, focusDelegate);
+
+ checkArgument(detailsLookup != null);
+ checkArgument(onContextClickListener != null);
+ checkArgument(onItemActivatedListener != null);
+
+ mDetailsLookup = detailsLookup;
+ mOnContextClickListener = onContextClickListener;
+ mOnItemActivatedListener = onItemActivatedListener;
+ mFocusDelegate = focusDelegate;
+ }
+
+ @Override
+ public boolean onDown(@NonNull MotionEvent e) {
+ if (VERBOSE) Log.v(TAG, "Delegated onDown event.");
+ if ((MotionEvents.isAltKeyPressed(e) && MotionEvents.isPrimaryMouseButtonPressed(e))
+ || MotionEvents.isSecondaryMouseButtonPressed(e)) {
+ mHandledOnDown = true;
+ return onRightClick(e);
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2,
+ float distanceX, float distanceY) {
+ // Don't scroll content window in response to mouse drag
+ // If it's two-finger trackpad scrolling, we want to scroll
+ return !MotionEvents.isTouchpadScroll(e2);
+ }
+
+ @Override
+ public boolean onSingleTapUp(@NonNull MotionEvent e) {
+ // See b/27377794. Since we don't get a button state back from UP events, we have to
+ // explicitly save this state to know whether something was previously handled by
+ // DOWN events or not.
+ if (mHandledOnDown) {
+ if (VERBOSE) Log.v(TAG, "Ignoring onSingleTapUp, previously handled in onDown.");
+ mHandledOnDown = false;
+ return false;
+ }
+
+ if (!mDetailsLookup.overItemWithSelectionKey(e)) {
+ if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection.");
+ mSelectionTracker.clearSelection();
+ mFocusDelegate.clearFocus();
+ return false;
+ }
+
+ if (MotionEvents.isTertiaryMouseButtonPressed(e)) {
+ if (DEBUG) Log.d(TAG, "Ignoring middle click");
+ return false;
+ }
+
+ if (mSelectionTracker.hasSelection()) {
+ onItemClick(e, mDetailsLookup.getItemDetails(e));
+ mHandledTapUp = true;
+ return true;
+ }
+
+ return false;
+ }
+
+ // tap on an item when there is an existing selection. We could extend
+ // a selection, we could clear selection (then launch)
+ private void onItemClick(@NonNull MotionEvent e, @NonNull ItemDetails item) {
+ checkState(mSelectionTracker.hasSelection());
+ checkArgument(item != null);
+
+ if (isRangeExtension(e)) {
+ extendSelectionRange(item);
+ } else {
+ if (shouldClearSelection(e, item)) {
+ mSelectionTracker.clearSelection();
+ }
+ if (mSelectionTracker.isSelected(item.getSelectionKey())) {
+ if (mSelectionTracker.deselect(item.getSelectionKey())) {
+ mFocusDelegate.clearFocus();
+ }
+ } else {
+ selectOrFocusItem(item, e);
+ }
+ }
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(@NonNull MotionEvent e) {
+ if (mHandledTapUp) {
+ if (VERBOSE) {
+ Log.v(TAG,
+ "Ignoring onSingleTapConfirmed, previously handled in onSingleTapUp.");
+ }
+ mHandledTapUp = false;
+ return false;
+ }
+
+ if (mSelectionTracker.hasSelection()) {
+ return false; // should have been handled by onSingleTapUp.
+ }
+
+ if (!mDetailsLookup.overItem(e)) {
+ if (DEBUG) Log.d(TAG, "Ignoring Confirmed Tap on non-item.");
+ return false;
+ }
+
+ if (MotionEvents.isTertiaryMouseButtonPressed(e)) {
+ if (DEBUG) Log.d(TAG, "Ignoring middle click");
+ return false;
+ }
+
+ @Nullable ItemDetails item = mDetailsLookup.getItemDetails(e);
+ if (item == null || !item.hasSelectionKey()) {
+ return false;
+ }
+
+ if (mFocusDelegate.hasFocusedItem() && MotionEvents.isShiftKeyPressed(e)) {
+ mSelectionTracker.startRange(mFocusDelegate.getFocusedPosition());
+ mSelectionTracker.extendRange(item.getPosition());
+ } else {
+ selectOrFocusItem(item, e);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onDoubleTap(@NonNull MotionEvent e) {
+ mHandledTapUp = false;
+
+ if (!mDetailsLookup.overItemWithSelectionKey(e)) {
+ if (DEBUG) Log.d(TAG, "Ignoring DoubleTap on non-model-backed item.");
+ return false;
+ }
+
+ if (MotionEvents.isTertiaryMouseButtonPressed(e)) {
+ if (DEBUG) Log.d(TAG, "Ignoring middle click");
+ return false;
+ }
+
+ ItemDetails item = mDetailsLookup.getItemDetails(e);
+ return (item != null) && mOnItemActivatedListener.onItemActivated(item, e);
+ }
+
+ private boolean onRightClick(@NonNull MotionEvent e) {
+ if (mDetailsLookup.overItemWithSelectionKey(e)) {
+ @Nullable ItemDetails item = mDetailsLookup.getItemDetails(e);
+ if (item != null && !mSelectionTracker.isSelected(item.getSelectionKey())) {
+ mSelectionTracker.clearSelection();
+ selectItem(item);
+ }
+ }
+
+ // We always delegate final handling of the event,
+ // since the handler might want to show a context menu
+ // in an empty area or some other weirdo view.
+ return mOnContextClickListener.onContextClick(e);
+ }
+
+ private void selectOrFocusItem(@NonNull ItemDetails item, @NonNull MotionEvent e) {
+ if (item.inSelectionHotspot(e) || MotionEvents.isCtrlKeyPressed(e)) {
+ selectItem(item);
+ } else {
+ focusItem(item);
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/MutableSelection.java b/app/src/main/java/androidx/recyclerview/selection/MutableSelection.java
new file mode 100644
index 0000000000..3a5cebe19f
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/MutableSelection.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Subclass of {@link Selection} exposing public support for mutating the underlying
+ * selection data. This is useful for clients of {@link SelectionTracker} that wish to
+ * manipulate a copy of selection data obtained via
+ * {@link SelectionTracker#copySelection(MutableSelection)}.
+ *
+ *
+ * While the {@link Selection} class is not intrinsically immutable, it is not mutable
+ * by non-library code. Furthermore the value returned from {@link SelectionTracker#getSelection()}
+ * is a live view of the underlying selection, mutable by the library itself.
+ *
+ *
+ * {@link MutableSelection} allows clients to obtain a mutable copy of the Selection
+ * state held by the selection library. This is useful in situations where a stable
+ * snapshot of the selection is required.
+ *
+ *
+ *
Example
+ *
+ *
+ *
+ * MutableSelection snapshot = new MutableSelection();
+ * selectionTracker.copySelection(snapshot);
+ *
+ * // Clear the user visible selection.
+ * selectionTracker.clearSelection();
+ * // tracker.getSelection().isEmpty() will be true.
+ * // shapshot has a copy of the previous selection.
+ *
+ *
+ * @see android.text.Selection
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+public final class MutableSelection extends Selection {
+
+ @Override
+ public boolean add(@NonNull K key) {
+ return super.add(key);
+ }
+
+ @Override
+ public boolean remove(@NonNull K key) {
+ return super.remove(key);
+ }
+
+ @Override
+ public void copyFrom(@NonNull Selection source) {
+ super.copyFrom(source);
+ }
+
+ @Override
+ public void clear() {
+ super.clear();
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/OnContextClickListener.java b/app/src/main/java/androidx/recyclerview/selection/OnContextClickListener.java
new file mode 100644
index 0000000000..b7e432e602
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/OnContextClickListener.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Override methods in this class to provide application specific behaviors
+ * related to mouse input.
+ */
+/**
+ * Register an OnContextClickListener to be notified when a context click
+ * occurs.
+ */
+public interface OnContextClickListener {
+
+ /**
+ * Called when user performs a context click, usually via mouse pointer
+ * right-click.
+ *
+ * @param e the event associated with the click.
+ * @return true if the event was handled.
+ */
+ boolean onContextClick(@NonNull MotionEvent e);
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/OnDragInitiatedListener.java b/app/src/main/java/androidx/recyclerview/selection/OnDragInitiatedListener.java
new file mode 100644
index 0000000000..50d8f1b3d7
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/OnDragInitiatedListener.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+import android.content.ClipData;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Register an OnDragInitiatedListener to be notified when user intent to perform drag and drop
+ * operations on an item or items has been detected. Handle these events using {@link View}
+ * support for Drag and drop.
+ *
+ *
+ * See {@link View#startDragAndDrop(ClipData, View.DragShadowBuilder, Object, int)}
+ * for details.
+ */
+public interface OnDragInitiatedListener {
+
+ /**
+ * Called when user intent to perform a drag and drop operation has been detected.
+ *
+ *
+ * The following circumstances are considered to be expressing drag and drop intent:
+ *
+ *
+ * - Long press on selected item.
+ * - Click and drag in the {@link ItemDetails#inDragRegion(MotionEvent) drag region}
+ * of selected item with a pointer device.
+ * - Click and drag in drag region of un-selected item with a pointer device.
+ *
+ *
+ *
+ * The RecyclerView item at the coordinates of the MotionEvent is not supplied as a parameter
+ * to this method as there may be multiple items selected or no items selected (as may be
+ * the case in pointer drive drag and drop.)
+ *
+ *
+ * Obtain the current list of selected items from
+ * {@link SelectionTracker#copySelection(MutableSelection)}. If there is no selection
+ * get the item under the event using {@link ItemDetailsLookup#getItemDetails(MotionEvent)}.
+ *
+ *
+ * Drag region used with pointer devices is specified by
+ * {@link ItemDetails#inDragRegion(MotionEvent)}
+ *
+ *
+ * See {@link android.view.View#startDragAndDrop(ClipData, View.DragShadowBuilder, Object, int)}
+ * for details on drag and drop implementation.
+ *
+ * @param e the event associated with the drag.
+ * @return true if drag and drop was initiated.
+ */
+ boolean onDragInitiated(@NonNull MotionEvent e);
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/OnItemActivatedListener.java b/app/src/main/java/androidx/recyclerview/selection/OnItemActivatedListener.java
new file mode 100644
index 0000000000..60c41c642c
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/OnItemActivatedListener.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+/**
+ * Register an OnItemActivatedListener to be notified when an item is activated
+ * (tapped or double clicked).
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+public interface OnItemActivatedListener {
+
+ /**
+ * Called when an item is "activated". An item is activated, for example, when no selection
+ * exists and the user taps an item with her finger, or double clicks an item with a
+ * pointing device like a Mouse.
+ *
+ * @param item details of the item.
+ * @param e the event associated with item.
+ *
+ * @return true if the event was handled.
+ */
+ boolean onItemActivated(@NonNull ItemDetails item, @NonNull MotionEvent e);
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/OperationMonitor.java b/app/src/main/java/androidx/recyclerview/selection/OperationMonitor.java
new file mode 100644
index 0000000000..f5a4508d1a
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/OperationMonitor.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+import static androidx.core.util.Preconditions.checkState;
+import static androidx.recyclerview.selection.Shared.DEBUG;
+
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * OperationMonitor provides a mechanism to coordinate application
+ * logic with ongoing user selection activities (such as active band selection
+ * and active gesture selection).
+ *
+ *
+ * The host {@link android.app.Activity} or {@link android.app.Fragment} should avoid changing
+ * {@link RecyclerView.Adapter Adapter} data while there
+ * are active selection operations, as this can result in a poor user experience.
+ *
+ *
+ * To know when an operation is active listen to changes using an {@link OnChangeListener}.
+ */
+public final class OperationMonitor {
+
+ private static final String TAG = "OperationMonitor";
+
+ private int mNumOps = 0;
+ private List mListeners = new ArrayList<>();
+
+ @MainThread
+ synchronized void start() {
+ mNumOps++;
+
+ if (mNumOps == 1) {
+ for (OnChangeListener l : mListeners) {
+ l.onChanged();
+ }
+ }
+
+ if (DEBUG) Log.v(TAG, "Incremented content lock count to " + mNumOps + ".");
+ }
+
+ @MainThread
+ synchronized void stop() {
+ checkState(mNumOps > 0);
+
+ mNumOps--;
+ if (DEBUG) Log.v(TAG, "Decremented content lock count to " + mNumOps + ".");
+
+ if (mNumOps == 0) {
+ for (OnChangeListener l : mListeners) {
+ l.onChanged();
+ }
+ }
+ }
+
+ /**
+ * @return true if there are any running operations.
+ */
+ @SuppressWarnings("unused")
+ public synchronized boolean isStarted() {
+ return mNumOps > 0;
+ }
+
+ /**
+ * Registers supplied listener to be notified when operation status changes.
+ * @param listener
+ */
+ public void addListener(@NonNull OnChangeListener listener) {
+ checkArgument(listener != null);
+ mListeners.add(listener);
+ }
+
+ /**
+ * Unregisters listener for further notifications.
+ * @param listener
+ */
+ public void removeListener(@NonNull OnChangeListener listener) {
+ checkArgument(listener != null);
+ mListeners.remove(listener);
+ }
+
+ /**
+ * Allows other selection code to perform a precondition check asserting the state is locked.
+ */
+ void checkStarted() {
+ checkState(mNumOps > 0);
+ }
+
+ /**
+ * Allows other selection code to perform a precondition check asserting the state is unlocked.
+ */
+ void checkStopped() {
+ checkState(mNumOps == 0);
+ }
+
+ /**
+ * Listen to changes in operation status. Authors should avoid
+ * changing the Adapter model while there are active operations.
+ */
+ public interface OnChangeListener {
+
+ /**
+ * Called when operation status changes. Call {@link OperationMonitor#isStarted()}
+ * to determine the current status.
+ */
+ void onChanged();
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/PointerDragEventInterceptor.java b/app/src/main/java/androidx/recyclerview/selection/PointerDragEventInterceptor.java
new file mode 100644
index 0000000000..46ec5ddf10
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/PointerDragEventInterceptor.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
+
+/**
+ * OnItemTouchListener that delegates drag events to a drag listener,
+ * else sends event to fallback {@link OnItemTouchListener}.
+ *
+ * See {@link OnDragInitiatedListener} for details on implementing drag and drop.
+ */
+final class PointerDragEventInterceptor implements OnItemTouchListener {
+
+ private final ItemDetailsLookup mEventDetailsLookup;
+ private final OnDragInitiatedListener mDragListener;
+ private @Nullable OnItemTouchListener mDelegate;
+
+ PointerDragEventInterceptor(
+ ItemDetailsLookup eventDetailsLookup,
+ OnDragInitiatedListener dragListener,
+ @Nullable OnItemTouchListener delegate) {
+
+ checkArgument(eventDetailsLookup != null);
+ checkArgument(dragListener != null);
+
+ mEventDetailsLookup = eventDetailsLookup;
+ mDragListener = dragListener;
+ mDelegate = delegate;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ if (MotionEvents.isPointerDragEvent(e) && mEventDetailsLookup.inItemDragRegion(e)) {
+ return mDragListener.onDragInitiated(e);
+ } else if (mDelegate != null) {
+ return mDelegate.onInterceptTouchEvent(rv, e);
+ }
+ return false;
+ }
+
+ @Override
+ public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ if (mDelegate != null) {
+ mDelegate.onTouchEvent(rv, e);
+ }
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ if (mDelegate != null) {
+ mDelegate.onRequestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/Range.java b/app/src/main/java/androidx/recyclerview/selection/Range.java
new file mode 100644
index 0000000000..35fb3a8ba5
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/Range.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+import static androidx.recyclerview.selection.Shared.DEBUG;
+import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
+
+import android.util.Log;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Class providing support for managing range selections.
+ */
+final class Range {
+
+ static final int TYPE_PRIMARY = 0;
+
+ /**
+ * "Provisional" selection represents a overlay on the primary selection. A provisional
+ * selection maybe be eventually added to the primary selection, or it may be abandoned.
+ *
+ *
+ * E.g. BandSelectionHelper creates a provisional selection while a user is actively
+ * selecting items with a band. GestureSelectionHelper creates a provisional selection
+ * while a user is active selecting via gesture.
+ *
+ *
+ * Provisionally selected items are considered to be selected in
+ * {@link Selection#contains(String)} and related methods. A provisional may be abandoned or
+ * merged into the promary selection.
+ *
+ *
+ * A provisional selection may intersect with the primary selection, however clearing the
+ * provisional selection will not affect the primary selection where the two may intersect.
+ */
+ static final int TYPE_PROVISIONAL = 1;
+ @IntDef({
+ TYPE_PRIMARY,
+ TYPE_PROVISIONAL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface RangeType {}
+
+ private static final String TAG = "Range";
+
+ private final Callbacks mCallbacks;
+ private final int mBegin;
+ private int mEnd = NO_POSITION;
+
+ /**
+ * Creates a new range anchored at {@code position}.
+ *
+ * @param position
+ * @param callbacks
+ */
+ Range(int position, @NonNull Callbacks callbacks) {
+ mBegin = position;
+ mCallbacks = callbacks;
+ if (DEBUG) Log.d(TAG, "Creating new Range anchored @ " + position);
+ }
+
+ void extendRange(int position, @RangeType int type) {
+ checkArgument(position != NO_POSITION, "Position cannot be NO_POSITION.");
+
+ if (mEnd == NO_POSITION || mEnd == mBegin) {
+ // Reset mEnd so it can be established in establishRange.
+ mEnd = NO_POSITION;
+ establishRange(position, type);
+ } else {
+ reviseRange(position, type);
+ }
+ }
+
+ private void establishRange(int position, @RangeType int type) {
+ checkArgument(mEnd == NO_POSITION, "End has already been set.");
+
+ mEnd = position;
+
+ if (position > mBegin) {
+ if (DEBUG) log(type, "Establishing initial range at @ " + position);
+ updateRange(mBegin + 1, position, true, type);
+ } else if (position < mBegin) {
+ if (DEBUG) log(type, "Establishing initial range at @ " + position);
+ updateRange(position, mBegin - 1, true, type);
+ }
+ }
+
+ private void reviseRange(int position, @RangeType int type) {
+ checkArgument(mEnd != NO_POSITION, "End must already be set.");
+ checkArgument(mBegin != mEnd, "Beging and end point to same position.");
+
+ if (position == mEnd) {
+ if (DEBUG) log(type, "Ignoring no-op revision for range @ " + position);
+ }
+
+ if (mEnd > mBegin) {
+ reviseAscending(position, type);
+ } else if (mEnd < mBegin) {
+ reviseDescending(position, type);
+ }
+ // the "else" case is covered by checkState at beginning of method.
+
+ mEnd = position;
+ }
+
+ /**
+ * Updates an existing ascending selection.
+ */
+ private void reviseAscending(int position, @RangeType int type) {
+ if (DEBUG) log(type, "*ascending* Revising range @ " + position);
+
+ if (position < mEnd) {
+ if (position < mBegin) {
+ updateRange(mBegin + 1, mEnd, false, type);
+ updateRange(position, mBegin - 1, true, type);
+ } else {
+ updateRange(position + 1, mEnd, false, type);
+ }
+ } else if (position > mEnd) { // Extending the range...
+ updateRange(mEnd + 1, position, true, type);
+ }
+ }
+
+ private void reviseDescending(int position, @RangeType int type) {
+ if (DEBUG) log(type, "*descending* Revising range @ " + position);
+
+ if (position > mEnd) {
+ if (position > mBegin) {
+ updateRange(mEnd, mBegin - 1, false, type);
+ updateRange(mBegin + 1, position, true, type);
+ } else {
+ updateRange(mEnd, position - 1, false, type);
+ }
+ } else if (position < mEnd) { // Extending the range...
+ 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) {
+ mCallbacks.updateForRange(begin, end, selected, type);
+ }
+
+ @Override
+ public String toString() {
+ return "Range{begin=" + mBegin + ", end=" + mEnd + "}";
+ }
+
+ private void log(@RangeType int type, String message) {
+ String opType = type == TYPE_PRIMARY ? "PRIMARY" : "PROVISIONAL";
+ Log.d(TAG, String.valueOf(this) + ": " + message + " (" + opType + ")");
+ }
+
+ /*
+ * @see {@link DefaultSelectionTracker#updateForRange(int, int , boolean, int)}.
+ */
+ abstract static class Callbacks {
+ abstract void updateForRange(
+ int begin, int end, boolean selected, @RangeType int type);
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/Selection.java b/app/src/main/java/androidx/recyclerview/selection/Selection.java
new file mode 100644
index 0000000000..78c816fb15
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/Selection.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Object representing a "primary" selection and a "provisional" selection.
+ *
+ *
+ * This class tracks selected items by managing two sets:
+ *
+ *
+ * Primary Selection
+ *
+ *
+ * Primary selection consists of items selected by a user. This represents the selection
+ * "at rest", as the selection does not contains items that are in a "provisional" selected
+ * state created by way of an ongoing gesture or band operation.
+ *
+ *
+ * Provisional Selection
+ *
+ *
+ * Provisional selections are selections which are interim in nature.
+ *
+ *
+ * Provisional selection exists to address issues where a transitory selection might
+ * momentarily intersect with a previously established selection resulting in a some
+ * or all of the established selection being erased. Such situations may arise
+ * when band selection is being performed in "additive" mode (e.g. SHIFT or CTRL is pressed
+ * on the keyboard prior to mouse down), or when there's an active gesture selection
+ * (which can be initiated by long pressing an unselected item while there is an
+ * existing selection).
+ *
+ * @see MutableSelection
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+public class Selection implements Iterable {
+
+ // NOTE: Not currently private as DefaultSelectionTracker directly manipulates values.
+ final Set mSelection;
+ final Set mProvisionalSelection;
+
+ Selection() {
+ mSelection = new HashSet<>();
+ mProvisionalSelection = new HashSet<>();
+ }
+
+ /**
+ * Used by {@link StorageStrategy} when restoring selection.
+ */
+ Selection(@NonNull Set selection) {
+ mSelection = selection;
+ mProvisionalSelection = new HashSet<>();
+ }
+
+ /**
+ * @param key
+ * @return true if the position is currently selected.
+ */
+ public boolean contains(@Nullable K key) {
+ return mSelection.contains(key) || mProvisionalSelection.contains(key);
+ }
+
+ /**
+ * Returns an {@link Iterator} that iterators over the selection, *excluding*
+ * any provisional selection.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public Iterator iterator() {
+ return mSelection.iterator();
+ }
+
+ /**
+ * @return size of the selection including both final and provisional selected items.
+ */
+ public int size() {
+ return mSelection.size() + mProvisionalSelection.size();
+ }
+
+ /**
+ * @return true if the selection is empty.
+ */
+ public boolean isEmpty() {
+ return mSelection.isEmpty() && mProvisionalSelection.isEmpty();
+ }
+
+ /**
+ * Sets the provisional selection, which is a temporary selection that can be saved,
+ * canceled, or adjusted at a later time. When a new provision selection is applied, the old
+ * one (if it exists) is abandoned.
+ * @return Map of ids added or removed. Added ids have a value of true, removed are false.
+ */
+ Map setProvisionalSelection(@NonNull Set newSelection) {
+ Map delta = new HashMap<>();
+
+ for (K key: mProvisionalSelection) {
+ // Mark each item that used to be in the provisional selection
+ // but is not in the new provisional selection.
+ if (!newSelection.contains(key) && !mSelection.contains(key)) {
+ delta.put(key, false);
+ }
+ }
+
+ for (K key: mSelection) {
+ // Mark each item that used to be in the selection but is unsaved and not in the new
+ // provisional selection.
+ if (!newSelection.contains(key)) {
+ delta.put(key, false);
+ }
+ }
+
+ for (K key: newSelection) {
+ // Mark each item that was not previously in the selection but is in the new
+ // provisional selection.
+ if (!mSelection.contains(key) && !mProvisionalSelection.contains(key)) {
+ delta.put(key, true);
+ }
+ }
+
+ // Now, iterate through the changes and actually add/remove them to/from the current
+ // selection. This could not be done in the previous loops because changing the size of
+ // the selection mid-iteration changes iteration order erroneously.
+ for (Map.Entry entry: delta.entrySet()) {
+ K key = entry.getKey();
+ if (entry.getValue()) {
+ mProvisionalSelection.add(key);
+ } else {
+ mProvisionalSelection.remove(key);
+ }
+ }
+
+ return delta;
+ }
+
+ /**
+ * Saves the existing provisional selection. Once the provisional selection is saved,
+ * subsequent provisional selections which are different from this existing one cannot
+ * cause items in this existing provisional selection to become deselected.
+ */
+ void mergeProvisionalSelection() {
+ mSelection.addAll(mProvisionalSelection);
+ mProvisionalSelection.clear();
+ }
+
+ /**
+ * Abandons the existing provisional selection so that all items provisionally selected are
+ * now deselected.
+ */
+ void clearProvisionalSelection() {
+ mProvisionalSelection.clear();
+ }
+
+ /**
+ * Adds a new item to the primary selection.
+ *
+ * @return true if the operation resulted in a modification to the selection.
+ */
+ boolean add(@NonNull K key) {
+ return mSelection.add(key);
+ }
+
+ /**
+ * Removes an item from the primary selection.
+ *
+ * @return true if the operation resulted in a modification to the selection.
+ */
+ boolean remove(@NonNull K key) {
+ return mSelection.remove(key);
+ }
+
+ /**
+ * Clears the primary selection. The provisional selection, if any, is unaffected.
+ */
+ void clear() {
+ mSelection.clear();
+ }
+
+ /**
+ * Clones primary and provisional selection from supplied {@link Selection}.
+ * Does not copy active range data.
+ */
+ void copyFrom(@NonNull Selection source) {
+ mSelection.clear();
+ mSelection.addAll(source.mSelection);
+
+ mProvisionalSelection.clear();
+ mProvisionalSelection.addAll(source.mProvisionalSelection);
+ }
+
+ @Override
+ public String toString() {
+ if (size() <= 0) {
+ return "size=0, items=[]";
+ }
+
+ StringBuilder buffer = new StringBuilder(size() * 28);
+ buffer.append("Selection{")
+ .append("primary{size=" + mSelection.size())
+ .append(", entries=" + mSelection)
+ .append("}, provisional{size=" + mProvisionalSelection.size())
+ .append(", entries=" + mProvisionalSelection)
+ .append("}}");
+ return buffer.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return mSelection.hashCode() ^ mProvisionalSelection.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return (this == other)
+ || (other instanceof Selection && isEqualTo((Selection) other));
+ }
+
+ private boolean isEqualTo(Selection other) {
+ return mSelection.equals(other.mSelection)
+ && mProvisionalSelection.equals(other.mProvisionalSelection);
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/SelectionPredicates.java b/app/src/main/java/androidx/recyclerview/selection/SelectionPredicates.java
new file mode 100644
index 0000000000..1e13bdefb3
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/SelectionPredicates.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
+
+/**
+ * Utility class for creating SelectionPredicate instances. Provides default
+ * implementations for common cases like "single selection" and "select anything".
+ */
+public final class SelectionPredicates {
+
+ private SelectionPredicates() {}
+
+ /**
+ * Returns a selection predicate that allows multiples items to be selected, without
+ * any restrictions on which items can be selected.
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ * @return
+ */
+ public static SelectionPredicate createSelectAnything() {
+ return new SelectionPredicate() {
+ @Override
+ public boolean canSetStateForKey(@NonNull K key, boolean nextState) {
+ return true;
+ }
+
+ @Override
+ public boolean canSetStateAtPosition(int position, boolean nextState) {
+ return true;
+ }
+
+ @Override
+ public boolean canSelectMultiple() {
+ return true;
+ }
+ };
+ }
+
+ /**
+ * Returns a selection predicate that allows a single item to be selected, without
+ * any restrictions on which item can be selected.
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ * @return
+ */
+ public static SelectionPredicate createSelectSingleAnything() {
+ return new SelectionPredicate() {
+ @Override
+ public boolean canSetStateForKey(@NonNull K key, boolean nextState) {
+ return true;
+ }
+
+ @Override
+ public boolean canSetStateAtPosition(int position, boolean nextState) {
+ return true;
+ }
+
+ @Override
+ public boolean canSelectMultiple() {
+ return false;
+ }
+ };
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java b/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
new file mode 100644
index 0000000000..f59bd9b692
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
@@ -0,0 +1,817 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.GestureDetector;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
+import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
+
+import java.util.Set;
+
+/**
+ * SelectionTracker provides support for managing a selection of items in a RecyclerView instance.
+ *
+ *
+ * This class provides support for managing a "primary" set of selected items,
+ * in addition to a "provisional" set of selected items using conventional
+ * {@link java.util.Collections}-like methods.
+ *
+ *
+ * Create an instance of SelectionTracker using {@link Builder SelectionTracker.Builder}.
+ *
+ *
+ * Inspecting the current selection
+ *
+ *
+ * The underlying selection is described by the {@link Selection} class.
+ *
+ *
+ * A live view of the current selection can be obtained using {@link #getSelection}. Changes made
+ * to the selection using SelectionTracker will be immediately reflected in this instance.
+ *
+ *
+ * To obtain a stable snapshot of the selection use {@link #copySelection(MutableSelection)}.
+ *
+ *
+ * Selection state for an individual item can be obtained using {@link #isSelected(Object)}.
+ *
+ *
+ * Provisional Selection
+ *
+ *
+ * Provisional selection exists to address issues where a transitory selection might
+ * momentarily intersect with a previously established selection resulting in a some
+ * or all of the established selection being erased. Such situations may arise
+ * when band selection is being performed in "additive" mode (e.g. SHIFT or CTRL is pressed
+ * on the keyboard prior to mouse down), or when there's an active gesture selection
+ * (which can be initiated by long pressing an unselected item while there is an
+ * existing selection).
+ *
+ *
+ * A provisional selection can be abandoned, or merged into the primary selection.
+ *
+ *
+ * Enforcing selection policies
+ *
+ *
+ * Which items can be selected by the user is a matter of policy in an Application.
+ * Developers supply these policies by way of {@link SelectionPredicate}.
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+public abstract class SelectionTracker {
+
+ /**
+ * This value is included in the payload when SelectionTracker notifies RecyclerView
+ * of changes to selection. Look for this value in the {@code payload}
+ * Object argument supplied to
+ * {@link RecyclerView.Adapter#onBindViewHolder
+ * Adapter#onBindViewHolder}.
+ * If present the call is occurring in response to a selection state change.
+ * This would be a good opportunity to animate changes between unselected and selected state.
+ * When state is being restored, this argument will not be present.
+ */
+ public static final String SELECTION_CHANGED_MARKER = "Selection-Changed";
+
+ /**
+ * Adds {@code observer} to be notified when changes to selection occur.
+ *
+ *
+ * Use an observer to track attributes about the selection and
+ * update the UI to reflect the state of the selection. For example, an author
+ * may use an observer to control the enabled status of menu items,
+ * or to initiate {@link android.view.ActionMode}.
+ */
+ public abstract void addObserver(SelectionObserver observer);
+
+ /** @return true if has a selection */
+ public abstract boolean hasSelection();
+
+ /**
+ * Returns a Selection object that provides a live view on the current selection.
+ *
+ * @return The current selection.
+ * @see #copySelection(MutableSelection) on how to get a snapshot
+ * of the selection that will not reflect future changes
+ * to selection.
+ */
+ public abstract Selection getSelection();
+
+ /**
+ * Updates {@code dest} to reflect the current selection.
+ */
+ public abstract void copySelection(@NonNull MutableSelection dest);
+
+ /**
+ * @return true if the item specified by its id is selected. Shorthand for
+ * {@code getSelection().contains(K)}.
+ */
+ public abstract boolean isSelected(@Nullable K key);
+
+ /**
+ * Restores the selected state of specified items. Used in cases such as restore the selection
+ * after rotation etc. Provisional selection is not restored.
+ *
+ *
+ * This affords clients the ability to restore selection from selection saved
+ * in Activity state.
+ *
+ * @see StorageStrategy details on selection state support.
+ *
+ * @param selection selection being restored.
+ */
+ protected abstract void restoreSelection(@NonNull Selection selection);
+
+ /**
+ * Clears both primary and provisional selections.
+ *
+ * @return true if primary selection changed.
+ */
+ public abstract boolean clearSelection();
+
+ /**
+ * Sets the selected state of the specified items if permitted after consulting
+ * SelectionPredicate.
+ */
+ public abstract boolean setItemsSelected(@NonNull Iterable keys, boolean selected);
+
+ /**
+ * Attempts to select an item.
+ *
+ * @return true if the item was selected. False if the item could not be selected, or was
+ * was already selected.
+ */
+ public abstract boolean select(@NonNull K key);
+
+ /**
+ * Attempts to deselect an item.
+ *
+ * @return true if the item was deselected. False if the item could not be deselected, or was
+ * was already un-selected.
+ */
+ public abstract boolean deselect(@NonNull K key);
+
+ abstract AdapterDataObserver getAdapterDataObserver();
+
+ /**
+ * Attempts to establish a range selection at {@code position}, selecting the item
+ * at {@code position} if needed.
+ *
+ * @param position The "anchor" position for the range. Subsequent range operations
+ * (primarily keyboard and mouse based operations like SHIFT + click)
+ * work with the established anchor point to define selection ranges.
+ */
+ abstract void startRange(int position);
+
+ /**
+ * Sets the end point for the active range selection.
+ *
+ *
+ * This function should only be called when a range selection is active
+ * (see {@link #isRangeActive()}. Items in the range [anchor, end] will be
+ * selected after consulting SelectionPredicate.
+ *
+ * @param position The new end position for the selection range.
+ * @throws IllegalStateException if a range selection is not active. Range selection
+ * must have been started by a call to {@link #startRange(int)}.
+ */
+ abstract void extendRange(int position);
+
+ /**
+ * Clears an in-progress range selection. Provisional range selection established
+ * using {@link #extendProvisionalRange(int)} will be cleared (unless
+ * {@link #mergeProvisionalSelection()} is called first.)
+ */
+ abstract void endRange();
+
+ /**
+ * @return Whether or not there is a current range selection active.
+ */
+ abstract boolean isRangeActive();
+
+ /**
+ * Establishes the "anchor" at which a selection range begins. This "anchor" is consulted
+ * when determining how to extend, and modify selection ranges. Calling this when a
+ * range selection is active will reset the range selection.
+ *
+ * TODO: Reconcile this with startRange. Maybe just docs need to be updated.
+ *
+ * @param position the anchor position. Must already be selected.
+ */
+ abstract void anchorRange(int position);
+
+ /**
+ * Creates a provisional selection from anchor to {@code position}.
+ *
+ * @param position the end point.
+ */
+ abstract void extendProvisionalRange(int position);
+
+ /**
+ * Sets the provisional selection, replacing any existing selection.
+ * @param newSelection
+ */
+ abstract void setProvisionalSelection(@NonNull Set newSelection);
+
+ /**
+ * Clears any existing provisional selection
+ */
+ abstract void clearProvisionalSelection();
+
+ /**
+ * Converts the provisional selection into primary selection, then clears
+ * provisional selection.
+ */
+ abstract void mergeProvisionalSelection();
+
+ /**
+ * Preserves selection, if any. Call this method from Activity#onSaveInstanceState
+ *
+ * @param state Bundle instance supplied to onSaveInstanceState.
+ */
+ public abstract void onSaveInstanceState(@NonNull Bundle state);
+
+ /**
+ * Restores selection from previously saved state. Call this method from
+ * Activity#onCreate.
+ *
+ * @param state Bundle instance supplied to onCreate.
+ */
+ public abstract void onRestoreInstanceState(@Nullable Bundle state);
+
+ /**
+ * Observer class providing access to information about Selection state changes.
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+ public abstract static class SelectionObserver {
+
+ /**
+ * Called when the state of an item has been changed.
+ */
+ public void onItemStateChanged(@NonNull K key, boolean selected) {
+ }
+
+ /**
+ * Called when the underlying data set has changed. After this method is called
+ * SelectionTracker will traverse the existing selection,
+ * calling {@link #onItemStateChanged(K, boolean)} for each selected item,
+ * and deselecting any items that cannot be selected given the updated data-set
+ * (and after consulting SelectionPredicate).
+ */
+ public void onSelectionRefresh() {
+ }
+
+ /**
+ * Called immediately after completion of any set of changes, excluding
+ * those resulting in calls to {@link #onSelectionRefresh()} and
+ * {@link #onSelectionRestored()}.
+ */
+ public void onSelectionChanged() {
+ }
+
+ /**
+ * Called immediately after selection is restored.
+ * {@link #onItemStateChanged(K, boolean)} will *not* be called
+ * for individual items in the selection.
+ */
+ public void onSelectionRestored() {
+ }
+ }
+
+ /**
+ * Implement SelectionPredicate to control when items can be selected or unselected.
+ * See {@link Builder#withSelectionPredicate(SelectionPredicate)}.
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+ public abstract static class SelectionPredicate {
+
+ /**
+ * Validates a change to selection for a specific key.
+ *
+ * @param key the item key
+ * @param nextState the next potential selected/unselected state
+ * @return true if the item at {@code id} can be set to {@code nextState}.
+ */
+ public abstract boolean canSetStateForKey(@NonNull K key, boolean nextState);
+
+ /**
+ * Validates a change to selection for a specific position. If necessary
+ * use {@link ItemKeyProvider} to identy associated key.
+ *
+ * @param position the item position
+ * @param nextState the next potential selected/unselected state
+ * @return true if the item at {@code id} can be set to {@code nextState}.
+ */
+ public abstract boolean canSetStateAtPosition(int position, boolean nextState);
+
+ /**
+ * Permits restriction to single selection mode. Single selection mode has
+ * unique behaviors in that it'll deselect an item already selected
+ * in order to select the new item.
+ *
+ *
+ * In order to limit the number of items that can be selected,
+ * use {@link #canSetStateForKey(Object, boolean)} and
+ * {@link #canSetStateAtPosition(int, boolean)}.
+ *
+ * @return true if more than a single item can be selected.
+ */
+ public abstract boolean canSelectMultiple();
+ }
+
+ /**
+ * Builder is the primary mechanism for create a {@link SelectionTracker} that
+ * can be used with your RecyclerView. Once installed, users will be able to create and
+ * manipulate selection using a variety of intuitive techniques like tap, gesture,
+ * and mouse lasso.
+ *
+ *
+ * Example usage:
+ *
SelectionTracker tracker = new SelectionTracker.Builder<>(
+ * "my-uri-selection",
+ * recyclerView,
+ * new DemoStableIdProvider(recyclerView.getAdapter()),
+ * new MyDetailsLookup(recyclerView),
+ * StorageStrategy.createParcelableStorage(Uri.class))
+ * .build();
+ *
+ *
+ *
+ * Restricting which items can be selected and limiting selection size
+ *
+ *
+ * {@link SelectionPredicate} provides a mechanism to restrict which Items can be selected,
+ * to limit the number of items that can be selected, as well as allowing the selection
+ * code to be placed into "single select" mode, which as the name indicates, constrains
+ * the selection size to a single item.
+ *
+ *
Configuring the tracker for single single selection support can be done
+ * by supplying {@link SelectionPredicates#createSelectSingleAnything()}.
+ *
+ * SelectionTracker tracker = new SelectionTracker.Builder<>(
+ * "my-string-selection",
+ * recyclerView,
+ * new DemoStableIdProvider(recyclerView.getAdapter()),
+ * new MyDetailsLookup(recyclerView),
+ * StorageStrategy.createStringStorage())
+ * .withSelectionPredicate(SelectionPredicates#createSelectSingleAnything())
+ * .build();
+ *
+ *
+ * Retaining state across Android lifecycle events
+ *
+ *
+ * Support for storage/persistence of selection must be configured and invoked manually
+ * owing to its reliance on Activity lifecycle events.
+ * Failure to include support for selection storage will result in the active selection
+ * being lost when the Activity receives a configuration change (e.g. rotation)
+ * or when the application process is destroyed by the OS to reclaim resources.
+ *
+ *
+ * Key Type
+ *
+ *
+ * Developers must decide on the key type used to identify selected items. Support
+ * is provided for three types: {@link Parcelable}, {@link String}, and {@link Long}.
+ *
+ *
+ * {@link Parcelable}: Any Parcelable type can be used as the selection key. This is especially
+ * useful in conjunction with {@link android.net.Uri} as the Android URI implementation is both
+ * parcelable and makes for a natural stable selection key for values represented by
+ * the Android Content Provider framework. If items in your view are associated with
+ * stable {@code content://} uris, you should use Uri for your key type.
+ *
+ *
+ * {@link String}: Use String when a string based stable identifier is available.
+ *
+ *
+ * {@link Long}: Use Long when RecyclerView's long stable ids are
+ * already in use. It comes with some limitations, however, as access to stable ids
+ * at runtime is limited. Band selection support is not available when using the default
+ * long key storage implementation. See {@link StableIdKeyProvider} for details.
+ *
+ *
+ * Usage:
+ *
+ *
+ * private SelectionTracker mTracker;
+ *
+ * public void onCreate(Bundle savedInstanceState) {
+ * // See above for details on constructing a SelectionTracker instance.
+ *
+ * if (savedInstanceState != null) {
+ * mTracker.onRestoreInstanceState(savedInstanceState);
+ * }
+ * }
+ *
+ * protected void onSaveInstanceState(Bundle outState) {
+ * super.onSaveInstanceState(outState);
+ * mTracker.onSaveInstanceState(outState);
+ * }
+ *
+ *
+ * @param Selection key type. Built in support is provided for {@link String},
+ * {@link Long}, and {@link Parcelable}. {@link StorageStrategy}
+ * provides factory methods for each type:
+ * {@link StorageStrategy#createStringStorage()},
+ * {@link StorageStrategy#createParcelableStorage(Class)},
+ * {@link StorageStrategy#createLongStorage()}
+ */
+ public static final class Builder {
+
+ final RecyclerView mRecyclerView;
+ private final RecyclerView.Adapter> mAdapter;
+ private final Context mContext;
+ private final String mSelectionId;
+ private final StorageStrategy mStorage;
+
+ SelectionPredicate mSelectionPredicate =
+ SelectionPredicates.createSelectAnything();
+ private OperationMonitor mMonitor = new OperationMonitor();
+ private ItemKeyProvider mKeyProvider;
+ private ItemDetailsLookup mDetailsLookup;
+
+ private FocusDelegate mFocusDelegate = FocusDelegate.dummy();
+
+ private OnItemActivatedListener mOnItemActivatedListener;
+ private OnDragInitiatedListener mOnDragInitiatedListener;
+ private OnContextClickListener mOnContextClickListener;
+
+ private BandPredicate mBandPredicate;
+ private int mBandOverlayId = eu.faircode.email.R.drawable.selection_band_overlay;
+
+ private int[] mGestureToolTypes = new int[] {
+ MotionEvent.TOOL_TYPE_FINGER,
+ MotionEvent.TOOL_TYPE_UNKNOWN
+ };
+
+ private int[] mPointerToolTypes = new int[] {
+ MotionEvent.TOOL_TYPE_MOUSE
+ };
+
+ /**
+ * Creates a new SelectionTracker.Builder useful for configuring and creating
+ * a new SelectionTracker for use with your {@link RecyclerView}.
+ *
+ * @param selectionId A unique string identifying this selection in the context
+ * of the activity or fragment.
+ * @param recyclerView the owning RecyclerView
+ * @param keyProvider the source of selection keys
+ * @param detailsLookup the source of information about RecyclerView items.
+ * @param storage Strategy for type-safe storage of selection state in
+ * {@link Bundle}.
+ */
+ public Builder(
+ @NonNull String selectionId,
+ @NonNull RecyclerView recyclerView,
+ @NonNull ItemKeyProvider keyProvider,
+ @NonNull ItemDetailsLookup detailsLookup,
+ @NonNull StorageStrategy storage) {
+
+ checkArgument(selectionId != null);
+ checkArgument(!selectionId.trim().isEmpty());
+ checkArgument(recyclerView != null);
+
+ mSelectionId = selectionId;
+ mRecyclerView = recyclerView;
+ mContext = recyclerView.getContext();
+ mAdapter = recyclerView.getAdapter();
+
+ checkArgument(mAdapter != null);
+ checkArgument(keyProvider != null);
+ checkArgument(detailsLookup != null);
+ checkArgument(storage != null);
+
+ mDetailsLookup = detailsLookup;
+ mKeyProvider = keyProvider;
+ mStorage = storage;
+
+ mBandPredicate = new BandPredicate.NonDraggableArea(mRecyclerView, detailsLookup);
+ }
+
+ /**
+ * Install selection predicate.
+ *
+ * @param predicate the predicate to be used.
+ * @return this
+ */
+ public Builder withSelectionPredicate(
+ @NonNull SelectionPredicate predicate) {
+
+ checkArgument(predicate != null);
+ mSelectionPredicate = predicate;
+ return this;
+ }
+
+ /**
+ * Add operation monitor allowing access to information about active
+ * operations (like band selection and gesture selection).
+ *
+ * @param monitor the monitor to be used
+ * @return this
+ */
+ public Builder withOperationMonitor(
+ @NonNull OperationMonitor monitor) {
+
+ checkArgument(monitor != null);
+ mMonitor = monitor;
+ return this;
+ }
+
+ /**
+ * Add focus delegate to interact with selection related focus changes.
+ *
+ * @param delegate the delegate to be used
+ * @return this
+ */
+ public Builder withFocusDelegate(@NonNull FocusDelegate delegate) {
+ checkArgument(delegate != null);
+ mFocusDelegate = delegate;
+ return this;
+ }
+
+ /**
+ * Adds an item activation listener. Respond to taps/enter/double-click on items.
+ *
+ * @param listener the listener to be used
+ * @return this
+ */
+ public Builder withOnItemActivatedListener(
+ @NonNull OnItemActivatedListener listener) {
+
+ checkArgument(listener != null);
+
+ mOnItemActivatedListener = listener;
+ return this;
+ }
+
+ /**
+ * Adds a context click listener. Respond to right-click.
+ *
+ * @param listener the listener to be used
+ * @return this
+ */
+ public Builder withOnContextClickListener(
+ @NonNull OnContextClickListener listener) {
+
+ checkArgument(listener != null);
+
+ mOnContextClickListener = listener;
+ return this;
+ }
+
+ /**
+ * Adds a drag initiated listener. Add support for drag and drop.
+ *
+ * @param listener the listener to be used
+ * @return this
+ */
+ public Builder withOnDragInitiatedListener(
+ @NonNull OnDragInitiatedListener listener) {
+
+ checkArgument(listener != null);
+
+ mOnDragInitiatedListener = listener;
+ return this;
+ }
+
+ /**
+ * Replaces default tap and gesture tool-types. Defaults are:
+ * {@link MotionEvent#TOOL_TYPE_FINGER} and {@link MotionEvent#TOOL_TYPE_UNKNOWN}.
+ *
+ * @param toolTypes the tool types to be used
+ * @return this
+ */
+ public Builder withGestureTooltypes(int... toolTypes) {
+ mGestureToolTypes = toolTypes;
+ return this;
+ }
+
+ /**
+ * Replaces default band overlay.
+ *
+ * @param bandOverlayId
+ * @return this
+ */
+ public Builder withBandOverlay(@DrawableRes int bandOverlayId) {
+ mBandOverlayId = bandOverlayId;
+ return this;
+ }
+
+ /**
+ * Replaces default band predicate.
+ * @param bandPredicate
+ * @return this
+ */
+ public Builder withBandPredicate(@NonNull BandPredicate bandPredicate) {
+ checkArgument(bandPredicate != null);
+
+ mBandPredicate = bandPredicate;
+ return this;
+ }
+
+ /**
+ * Replaces default pointer tool-types. Pointer tools
+ * are associated with band selection, and certain
+ * drag and drop behaviors. Defaults are:
+ * {@link MotionEvent#TOOL_TYPE_MOUSE}.
+ *
+ * @param toolTypes the tool types to be used
+ * @return this
+ */
+ public Builder withPointerTooltypes(int... toolTypes) {
+ mPointerToolTypes = toolTypes;
+ return this;
+ }
+
+ /**
+ * Prepares and returns a SelectionTracker.
+ *
+ * @return this
+ */
+ public SelectionTracker build() {
+
+ SelectionTracker tracker = new DefaultSelectionTracker<>(
+ mSelectionId, mKeyProvider, mSelectionPredicate, mStorage);
+
+ // Event glue between RecyclerView and SelectionTracker keeps the classes separate
+ // so that a SelectionTracker can be shared across RecyclerView instances that
+ // represent the same data in different ways.
+ EventBridge.install(mAdapter, tracker, mKeyProvider);
+
+ AutoScroller scroller =
+ new ViewAutoScroller(ViewAutoScroller.createScrollHost(mRecyclerView));
+
+ // Setup basic input handling, with the touch handler as the default consumer
+ // of events. If mouse handling is configured as well, the mouse input
+ // related handlers will intercept mouse input events.
+
+ // GestureRouter is responsible for routing GestureDetector events
+ // to tool-type specific handlers.
+ GestureRouter gestureRouter = new GestureRouter<>();
+ GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter);
+
+ // TouchEventRouter takes its name from RecyclerView#OnItemTouchListener.
+ // Despite "Touch" being in the name, it receives events for all types of tools.
+ // This class is responsible for routing events to tool-type specific handlers,
+ // and if not handled by a handler, on to a GestureDetector for analysis.
+ TouchEventRouter eventRouter = new TouchEventRouter(gestureDetector);
+
+ // GestureSelectionHelper provides logic that interprets a combination
+ // of motions and gestures in order to provide gesture driven selection support
+ // when used in conjunction with RecyclerView.
+ final GestureSelectionHelper gestureHelper = GestureSelectionHelper.create(
+ tracker, mDetailsLookup, mRecyclerView, scroller, mMonitor);
+
+ // Finally hook the framework up to listening to recycle view events.
+ mRecyclerView.addOnItemTouchListener(eventRouter);
+
+ // But before you move on, there's more work to do. Event plumbing has been
+ // installed, but we haven't registered any of our helpers or callbacks.
+ // Helpers contain predefined logic converting events into selection related events.
+ // Callbacks provide developers the ability to reponspond to other types of
+ // events (like "activate" a tapped item). This is broken up into two main
+ // suites, one for "touch" and one for "mouse", though both can and should (usually)
+ // be configured to handle other types of input (to satisfy user expectation).);
+
+ // Internally, the code doesn't permit nullable listeners, so we lazily
+ // initialize dummy instances if the developer didn't supply a real listener.
+ mOnDragInitiatedListener = (mOnDragInitiatedListener != null)
+ ? mOnDragInitiatedListener
+ : new OnDragInitiatedListener() {
+ @Override
+ public boolean onDragInitiated(@NonNull MotionEvent e) {
+ return false;
+ }
+ };
+
+ mOnItemActivatedListener = (mOnItemActivatedListener != null)
+ ? mOnItemActivatedListener
+ : new OnItemActivatedListener() {
+ @Override
+ public boolean onItemActivated(
+ @NonNull ItemDetailsLookup.ItemDetails item,
+ @NonNull MotionEvent e) {
+ return false;
+ }
+ };
+
+ mOnContextClickListener = (mOnContextClickListener != null)
+ ? mOnContextClickListener
+ : new OnContextClickListener() {
+ @Override
+ public boolean onContextClick(@NonNull MotionEvent e) {
+ return false;
+ }
+ };
+
+ // Provides high level glue for binding touch events
+ // and gestures to selection framework.
+ TouchInputHandler touchHandler = new TouchInputHandler(
+ tracker,
+ mKeyProvider,
+ mDetailsLookup,
+ mSelectionPredicate,
+ new Runnable() {
+ @Override
+ public void run() {
+ if (mSelectionPredicate.canSelectMultiple()) {
+ try {
+ gestureHelper.start();
+ } catch (IllegalStateException ex) {
+ ex.printStackTrace();
+ }
+ }
+ }
+ },
+ mOnDragInitiatedListener,
+ mOnItemActivatedListener,
+ mFocusDelegate,
+ new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
+ });
+
+ for (int toolType : mGestureToolTypes) {
+ gestureRouter.register(toolType, touchHandler);
+ eventRouter.register(toolType, gestureHelper);
+ }
+
+ // Provides high level glue for binding mouse events and gestures
+ // to selection framework.
+ MouseInputHandler mouseHandler = new MouseInputHandler<>(
+ tracker,
+ mKeyProvider,
+ mDetailsLookup,
+ mOnContextClickListener,
+ mOnItemActivatedListener,
+ mFocusDelegate);
+
+ for (int toolType : mPointerToolTypes) {
+ gestureRouter.register(toolType, mouseHandler);
+ }
+
+ @Nullable BandSelectionHelper bandHelper = null;
+
+ // Band selection not supported in single select mode, or when key access
+ // is limited to anything less than the entire corpus.
+ if (mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED)
+ && mSelectionPredicate.canSelectMultiple()) {
+ // BandSelectionHelper provides support for band selection on-top of a RecyclerView
+ // instance. Given the recycling nature of RecyclerView BandSelectionController
+ // necessarily models and caches list/grid information as the user's pointer
+ // interacts with the item in the RecyclerView. Selectable items that intersect
+ // with the band, both on and off screen, are selected.
+ bandHelper = BandSelectionHelper.create(
+ mRecyclerView,
+ scroller,
+ mBandOverlayId,
+ mKeyProvider,
+ tracker,
+ mSelectionPredicate,
+ mBandPredicate,
+ mFocusDelegate,
+ mMonitor);
+ }
+
+ OnItemTouchListener pointerEventHandler = new PointerDragEventInterceptor(
+ mDetailsLookup, mOnDragInitiatedListener, bandHelper);
+
+ for (int toolType : mPointerToolTypes) {
+ eventRouter.register(toolType, pointerEventHandler);
+ }
+
+ return tracker;
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/Shared.java b/app/src/main/java/androidx/recyclerview/selection/Shared.java
new file mode 100644
index 0000000000..bded1e4b90
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/Shared.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+/**
+ * Shared constants used in this package. Disable DEBUG and VERBOSE prior to releases.
+ */
+final class Shared {
+
+ static final boolean DEBUG = false;
+ static final boolean VERBOSE = false;
+
+ private Shared() {}
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/StableIdKeyProvider.java b/app/src/main/java/androidx/recyclerview/selection/StableIdKeyProvider.java
new file mode 100644
index 0000000000..b6c9a36a88
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/StableIdKeyProvider.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import android.util.SparseArray;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * An {@link ItemKeyProvider} that provides stable ids by way of cached
+ * {@link RecyclerView.Adapter} stable ids. Items enter the cache as they are laid out by
+ * RecyclerView, and are removed from the cache as they are recycled.
+ *
+ *
+ * There are trade-offs with this implementation as it necessarily auto-boxes {@code long}
+ * stable id values into {@code Long} values for use as selection keys. The core Selection API
+ * uses a parameterized key type to permit other keys (such as Strings or URIs).
+ */
+public final class StableIdKeyProvider extends ItemKeyProvider {
+
+ private final SparseArray mPositionToKey = new SparseArray<>();
+ private final Map mKeyToPosition = new HashMap();
+ private final RecyclerView mRecyclerView;
+
+ /**
+ * Creates a new key provider that uses cached {@code long} stable ids associated
+ * with the RecyclerView items.
+ *
+ * @param recyclerView the owner RecyclerView
+ */
+ public StableIdKeyProvider(@NonNull RecyclerView recyclerView) {
+
+ // Since this provide is based on stable ids based on whats laid out in the window
+ // we can only satisfy "window" scope key access.
+ super(SCOPE_CACHED);
+
+ mRecyclerView = recyclerView;
+
+ mRecyclerView.addOnChildAttachStateChangeListener(
+ new OnChildAttachStateChangeListener() {
+ @Override
+ public void onChildViewAttachedToWindow(View view) {
+ onAttached(view);
+ }
+
+ @Override
+ public void onChildViewDetachedFromWindow(View view) {
+ onDetached(view);
+ }
+ }
+ );
+
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ void onAttached(@NonNull View view) {
+ RecyclerView.ViewHolder holder = mRecyclerView.findContainingViewHolder(view);
+ int position = holder.getAdapterPosition();
+ long id = holder.getItemId();
+ if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) {
+ mPositionToKey.put(position, id);
+ mKeyToPosition.put(id, position);
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ void onDetached(@NonNull View view) {
+ RecyclerView.ViewHolder holder = mRecyclerView.findContainingViewHolder(view);
+ int position = holder.getAdapterPosition();
+ long id = holder.getItemId();
+ if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) {
+ mPositionToKey.delete(position);
+ mKeyToPosition.remove(id);
+ }
+ }
+
+ @Override
+ public @Nullable Long getKey(int position) {
+ return mPositionToKey.get(position, null);
+ }
+
+ @Override
+ public int getPosition(@NonNull Long key) {
+ if (mKeyToPosition.containsKey(key)) {
+ return mKeyToPosition.get(key);
+ }
+ return RecyclerView.NO_POSITION;
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/StorageStrategy.java b/app/src/main/java/androidx/recyclerview/selection/StorageStrategy.java
new file mode 100644
index 0000000000..a27752f159
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/StorageStrategy.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+
+import android.os.Bundle;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+
+/**
+ * Strategy for storing keys in saved state. Extend this class when using custom
+ * key types that aren't supported by default. Prefer use of builtin storage strategies:
+ * {@link #createStringStorage()}, {@link #createLongStorage()},
+ * {@link #createParcelableStorage(Class)}.
+ *
+ *
+ * See
+ * {@link androidx.recyclerview.selection.SelectionTracker.Builder SelectionTracker.Builder}
+ * for more detailed advice on which key type to use for your selection keys.
+ *
+ * @param Selection key type. Built in support is provided for String, Long, and Parcelable
+ * types. Use the respective factory method to create a StorageStrategy instance
+ * appropriate to the desired type.
+ * {@link #createStringStorage()},
+ * {@link #createParcelableStorage(Class)},
+ * {@link #createLongStorage()}
+ */
+public abstract class StorageStrategy {
+
+ @VisibleForTesting
+ static final String SELECTION_ENTRIES = "androidx.recyclerview.selection.entries";
+
+ @VisibleForTesting
+ static final String SELECTION_KEY_TYPE = "androidx.recyclerview.selection.type";
+
+ private final Class mType;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param type the key type class that is being used.
+ */
+ public StorageStrategy(@NonNull Class type) {
+ checkArgument(type != null);
+ mType = type;
+ }
+
+ /**
+ * Create a {@link Selection} from supplied {@link Bundle}.
+ *
+ * @param state Bundle instance that may contain parceled Selection instance.
+ * @return
+ */
+ public abstract @Nullable Selection asSelection(@NonNull Bundle state);
+
+ /**
+ * Creates a {@link Bundle} from supplied {@link Selection}.
+ *
+ * @param selection The selection to asBundle.
+ * @return
+ */
+ public abstract @NonNull Bundle asBundle(@NonNull Selection selection);
+
+ String getKeyTypeName() {
+ return mType.getCanonicalName();
+ }
+
+ /**
+ * @return StorageStrategy suitable for use with {@link Parcelable} keys
+ * (like {@link android.net.Uri}).
+ */
+ public static StorageStrategy createParcelableStorage(Class type) {
+ return new ParcelableStorageStrategy(type);
+ }
+
+ /**
+ * @return StorageStrategy suitable for use with {@link String} keys.
+ */
+ public static StorageStrategy createStringStorage() {
+ return new StringStorageStrategy();
+ }
+
+ /**
+ * @return StorageStrategy suitable for use with {@link Long} keys.
+ */
+ public static StorageStrategy createLongStorage() {
+ return new LongStorageStrategy();
+ }
+
+ private static class StringStorageStrategy extends StorageStrategy {
+
+ StringStorageStrategy() {
+ super(String.class);
+ }
+
+ @Override
+ public @Nullable Selection asSelection(@NonNull Bundle state) {
+
+ String keyType = state.getString(SELECTION_KEY_TYPE, null);
+ if (keyType == null || !keyType.equals(getKeyTypeName())) {
+ return null;
+ }
+
+ @Nullable ArrayList stored = state.getStringArrayList(SELECTION_ENTRIES);
+ if (stored == null) {
+ return null;
+ }
+
+ Selection selection = new Selection<>();
+ selection.mSelection.addAll(stored);
+ return selection;
+ }
+
+ @Override
+ public @NonNull Bundle asBundle(@NonNull Selection selection) {
+
+ Bundle bundle = new Bundle();
+
+ bundle.putString(SELECTION_KEY_TYPE, getKeyTypeName());
+
+ ArrayList value = new ArrayList<>(selection.size());
+ value.addAll(selection.mSelection);
+ bundle.putStringArrayList(SELECTION_ENTRIES, value);
+
+ return bundle;
+ }
+ }
+
+ private static class LongStorageStrategy extends StorageStrategy {
+
+ LongStorageStrategy() {
+ super(Long.class);
+ }
+
+ @Override
+ public @Nullable Selection asSelection(@NonNull Bundle state) {
+ String keyType = state.getString(SELECTION_KEY_TYPE, null);
+ if (keyType == null || !keyType.equals(getKeyTypeName())) {
+ return null;
+ }
+
+ @Nullable long[] stored = state.getLongArray(SELECTION_ENTRIES);
+ if (stored == null) {
+ return null;
+ }
+
+ Selection selection = new Selection<>();
+ for (long key : stored) {
+ selection.mSelection.add(key);
+ }
+ return selection;
+ }
+
+ @Override
+ public @NonNull Bundle asBundle(@NonNull Selection selection) {
+
+ Bundle bundle = new Bundle();
+ bundle.putString(SELECTION_KEY_TYPE, getKeyTypeName());
+
+ long[] value = new long[selection.size()];
+ int i = 0;
+ for (Long key : selection) {
+ value[i++] = key;
+ }
+ bundle.putLongArray(SELECTION_ENTRIES, value);
+
+ return bundle;
+ }
+ }
+
+ private static class ParcelableStorageStrategy
+ extends StorageStrategy {
+
+ ParcelableStorageStrategy(Class type) {
+ super(type);
+ checkArgument(Parcelable.class.isAssignableFrom(type));
+ }
+
+ @Override
+ public @Nullable Selection asSelection(@NonNull Bundle state) {
+
+ String keyType = state.getString(SELECTION_KEY_TYPE, null);
+ if (keyType == null || !keyType.equals(getKeyTypeName())) {
+ return null;
+ }
+
+ @Nullable ArrayList stored = state.getParcelableArrayList(SELECTION_ENTRIES);
+ if (stored == null) {
+ return null;
+ }
+
+ Selection selection = new Selection<>();
+ selection.mSelection.addAll(stored);
+ return selection;
+ }
+
+ @Override
+ public @NonNull Bundle asBundle(@NonNull Selection selection) {
+
+ Bundle bundle = new Bundle();
+ bundle.putString(SELECTION_KEY_TYPE, getKeyTypeName());
+
+ ArrayList value = new ArrayList<>(selection.size());
+ value.addAll(selection.mSelection);
+ bundle.putParcelableArrayList(SELECTION_ENTRIES, value);
+
+ return bundle;
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/ToolHandlerRegistry.java b/app/src/main/java/androidx/recyclerview/selection/ToolHandlerRegistry.java
new file mode 100644
index 0000000000..8fc82c6297
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/ToolHandlerRegistry.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+import static androidx.core.util.Preconditions.checkState;
+
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Registry for tool specific event handler. This provides map like functionality,
+ * along with fallback to a default handler, while avoiding auto-boxing of tool
+ * type values that would be necessitated where a Map used.
+ *
+ * @param type of item being registered.
+ */
+final class ToolHandlerRegistry {
+
+ // Currently there are four known input types. ERASER is the last one, so has the
+ // highest value. UNKNOWN is zero, so we add one. This allows delegates to be
+ // registered by type, and avoid the auto-boxing that would be necessary were we
+ // to store delegates in a Map.
+ private static final int NUM_INPUT_TYPES = MotionEvent.TOOL_TYPE_ERASER + 1;
+
+ private final List mHandlers = Arrays.asList(null, null, null, null, null);
+ private final T mDefault;
+
+ ToolHandlerRegistry(@NonNull T defaultDelegate) {
+ checkArgument(defaultDelegate != null);
+ mDefault = defaultDelegate;
+
+ // Initialize all values to null.
+ for (int i = 0; i < NUM_INPUT_TYPES; i++) {
+ mHandlers.set(i, null);
+ }
+ }
+
+ /**
+ * @param toolType
+ * @param delegate the delegate, or null to unregister.
+ * @throws IllegalStateException if an tooltype handler is already registered.
+ */
+ void set(int toolType, @Nullable T delegate) {
+ checkArgument(toolType >= 0 && toolType <= MotionEvent.TOOL_TYPE_ERASER);
+ checkState(mHandlers.get(toolType) == null);
+
+ mHandlers.set(toolType, delegate);
+ }
+
+ T get(@NonNull MotionEvent e) {
+ T d = mHandlers.get(e.getToolType(0));
+ return d != null ? d : mDefault;
+ }
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/TouchEventRouter.java b/app/src/main/java/androidx/recyclerview/selection/TouchEventRouter.java
new file mode 100644
index 0000000000..a5501da1d1
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/TouchEventRouter.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
+
+/**
+ * A class responsible for routing MotionEvents to tool-type specific handlers,
+ * and if not handled by a handler, on to a {@link GestureDetector} for further
+ * processing.
+ *
+ *
+ * TouchEventRouter takes its name from
+ * {@link RecyclerView#addOnItemTouchListener(OnItemTouchListener)}. Despite "Touch"
+ * being in the name, it receives MotionEvents for all types of tools.
+ */
+final class TouchEventRouter implements OnItemTouchListener {
+
+ private static final String TAG = "TouchEventRouter";
+
+ private final GestureDetector mDetector;
+ private final ToolHandlerRegistry mDelegates;
+
+ TouchEventRouter(
+ @NonNull GestureDetector detector, @NonNull OnItemTouchListener defaultDelegate) {
+
+ checkArgument(detector != null);
+ checkArgument(defaultDelegate != null);
+
+ mDetector = detector;
+ mDelegates = new ToolHandlerRegistry<>(defaultDelegate);
+ }
+
+ TouchEventRouter(@NonNull GestureDetector detector) {
+ this(
+ detector,
+ // Supply a fallback listener does nothing...because the caller
+ // didn't supply a fallback.
+ new OnItemTouchListener() {
+ @Override
+ public boolean onInterceptTouchEvent(
+ @NonNull RecyclerView unused, @NonNull MotionEvent e) {
+
+ return false;
+ }
+
+ @Override
+ public void onTouchEvent(
+ @NonNull RecyclerView unused, @NonNull MotionEvent e) {
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ }
+ });
+ }
+
+ /**
+ * @param toolType See MotionEvent for details on available types.
+ * @param delegate An {@link OnItemTouchListener} to receive events
+ * of {@code toolType}.
+ */
+ void register(int toolType, @NonNull OnItemTouchListener delegate) {
+ checkArgument(delegate != null);
+ mDelegates.set(toolType, delegate);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
+ boolean handled = mDelegates.get(e).onInterceptTouchEvent(rv, e);
+
+ // Forward all events to UserInputHandler.
+ // This is necessary since UserInputHandler needs to always see the first DOWN event. Or
+ // else all future UP events will be tossed.
+ handled |= mDetector.onTouchEvent(e);
+
+ return handled;
+ }
+
+ @Override
+ public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
+ mDelegates.get(e).onTouchEvent(rv, e);
+
+ // Note: even though this event is being handled as part of gestures such as drag and band,
+ // continue forwarding to the GestureDetector. The detector needs to see the entire cluster
+ // of events in order to properly interpret other gestures, such as long press.
+ mDetector.onTouchEvent(e);
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
+}
diff --git a/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java b/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java
new file mode 100644
index 0000000000..271ff3cc40
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+
+import android.util.Log;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * A MotionInputHandler that provides the high-level glue for touch driven selection. This class
+ * works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper} to
+ * to implement the primary policies around touch input.
+ *
+ * @param Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+final class TouchInputHandler extends MotionInputHandler {
+
+ private static final String TAG = "TouchInputDelegate";
+ private static final boolean DEBUG = false;
+
+ private final ItemDetailsLookup mDetailsLookup;
+ private final SelectionPredicate mSelectionPredicate;
+ private final OnItemActivatedListener mOnItemActivatedListener;
+ private final OnDragInitiatedListener mOnDragInitiatedListener;
+ private final Runnable mGestureStarter;
+ private final Runnable mHapticPerformer;
+
+ TouchInputHandler(
+ @NonNull SelectionTracker selectionTracker,
+ @NonNull ItemKeyProvider keyProvider,
+ @NonNull ItemDetailsLookup detailsLookup,
+ @NonNull SelectionPredicate selectionPredicate,
+ @NonNull Runnable gestureStarter,
+ @NonNull OnDragInitiatedListener onDragInitiatedListener,
+ @NonNull OnItemActivatedListener