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