mirror of https://github.com/M66B/FairEmail.git
parent
e6c4e05e9d
commit
c761642ef2
@ -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);
|
||||
}
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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 <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
class BandSelectionHelper<K> implements OnItemTouchListener {
|
||||
|
||||
static final String TAG = "BandSelectionHelper";
|
||||
static final boolean DEBUG = false;
|
||||
|
||||
private final BandHost mHost;
|
||||
private final ItemKeyProvider<K> mKeyProvider;
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
final SelectionTracker<K> mSelectionTracker;
|
||||
private final BandPredicate mBandPredicate;
|
||||
private final FocusDelegate<K> 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<K> keyProvider,
|
||||
@NonNull SelectionTracker<K> selectionTracker,
|
||||
@NonNull BandPredicate bandPredicate,
|
||||
@NonNull FocusDelegate<K> 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<K>() {
|
||||
@Override
|
||||
public void onSelectionChanged(Set<K> updatedSelection) {
|
||||
mSelectionTracker.setProvisionalSelection(updatedSelection);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @return new BandSelectionHelper instance.
|
||||
*/
|
||||
static <K> BandSelectionHelper create(
|
||||
@NonNull RecyclerView recyclerView,
|
||||
@NonNull AutoScroller scroller,
|
||||
@DrawableRes int bandOverlayId,
|
||||
@NonNull ItemKeyProvider<K> keyProvider,
|
||||
@NonNull SelectionTracker<K> selectionTracker,
|
||||
@NonNull SelectionPredicate<K> selectionPredicate,
|
||||
@NonNull BandPredicate bandPredicate,
|
||||
@NonNull FocusDelegate<K> 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 <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
abstract static class BandHost<K> {
|
||||
|
||||
/**
|
||||
* Returns a new GridModel instance.
|
||||
*/
|
||||
abstract GridModel<K> 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);
|
||||
}
|
||||
}
|
@ -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<K> extends GridModel.GridHost<K> {
|
||||
|
||||
private static final Rect NILL_RECT = new Rect(0, 0, 0, 0);
|
||||
|
||||
private final RecyclerView mRecyclerView;
|
||||
private final Drawable mBand;
|
||||
private final ItemKeyProvider<K> mKeyProvider;
|
||||
private final SelectionPredicate<K> mSelectionPredicate;
|
||||
|
||||
DefaultBandHost(
|
||||
@NonNull RecyclerView recyclerView,
|
||||
@DrawableRes int bandOverlayId,
|
||||
@NonNull ItemKeyProvider<K> keyProvider,
|
||||
@NonNull SelectionPredicate<K> 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<K> 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;
|
||||
}
|
||||
}
|
@ -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}.
|
||||
*
|
||||
* <p>
|
||||
* The class supports running in a single-select mode, which can be enabled using
|
||||
* {@link SelectionPredicate#canSelectMultiple()}.
|
||||
*
|
||||
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(LIBRARY_GROUP)
|
||||
public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
|
||||
|
||||
private static final String TAG = "DefaultSelectionTracker";
|
||||
private static final String EXTRA_SELECTION_PREFIX = "androidx.recyclerview.selection";
|
||||
|
||||
private final Selection<K> mSelection = new Selection<>();
|
||||
private final List<SelectionObserver> mObservers = new ArrayList<>(1);
|
||||
private final ItemKeyProvider<K> mKeyProvider;
|
||||
private final SelectionPredicate<K> mSelectionPredicate;
|
||||
private final StorageStrategy<K> 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<K> 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<K> keys, boolean selected) {
|
||||
boolean changed = setItemsSelectedQuietly(keys, selected);
|
||||
notifySelectionChanged();
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean setItemsSelectedQuietly(@NonNull Iterable<K> 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<K> newSelection) {
|
||||
if (mSingleSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<K, Boolean> delta = mSelection.setProvisionalSelection(newSelection);
|
||||
for (Map.Entry<K, Boolean> 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<K> 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<K> 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<K> 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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 <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
public static <K> void install(
|
||||
@NonNull RecyclerView.Adapter<?> adapter,
|
||||
@NonNull SelectionTracker<K> selectionTracker,
|
||||
@NonNull ItemKeyProvider<K> keyProvider) {
|
||||
|
||||
// setup bridges to relay selection and adapter events
|
||||
new TrackerToAdapterBridge<>(selectionTracker, keyProvider, adapter);
|
||||
adapter.registerAdapterDataObserver(selectionTracker.getAdapterDataObserver());
|
||||
}
|
||||
|
||||
private static final class TrackerToAdapterBridge<K>
|
||||
extends SelectionTracker.SelectionObserver<K> {
|
||||
|
||||
private final ItemKeyProvider<K> mKeyProvider;
|
||||
private final RecyclerView.Adapter<?> mAdapter;
|
||||
|
||||
TrackerToAdapterBridge(
|
||||
@NonNull SelectionTracker<K> selectionTracker,
|
||||
@NonNull ItemKeyProvider<K> 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() {
|
||||
}
|
||||
}
|
@ -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 <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
public abstract class FocusDelegate<K> {
|
||||
|
||||
static <K> FocusDelegate<K> dummy() {
|
||||
return new FocusDelegate<K>() {
|
||||
@Override
|
||||
public void focusItem(@NonNull ItemDetails<K> 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<K> 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();
|
||||
}
|
@ -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 <T> listener type. Must extend OnGestureListener & OnDoubleTapListener.
|
||||
*/
|
||||
final class GestureRouter<T extends OnGestureListener & OnDoubleTapListener>
|
||||
implements OnGestureListener, OnDoubleTapListener {
|
||||
|
||||
private final ToolHandlerRegistry<T> 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);
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
* <p>This code, and the fact that this method is used by both OnInterceptTouchEvent and
|
||||
* OnTouchEvent, is correct and valid because:
|
||||
* <ol>
|
||||
* <li>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.
|
||||
* <li>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.
|
||||
* </ol>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
final class GridModel<K> {
|
||||
|
||||
// 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<K> mHost;
|
||||
private final ItemKeyProvider<K> mKeyProvider;
|
||||
private final SelectionPredicate<K> mSelectionPredicate;
|
||||
|
||||
private final List<SelectionObserver> 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<SparseIntArray> mColumns = new SparseArray<>();
|
||||
|
||||
// List of limits along the x-axis (columns).
|
||||
// This list is sorted from furthest left to furthest right.
|
||||
private final List<Limits> 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<Limits> 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<K> 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<K> keyProvider,
|
||||
SelectionPredicate<K> 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<Limits> 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<K> {
|
||||
abstract void onSelectionChanged(Set<K> 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<Limits> {
|
||||
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<RelativeCoordinate> {
|
||||
/**
|
||||
* 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<Limits> 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<Limits> columnLimits,
|
||||
@NonNull List<Limits> 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<Limits> 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 <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
abstract static class GridHost<K> extends BandSelectionHelper.BandHost<K> {
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* <b>Example</b>
|
||||
* <pre>
|
||||
* final class MyDetailsLookup extends ItemDetailsLookup<Uri> {
|
||||
*
|
||||
* private final RecyclerView mRecyclerView;
|
||||
*
|
||||
* MyDetailsLookup(RecyclerView recyclerView) {
|
||||
* mRecyclerView = recyclerView;
|
||||
* }
|
||||
*
|
||||
* public ItemDetails<Uri> 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;
|
||||
* }
|
||||
*}
|
||||
* </pre>
|
||||
*
|
||||
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
public abstract class ItemDetailsLookup<K> {
|
||||
|
||||
/**
|
||||
* @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<K> 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.
|
||||
*
|
||||
* <p>
|
||||
* <b>Selection Hotspot</b>
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* See {@link OnItemActivatedListener} for details on handling item activation.
|
||||
*
|
||||
* <p>
|
||||
* <b>Drag Region</b>
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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":
|
||||
* <pre>
|
||||
* -------------------------------------------------------
|
||||
* | [icon] A string label. ...empty space... |
|
||||
* -------------------------------------------------------
|
||||
* < --- drag region --> < --treated as background-->
|
||||
*</pre>
|
||||
*
|
||||
* <p>
|
||||
* Further more, within a drag region, a mouse click and drag will immediately
|
||||
* initiate drag and drop (if supported by your configuration).
|
||||
*
|
||||
* <p>
|
||||
* As user expectations around touch and mouse input differ substantially,
|
||||
* "drag region" has no effect on handling of touch input.
|
||||
*
|
||||
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
public abstract static class ItemDetails<K> {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
public abstract class ItemKeyProvider<K> {
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<K> extends SimpleOnGestureListener {
|
||||
|
||||
protected final SelectionTracker<K> mSelectionTracker;
|
||||
|
||||
private final ItemKeyProvider<K> mKeyProvider;
|
||||
private final FocusDelegate<K> mFocusDelegate;
|
||||
|
||||
MotionInputHandler(
|
||||
@NonNull SelectionTracker<K> selectionTracker,
|
||||
@NonNull ItemKeyProvider<K> keyProvider,
|
||||
@NonNull FocusDelegate<K> focusDelegate) {
|
||||
|
||||
checkArgument(selectionTracker != null);
|
||||
checkArgument(keyProvider != null);
|
||||
checkArgument(focusDelegate != null);
|
||||
|
||||
mSelectionTracker = selectionTracker;
|
||||
mKeyProvider = keyProvider;
|
||||
mFocusDelegate = focusDelegate;
|
||||
}
|
||||
|
||||
final boolean selectItem(@NonNull ItemDetails<K> 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<K> details) {
|
||||
checkArgument(details != null);
|
||||
checkArgument(hasSelectionKey(details));
|
||||
|
||||
mSelectionTracker.clearSelection();
|
||||
mFocusDelegate.focusItem(details);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected final void extendSelectionRange(@NonNull ItemDetails<K> 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<K> 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;
|
||||
}
|
||||
}
|
@ -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<K> extends MotionInputHandler<K> {
|
||||
|
||||
private static final String TAG = "MouseInputDelegate";
|
||||
|
||||
private final ItemDetailsLookup<K> mDetailsLookup;
|
||||
private final OnContextClickListener mOnContextClickListener;
|
||||
private final OnItemActivatedListener<K> mOnItemActivatedListener;
|
||||
private final FocusDelegate<K> 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<K> selectionTracker,
|
||||
@NonNull ItemKeyProvider<K> keyProvider,
|
||||
@NonNull ItemDetailsLookup<K> detailsLookup,
|
||||
@NonNull OnContextClickListener onContextClickListener,
|
||||
@NonNull OnItemActivatedListener<K> onItemActivatedListener,
|
||||
@NonNull FocusDelegate<K> 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<K> 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<K> 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<K> item = mDetailsLookup.getItemDetails(e);
|
||||
return (item != null) && mOnItemActivatedListener.onItemActivated(item, e);
|
||||
}
|
||||
|
||||
private boolean onRightClick(@NonNull MotionEvent e) {
|
||||
if (mDetailsLookup.overItemWithSelectionKey(e)) {
|
||||
@Nullable ItemDetails<K> 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<K> item, @NonNull MotionEvent e) {
|
||||
if (item.inSelectionHotspot(e) || MotionEvents.isCtrlKeyPressed(e)) {
|
||||
selectItem(item);
|
||||
} else {
|
||||
focusItem(item);
|
||||
}
|
||||
}
|
||||
}
|
@ -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)}.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* {@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.
|
||||
*
|
||||
*
|
||||
* <p><b>Example</b>
|
||||
*
|
||||
* <p>
|
||||
* <pre>
|
||||
* 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.
|
||||
* </pre>
|
||||
*
|
||||
* @see android.text.Selection
|
||||
*
|
||||
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
public final class MutableSelection<K> extends Selection<K> {
|
||||
|
||||
@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<K> source) {
|
||||
super.copyFrom(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
super.clear();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* The following circumstances are considered to be expressing drag and drop intent:
|
||||
*
|
||||
* <ol>
|
||||
* <li>Long press on selected item.</li>
|
||||
* <li>Click and drag in the {@link ItemDetails#inDragRegion(MotionEvent) drag region}
|
||||
* of selected item with a pointer device.</li>
|
||||
* <li>Click and drag in drag region of un-selected item with a pointer device.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>
|
||||
* 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.)
|
||||
*
|
||||
* <p>
|
||||
* 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)}.
|
||||
*
|
||||
* <p>
|
||||
* Drag region used with pointer devices is specified by
|
||||
* {@link ItemDetails#inDragRegion(MotionEvent)}
|
||||
*
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
@ -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 <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
public interface OnItemActivatedListener<K> {
|
||||
|
||||
/**
|
||||
* 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<K> item, @NonNull MotionEvent e);
|
||||
}
|
@ -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).
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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<OnChangeListener> 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();
|
||||
}
|
||||
}
|
@ -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}.
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* This class tracks selected items by managing two sets:
|
||||
*
|
||||
* <p>
|
||||
* <b>Primary Selection</b>
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* <b>Provisional Selection</b>
|
||||
*
|
||||
* <p>
|
||||
* Provisional selections are selections which are interim in nature.
|
||||
*
|
||||
* <p>
|
||||
* 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 <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
public class Selection<K> implements Iterable<K> {
|
||||
|
||||
// NOTE: Not currently private as DefaultSelectionTracker directly manipulates values.
|
||||
final Set<K> mSelection;
|
||||
final Set<K> mProvisionalSelection;
|
||||
|
||||
Selection() {
|
||||
mSelection = new HashSet<>();
|
||||
mProvisionalSelection = new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by {@link StorageStrategy} when restoring selection.
|
||||
*/
|
||||
Selection(@NonNull Set<K> 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<K> 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<K, Boolean> setProvisionalSelection(@NonNull Set<K> newSelection) {
|
||||
Map<K, Boolean> 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<K, Boolean> 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<K> 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);
|
||||
}
|
||||
}
|
@ -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 <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
* @return
|
||||
*/
|
||||
public static <K> SelectionPredicate<K> createSelectAnything() {
|
||||
return new SelectionPredicate<K>() {
|
||||
@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 <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
* @return
|
||||
*/
|
||||
public static <K> SelectionPredicate<K> createSelectSingleAnything() {
|
||||
return new SelectionPredicate<K>() {
|
||||
@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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* Create an instance of SelectionTracker using {@link Builder SelectionTracker.Builder}.
|
||||
*
|
||||
* <p>
|
||||
* <b>Inspecting the current selection</b>
|
||||
*
|
||||
* <p>
|
||||
* The underlying selection is described by the {@link Selection} class.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* To obtain a stable snapshot of the selection use {@link #copySelection(MutableSelection)}.
|
||||
*
|
||||
* <p>
|
||||
* Selection state for an individual item can be obtained using {@link #isSelected(Object)}.
|
||||
*
|
||||
* <p>
|
||||
* <b>Provisional Selection</b>
|
||||
*
|
||||
* <p>
|
||||
* 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).
|
||||
*
|
||||
* <p>
|
||||
* A provisional selection can be abandoned, or merged into the primary selection.
|
||||
*
|
||||
* <p>
|
||||
* <b>Enforcing selection policies</b>
|
||||
*
|
||||
* <p>
|
||||
* 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 <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
public abstract class SelectionTracker<K> {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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<K> getSelection();
|
||||
|
||||
/**
|
||||
* Updates {@code dest} to reflect the current selection.
|
||||
*/
|
||||
public abstract void copySelection(@NonNull MutableSelection<K> 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.
|
||||
*
|
||||
* <p>
|
||||
* 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<K> 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<K> 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.
|
||||
*
|
||||
* <p>
|
||||
* 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<K> 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 <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
public abstract static class SelectionObserver<K> {
|
||||
|
||||
/**
|
||||
* 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 <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
public abstract static class SelectionPredicate<K> {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* Example usage:
|
||||
* <pre>SelectionTracker<Uri> tracker = new SelectionTracker.Builder<>(
|
||||
* "my-uri-selection",
|
||||
* recyclerView,
|
||||
* new DemoStableIdProvider(recyclerView.getAdapter()),
|
||||
* new MyDetailsLookup(recyclerView),
|
||||
* StorageStrategy.createParcelableStorage(Uri.class))
|
||||
* .build();
|
||||
*</pre>
|
||||
*
|
||||
* <p>
|
||||
* <b>Restricting which items can be selected and limiting selection size</b>
|
||||
*
|
||||
* <p>
|
||||
* {@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.
|
||||
*
|
||||
* <p>Configuring the tracker for single single selection support can be done
|
||||
* by supplying {@link SelectionPredicates#createSelectSingleAnything()}.
|
||||
*
|
||||
* SelectionTracker<String> tracker = new SelectionTracker.Builder<>(
|
||||
* "my-string-selection",
|
||||
* recyclerView,
|
||||
* new DemoStableIdProvider(recyclerView.getAdapter()),
|
||||
* new MyDetailsLookup(recyclerView),
|
||||
* StorageStrategy.createStringStorage())
|
||||
* .withSelectionPredicate(SelectionPredicates#createSelectSingleAnything())
|
||||
* .build();
|
||||
*</pre>
|
||||
* <p>
|
||||
* <b>Retaining state across Android lifecycle events</b>
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* <b>Key Type</b>
|
||||
*
|
||||
* <p>
|
||||
* 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}.
|
||||
*
|
||||
* <p>
|
||||
* {@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.
|
||||
*
|
||||
* <p>
|
||||
* {@link String}: Use String when a string based stable identifier is available.
|
||||
*
|
||||
* <p>
|
||||
* {@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.
|
||||
*
|
||||
* <p>
|
||||
* Usage:
|
||||
*
|
||||
* <pre>
|
||||
* private SelectionTracker<Uri> 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);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @param <K> 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<K> {
|
||||
|
||||
final RecyclerView mRecyclerView;
|
||||
private final RecyclerView.Adapter<?> mAdapter;
|
||||
private final Context mContext;
|
||||
private final String mSelectionId;
|
||||
private final StorageStrategy<K> mStorage;
|
||||
|
||||
SelectionPredicate<K> mSelectionPredicate =
|
||||
SelectionPredicates.createSelectAnything();
|
||||
private OperationMonitor mMonitor = new OperationMonitor();
|
||||
private ItemKeyProvider<K> mKeyProvider;
|
||||
private ItemDetailsLookup<K> mDetailsLookup;
|
||||
|
||||
private FocusDelegate<K> mFocusDelegate = FocusDelegate.dummy();
|
||||
|
||||
private OnItemActivatedListener<K> 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<K> keyProvider,
|
||||
@NonNull ItemDetailsLookup<K> detailsLookup,
|
||||
@NonNull StorageStrategy<K> 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<K> withSelectionPredicate(
|
||||
@NonNull SelectionPredicate<K> 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<K> 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<K> withFocusDelegate(@NonNull FocusDelegate<K> 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<K> withOnItemActivatedListener(
|
||||
@NonNull OnItemActivatedListener<K> 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<K> 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<K> 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<K> withGestureTooltypes(int... toolTypes) {
|
||||
mGestureToolTypes = toolTypes;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces default band overlay.
|
||||
*
|
||||
* @param bandOverlayId
|
||||
* @return this
|
||||
*/
|
||||
public Builder<K> withBandOverlay(@DrawableRes int bandOverlayId) {
|
||||
mBandOverlayId = bandOverlayId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces default band predicate.
|
||||
* @param bandPredicate
|
||||
* @return this
|
||||
*/
|
||||
public Builder<K> 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<K> withPointerTooltypes(int... toolTypes) {
|
||||
mPointerToolTypes = toolTypes;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares and returns a SelectionTracker.
|
||||
*
|
||||
* @return this
|
||||
*/
|
||||
public SelectionTracker<K> build() {
|
||||
|
||||
SelectionTracker<K> 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<MotionInputHandler> 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<K>() {
|
||||
@Override
|
||||
public boolean onItemActivated(
|
||||
@NonNull ItemDetailsLookup.ItemDetails<K> 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<K> touchHandler = new TouchInputHandler<K>(
|
||||
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<K> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {}
|
||||
}
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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<Long> {
|
||||
|
||||
private final SparseArray<Long> mPositionToKey = new SparseArray<>();
|
||||
private final Map<Long, Integer> mKeyToPosition = new HashMap<Long, Integer>();
|
||||
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;
|
||||
}
|
||||
}
|
@ -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)}.
|
||||
*
|
||||
* <p>
|
||||
* See
|
||||
* {@link androidx.recyclerview.selection.SelectionTracker.Builder SelectionTracker.Builder}
|
||||
* for more detailed advice on which key type to use for your selection keys.
|
||||
*
|
||||
* @param <K> 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<K> {
|
||||
|
||||
@VisibleForTesting
|
||||
static final String SELECTION_ENTRIES = "androidx.recyclerview.selection.entries";
|
||||
|
||||
@VisibleForTesting
|
||||
static final String SELECTION_KEY_TYPE = "androidx.recyclerview.selection.type";
|
||||
|
||||
private final Class<K> mType;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param type the key type class that is being used.
|
||||
*/
|
||||
public StorageStrategy(@NonNull Class<K> 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<K> 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<K> selection);
|
||||
|
||||
String getKeyTypeName() {
|
||||
return mType.getCanonicalName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return StorageStrategy suitable for use with {@link Parcelable} keys
|
||||
* (like {@link android.net.Uri}).
|
||||
*/
|
||||
public static <K extends Parcelable> StorageStrategy<K> createParcelableStorage(Class<K> type) {
|
||||
return new ParcelableStorageStrategy(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return StorageStrategy suitable for use with {@link String} keys.
|
||||
*/
|
||||
public static StorageStrategy<String> createStringStorage() {
|
||||
return new StringStorageStrategy();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return StorageStrategy suitable for use with {@link Long} keys.
|
||||
*/
|
||||
public static StorageStrategy<Long> createLongStorage() {
|
||||
return new LongStorageStrategy();
|
||||
}
|
||||
|
||||
private static class StringStorageStrategy extends StorageStrategy<String> {
|
||||
|
||||
StringStorageStrategy() {
|
||||
super(String.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Selection<String> asSelection(@NonNull Bundle state) {
|
||||
|
||||
String keyType = state.getString(SELECTION_KEY_TYPE, null);
|
||||
if (keyType == null || !keyType.equals(getKeyTypeName())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable ArrayList<String> stored = state.getStringArrayList(SELECTION_ENTRIES);
|
||||
if (stored == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Selection<String> selection = new Selection<>();
|
||||
selection.mSelection.addAll(stored);
|
||||
return selection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Bundle asBundle(@NonNull Selection<String> selection) {
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
|
||||
bundle.putString(SELECTION_KEY_TYPE, getKeyTypeName());
|
||||
|
||||
ArrayList<String> value = new ArrayList<>(selection.size());
|
||||
value.addAll(selection.mSelection);
|
||||
bundle.putStringArrayList(SELECTION_ENTRIES, value);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
}
|
||||
|
||||
private static class LongStorageStrategy extends StorageStrategy<Long> {
|
||||
|
||||
LongStorageStrategy() {
|
||||
super(Long.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Selection<Long> 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<Long> selection = new Selection<>();
|
||||
for (long key : stored) {
|
||||
selection.mSelection.add(key);
|
||||
}
|
||||
return selection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Bundle asBundle(@NonNull Selection<Long> 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<K extends Parcelable>
|
||||
extends StorageStrategy<K> {
|
||||
|
||||
ParcelableStorageStrategy(Class<K> type) {
|
||||
super(type);
|
||||
checkArgument(Parcelable.class.isAssignableFrom(type));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Selection<K> asSelection(@NonNull Bundle state) {
|
||||
|
||||
String keyType = state.getString(SELECTION_KEY_TYPE, null);
|
||||
if (keyType == null || !keyType.equals(getKeyTypeName())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable ArrayList<K> stored = state.getParcelableArrayList(SELECTION_ENTRIES);
|
||||
if (stored == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Selection<K> selection = new Selection<>();
|
||||
selection.mSelection.addAll(stored);
|
||||
return selection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Bundle asBundle(@NonNull Selection<K> selection) {
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(SELECTION_KEY_TYPE, getKeyTypeName());
|
||||
|
||||
ArrayList<K> value = new ArrayList<>(selection.size());
|
||||
value.addAll(selection.mSelection);
|
||||
bundle.putParcelableArrayList(SELECTION_ENTRIES, value);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 <T> type of item being registered.
|
||||
*/
|
||||
final class ToolHandlerRegistry<T> {
|
||||
|
||||
// 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<Integer, Delegate>.
|
||||
private static final int NUM_INPUT_TYPES = MotionEvent.TOOL_TYPE_ERASER + 1;
|
||||
|
||||
private final List<T> 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;
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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<OnItemTouchListener> 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) {}
|
||||
}
|
@ -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 <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
final class TouchInputHandler<K> extends MotionInputHandler<K> {
|
||||
|
||||
private static final String TAG = "TouchInputDelegate";
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private final ItemDetailsLookup<K> mDetailsLookup;
|
||||
private final SelectionPredicate<K> mSelectionPredicate;
|
||||
private final OnItemActivatedListener<K> mOnItemActivatedListener;
|
||||
private final OnDragInitiatedListener mOnDragInitiatedListener;
|
||||
private final Runnable mGestureStarter;
|
||||
private final Runnable mHapticPerformer;
|
||||
|
||||
TouchInputHandler(
|
||||
@NonNull SelectionTracker<K> selectionTracker,
|
||||
@NonNull ItemKeyProvider<K> keyProvider,
|
||||
@NonNull ItemDetailsLookup<K> detailsLookup,
|
||||
@NonNull SelectionPredicate<K> selectionPredicate,
|
||||
@NonNull Runnable gestureStarter,
|
||||
@NonNull OnDragInitiatedListener onDragInitiatedListener,
|
||||
@NonNull OnItemActivatedListener<K> onItemActivatedListener,
|
||||
@NonNull FocusDelegate<K> focusDelegate,
|
||||
@NonNull Runnable hapticPerformer) {
|
||||
|
||||
super(selectionTracker, keyProvider, focusDelegate);
|
||||
|
||||
checkArgument(detailsLookup != null);
|
||||
checkArgument(selectionPredicate != null);
|
||||
checkArgument(gestureStarter != null);
|
||||
checkArgument(onItemActivatedListener != null);
|
||||
checkArgument(onDragInitiatedListener != null);
|
||||
checkArgument(hapticPerformer != null);
|
||||
|
||||
mDetailsLookup = detailsLookup;
|
||||
mSelectionPredicate = selectionPredicate;
|
||||
mGestureStarter = gestureStarter;
|
||||
mOnItemActivatedListener = onItemActivatedListener;
|
||||
mOnDragInitiatedListener = onDragInitiatedListener;
|
||||
mHapticPerformer = hapticPerformer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(@NonNull MotionEvent e) {
|
||||
if (!mDetailsLookup.overItemWithSelectionKey(e)) {
|
||||
if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection.");
|
||||
mSelectionTracker.clearSelection();
|
||||
return false;
|
||||
}
|
||||
|
||||
ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
|
||||
// Should really not be null at this point, but...
|
||||
if (item == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mSelectionTracker.hasSelection()) {
|
||||
if (isRangeExtension(e)) {
|
||||
extendSelectionRange(item);
|
||||
} else if (mSelectionTracker.isSelected(item.getSelectionKey())) {
|
||||
mSelectionTracker.deselect(item.getSelectionKey());
|
||||
} else {
|
||||
selectItem(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Touch events select if they occur in the selection hotspot,
|
||||
// otherwise they activate.
|
||||
return item.inSelectionHotspot(e)
|
||||
? selectItem(item)
|
||||
: mOnItemActivatedListener.onItemActivated(item, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(@NonNull MotionEvent e) {
|
||||
if (!mDetailsLookup.overItemWithSelectionKey(e)) {
|
||||
if (DEBUG) Log.d(TAG, "Ignoring LongPress on non-model-backed item.");
|
||||
return;
|
||||
}
|
||||
|
||||
ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
|
||||
// Should really not be null at this point, but...
|
||||
if (item == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean handled = false;
|
||||
|
||||
if (isRangeExtension(e)) {
|
||||
extendSelectionRange(item);
|
||||
handled = true;
|
||||
} else {
|
||||
if (!mSelectionTracker.isSelected(item.getSelectionKey())
|
||||
&& mSelectionPredicate.canSetStateForKey(item.getSelectionKey(), true)) {
|
||||
// If we cannot select it, we didn't apply anchoring - therefore should not
|
||||
// start gesture selection
|
||||
if (selectItem(item)) {
|
||||
// And finally if the item was selected && we can select multiple
|
||||
// we kick off gesture selection.
|
||||
if (mSelectionPredicate.canSelectMultiple()) {
|
||||
mGestureStarter.run();
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
} else {
|
||||
// We only initiate drag and drop on long press for touch to allow regular
|
||||
// touch-based scrolling
|
||||
mOnDragInitiatedListener.onDragInitiated(e);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
mHapticPerformer.run();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,273 @@
|
||||
/*
|
||||
* 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.graphics.Point;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* Provides auto-scrolling upon request when user's interaction with the application
|
||||
* introduces a natural intent to scroll. Used by BandSelectionHelper and GestureSelectionHelper,
|
||||
* to provide auto scrolling when user is performing selection operations.
|
||||
*/
|
||||
final class ViewAutoScroller extends AutoScroller {
|
||||
|
||||
private static final String TAG = "ViewAutoScroller";
|
||||
|
||||
// ratio used to calculate the top/bottom hotspot region; used with view height
|
||||
private static final float DEFAULT_SCROLL_THRESHOLD_RATIO = 0.125f;
|
||||
private static final int MAX_SCROLL_STEP = 70;
|
||||
|
||||
private final float mScrollThresholdRatio;
|
||||
|
||||
private final ScrollHost mHost;
|
||||
private final Runnable mRunner;
|
||||
|
||||
private @Nullable Point mOrigin;
|
||||
private @Nullable Point mLastLocation;
|
||||
private boolean mPassedInitialMotionThreshold;
|
||||
|
||||
ViewAutoScroller(@NonNull ScrollHost scrollHost) {
|
||||
this(scrollHost, DEFAULT_SCROLL_THRESHOLD_RATIO);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ViewAutoScroller(@NonNull ScrollHost scrollHost, float scrollThresholdRatio) {
|
||||
|
||||
checkArgument(scrollHost != null);
|
||||
|
||||
mHost = scrollHost;
|
||||
mScrollThresholdRatio = scrollThresholdRatio;
|
||||
|
||||
mRunner = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
runScroll();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
mHost.removeCallback(mRunner);
|
||||
mOrigin = null;
|
||||
mLastLocation = null;
|
||||
mPassedInitialMotionThreshold = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scroll(@NonNull Point location) {
|
||||
mLastLocation = location;
|
||||
|
||||
// See #aboveMotionThreshold for details on how we track initial location.
|
||||
if (mOrigin == null) {
|
||||
mOrigin = location;
|
||||
if (VERBOSE) Log.v(TAG, "Origin @ " + mOrigin);
|
||||
}
|
||||
|
||||
if (VERBOSE) Log.v(TAG, "Current location @ " + mLastLocation);
|
||||
|
||||
mHost.runAtNextFrame(mRunner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to smooth-scroll the view at the given UI frame. Application should be
|
||||
* responsible to do any clean up (such as unsubscribing scrollListeners) after the run has
|
||||
* finished, and re-run this method on the next UI frame if applicable.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
void runScroll() {
|
||||
if (DEBUG) checkState(mLastLocation != null);
|
||||
|
||||
if (VERBOSE) Log.v(TAG, "Running in background using event location @ " + mLastLocation);
|
||||
|
||||
// Compute the number of pixels the pointer's y-coordinate is past the view.
|
||||
// Negative values mean the pointer is at or before the top of the view, and
|
||||
// positive values mean that the pointer is at or after the bottom of the view. Note
|
||||
// that top/bottom threshold is added here so that the view still scrolls when the
|
||||
// pointer are in these buffer pixels.
|
||||
int pixelsPastView = 0;
|
||||
|
||||
final int verticalThreshold = (int) (mHost.getViewHeight()
|
||||
* mScrollThresholdRatio);
|
||||
|
||||
if (mLastLocation.y <= verticalThreshold) {
|
||||
pixelsPastView = mLastLocation.y - verticalThreshold;
|
||||
} else if (mLastLocation.y >= mHost.getViewHeight()
|
||||
- verticalThreshold) {
|
||||
pixelsPastView = mLastLocation.y - mHost.getViewHeight()
|
||||
+ verticalThreshold;
|
||||
}
|
||||
|
||||
if (pixelsPastView == 0) {
|
||||
// If the operation that started the scrolling is no longer inactive, or if it is active
|
||||
// but not at the edge of the view, no scrolling is necessary.
|
||||
return;
|
||||
}
|
||||
|
||||
// We're in one of the endzones. Now determine if there's enough of a difference
|
||||
// from the orgin to take any action. Basically if a user has somehow initiated
|
||||
// selection, but is hovering at or near their initial contact point, we don't
|
||||
// scroll. This avoids a situation where the user initiates selection in an "endzone"
|
||||
// only to have scrolling start automatically.
|
||||
if (!mPassedInitialMotionThreshold && !aboveMotionThreshold(mLastLocation)) {
|
||||
if (VERBOSE) Log.v(TAG, "Ignoring event below motion threshold.");
|
||||
return;
|
||||
}
|
||||
mPassedInitialMotionThreshold = true;
|
||||
|
||||
if (pixelsPastView > verticalThreshold) {
|
||||
pixelsPastView = verticalThreshold;
|
||||
}
|
||||
|
||||
// Compute the number of pixels to scroll, and scroll that many pixels.
|
||||
final int numPixels = computeScrollDistance(pixelsPastView);
|
||||
mHost.scrollBy(numPixels);
|
||||
|
||||
// Replace any existing scheduled jobs with the latest and greatest..
|
||||
mHost.removeCallback(mRunner);
|
||||
mHost.runAtNextFrame(mRunner);
|
||||
}
|
||||
|
||||
private boolean aboveMotionThreshold(@NonNull Point location) {
|
||||
// We reuse the scroll threshold to calculate a much smaller area
|
||||
// in which we ignore motion initially.
|
||||
int motionThreshold =
|
||||
(int) ((mHost.getViewHeight() * mScrollThresholdRatio)
|
||||
* (mScrollThresholdRatio * 2));
|
||||
return Math.abs(mOrigin.y - location.y) >= motionThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the number of pixels to scroll based on how far the pointer is past the end
|
||||
* of the region. Roughly based on ItemTouchHelper's algorithm for computing the number of
|
||||
* pixels to scroll when an item is dragged to the end of a view.
|
||||
* @return
|
||||
*/
|
||||
@VisibleForTesting
|
||||
int computeScrollDistance(int pixelsPastView) {
|
||||
final int topBottomThreshold =
|
||||
(int) (mHost.getViewHeight() * mScrollThresholdRatio);
|
||||
|
||||
final int direction = (int) Math.signum(pixelsPastView);
|
||||
final int absPastView = Math.abs(pixelsPastView);
|
||||
|
||||
// Calculate the ratio of how far out of the view the pointer currently resides to
|
||||
// the top/bottom scrolling hotspot of the view.
|
||||
final float outOfBoundsRatio = Math.min(
|
||||
1.0f, (float) absPastView / topBottomThreshold);
|
||||
// Interpolate this ratio and use it to compute the maximum scroll that should be
|
||||
// possible for this step.
|
||||
final int cappedScrollStep =
|
||||
(int) (direction * MAX_SCROLL_STEP * smoothOutOfBoundsRatio(outOfBoundsRatio));
|
||||
|
||||
// If the final number of pixels to scroll ends up being 0, the view should still
|
||||
// scroll at least one pixel.
|
||||
return cappedScrollStep != 0 ? cappedScrollStep : direction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends
|
||||
* at (1,1) and quickly approaches 1 near the start of that interval. This ensures that
|
||||
* drags that are at the edge or barely past the edge of the threshold does little to no
|
||||
* scrolling, while drags that are near the edge of the view does a lot of
|
||||
* scrolling. The equation y=x^10 is used, but this could also be tweaked if
|
||||
* needed.
|
||||
* @param ratio A ratio which is in the range [0, 1].
|
||||
* @return A "smoothed" value, also in the range [0, 1].
|
||||
*/
|
||||
private float smoothOutOfBoundsRatio(float ratio) {
|
||||
return (float) Math.pow(ratio, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by to calculate the proper amount of pixels to scroll given time passed
|
||||
* since scroll started, and to properly scroll / proper listener clean up if necessary.
|
||||
*
|
||||
* Callback used by scroller to perform UI tasks, such as scrolling and rerunning at next UI
|
||||
* cycle.
|
||||
*/
|
||||
abstract static class ScrollHost {
|
||||
/**
|
||||
* @return height of the view.
|
||||
*/
|
||||
abstract int getViewHeight();
|
||||
|
||||
/**
|
||||
* @param dy distance to scroll.
|
||||
*/
|
||||
abstract void scrollBy(int dy);
|
||||
|
||||
/**
|
||||
* @param r schedule runnable to be run at next convenient time.
|
||||
*/
|
||||
abstract void runAtNextFrame(@NonNull Runnable r);
|
||||
|
||||
/**
|
||||
* @param r remove runnable from being run.
|
||||
*/
|
||||
abstract void removeCallback(@NonNull Runnable r);
|
||||
}
|
||||
|
||||
static ScrollHost createScrollHost(final RecyclerView recyclerView) {
|
||||
return new RuntimeHost(recyclerView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks location of last surface contact as reported by RecyclerView.
|
||||
*/
|
||||
private static final class RuntimeHost extends ScrollHost {
|
||||
|
||||
private final RecyclerView mRecyclerView;
|
||||
|
||||
RuntimeHost(@NonNull RecyclerView recyclerView) {
|
||||
mRecyclerView = recyclerView;
|
||||
}
|
||||
|
||||
@Override
|
||||
void runAtNextFrame(@NonNull Runnable r) {
|
||||
ViewCompat.postOnAnimation(mRecyclerView, r);
|
||||
}
|
||||
|
||||
@Override
|
||||
void removeCallback(@NonNull Runnable r) {
|
||||
mRecyclerView.removeCallbacks(r);
|
||||
}
|
||||
|
||||
@Override
|
||||
void scrollBy(int dy) {
|
||||
if (VERBOSE) Log.v(TAG, "Scrolling view by: " + dy);
|
||||
mRecyclerView.scrollBy(0, dy);
|
||||
}
|
||||
|
||||
@Override
|
||||
int getViewHeight() {
|
||||
return mRecyclerView.getHeight();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A {@link androidx.recyclerview.widget.RecyclerView RecyclerView} addon library providing
|
||||
* support for item selection. The library provides support for both touch
|
||||
* and mouse driven selection. Developers retain control over the visual representation,
|
||||
* and the policies controlling selection behavior (like which items are eligible
|
||||
* for selection, and how many items can be selected.)
|
||||
*
|
||||
* <p>
|
||||
* Want to add selection support to your RecyclerView? Here's how you do it:
|
||||
*
|
||||
* <p>
|
||||
* <b>Determine which selection key type to use, then build your KeyProvider</b>
|
||||
*
|
||||
* <p>
|
||||
* Developers must decide on the key type used to identify selected items. Support
|
||||
* is provided for three types: {@link android.os.Parcelable Parcelable},
|
||||
* {@link java.lang.String String}, and {@link java.lang.Long Long}.
|
||||
*
|
||||
* <p>
|
||||
* See
|
||||
* {@link androidx.recyclerview.selection.SelectionTracker.Builder SelectionTracker.Builder}
|
||||
* for more detailed advice on which key type to use for your selection keys.
|
||||
*
|
||||
* <p>
|
||||
* <b>Implement {@link androidx.recyclerview.selection.ItemDetailsLookup ItemDetailsLookup}
|
||||
* </b>
|
||||
*
|
||||
* <p>
|
||||
* This class provides the selection library code necessary access to information about
|
||||
* items associated with {@link android.view.MotionEvent}. This will likely
|
||||
* depend on concrete {@link androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
* RecyclerView.ViewHolder} type employed by your application.
|
||||
*
|
||||
* <p>
|
||||
* <b>Update views used in RecyclerView to reflect selected state</b>
|
||||
*
|
||||
* <p>
|
||||
* When the user selects an item the library will record that in
|
||||
* {@link androidx.recyclerview.selection.SelectionTracker SelectionTracker}
|
||||
* then notify RecyclerView that the state of the item has changed. This
|
||||
* will ultimately cause the value to be rebound by way of
|
||||
* {@link androidx.recyclerview.widget.RecyclerView.Adapter#onBindViewHolder
|
||||
* RecyclerView.Adapter#onBindViewHolder}. The item must then be updated
|
||||
* to reflect the new selection status. Without this
|
||||
* the user will not *see* that the item has been selected.
|
||||
*
|
||||
* <ul>
|
||||
* <li>In Adapter#onBindViewHolder, set the "activated" status on view.
|
||||
* Note that the status should be "activated" not "selected".
|
||||
* See <a href="https://developer.android.com/reference/android/view/View.html#setActivated(boolean)">
|
||||
* View.html#setActivated</a> for details.
|
||||
* <li>Update the styling of the view to represent the activated status. This can be done
|
||||
* with a
|
||||
* <a href="https://developer.android.com/guide/topics/resources/color-list-resource.html">
|
||||
* color state list</a>.
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* <b>Use {@link androidx.appcompat.view.ActionMode ActionMode} when there is a selection</b>
|
||||
*
|
||||
* <p>
|
||||
* Register a {@link androidx.recyclerview.selection.SelectionTracker.SelectionObserver}
|
||||
* to be notified when selection changes. When a selection is first created, start
|
||||
* {@link androidx.appcompat.view.ActionMode ActionMode} to represent this to the user,
|
||||
* and provide selection specific actions.
|
||||
*
|
||||
* <p>
|
||||
* <b>Interpreted secondary actions: Drag and Drop, and Item Activation</b>
|
||||
*
|
||||
* <p>
|
||||
* At the end of the event processing pipeline the library may determine that the user
|
||||
* is attempting to activate an item by tapping it, or is attempting to drag and drop
|
||||
* an item or set of selected items. React to these interpretations by registering a
|
||||
* respective listener. See
|
||||
* {@link androidx.recyclerview.selection.SelectionTracker.Builder SelectionTracker.Builder}
|
||||
* for details.
|
||||
*
|
||||
* <p>
|
||||
* <b>Assemble everything with
|
||||
* {@link androidx.recyclerview.selection.SelectionTracker.Builder SelectionTracker.Builder}
|
||||
* </b>
|
||||
*
|
||||
* <p>
|
||||
* Example usage (with {@code Long} selection keys:
|
||||
* <pre>SelectionTracker<Long> tracker = new SelectionTracker.Builder<>(
|
||||
* "my-selection-id",
|
||||
* recyclerView,
|
||||
* new StableIdKeyProvider(recyclerView),
|
||||
* new MyDetailsLookup(recyclerView),
|
||||
* StorageStrategy.createLongStorage())
|
||||
* .build();
|
||||
*</pre>
|
||||
*
|
||||
* <p>In order to build a SelectionTracker instance the supplied RecyclerView must be initialized
|
||||
* with an Adapter. Given this fact, you will probably need to inject the SelectionTracker
|
||||
* instance into your RecyclerView.Adapter after the Adapter is created, as it will be necessary
|
||||
* to consult selected status using SelectionTracker from the onBindViewHolder method.
|
||||
*
|
||||
* <p>
|
||||
* <b>Include Selection in Activity lifecycle events</b>
|
||||
*
|
||||
* <p>
|
||||
* In order to preserve state the author must the selection library in handling
|
||||
* of Activity lifecycle events. See SelectionTracker#onSaveInstanceState
|
||||
* and SelectionTracker#onRestoreInstanceState.
|
||||
*
|
||||
* <p>A unique selection id must be supplied to
|
||||
* {@link androidx.recyclerview.selection.SelectionTracker.Builder SelectionTracker.Builder}
|
||||
* constructor. This is necessary as an activity or fragment may have multiple distinct
|
||||
* selectable lists that may both need to be persisted in saved state.
|
||||
*/
|
||||
|
||||
package androidx.recyclerview.selection;
|
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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
|
||||
-->
|
||||
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#339999ff" />
|
||||
<stroke android:width="1dp" android:color="#44000000" />
|
||||
</shape>
|
Loading…
Reference in new issue