mirror of
https://github.com/M66B/FairEmail.git
synced 2026-04-28 19:56:33 +02:00
308 lines
11 KiB
Java
308 lines
11 KiB
Java
/*
|
|
* 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.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.selection.SelectionTracker.SelectionPredicate;
|
|
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 SelectionTracker.SelectionPredicate<?> mSelectionPredicate;
|
|
private final AutoScroller mScroller;
|
|
private final ViewDelegate mView;
|
|
private final OperationMonitor mLock;
|
|
|
|
private boolean mStarted = false;
|
|
|
|
/**
|
|
* See {@link GestureSelectionHelper#create} for convenience
|
|
* method.
|
|
*/
|
|
GestureSelectionHelper(
|
|
@NonNull SelectionTracker<?> selectionTracker,
|
|
@NonNull SelectionPredicate<?> selectionPredicate,
|
|
@NonNull ViewDelegate view,
|
|
@NonNull AutoScroller scroller,
|
|
@NonNull OperationMonitor lock) {
|
|
|
|
checkArgument(selectionTracker != null);
|
|
checkArgument(selectionPredicate != null);
|
|
checkArgument(view != null);
|
|
checkArgument(scroller != null);
|
|
checkArgument(lock != null);
|
|
|
|
mSelectionMgr = selectionTracker;
|
|
mSelectionPredicate = selectionPredicate;
|
|
mView = view;
|
|
mScroller = scroller;
|
|
mLock = lock;
|
|
}
|
|
|
|
/**
|
|
* Explicitly kicks off a gesture multi-select.
|
|
*/
|
|
void start() {
|
|
checkState(!mStarted);
|
|
|
|
// Partner code in MotionInputHandler ensures items
|
|
// are selected and range anchor initialized 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) {
|
|
switch (e.getActionMasked()) {
|
|
case MotionEvent.ACTION_MOVE:
|
|
case MotionEvent.ACTION_UP:
|
|
case MotionEvent.ACTION_CANCEL:
|
|
return mStarted;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@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;
|
|
}
|
|
|
|
if (!mSelectionMgr.isRangeActive()) {
|
|
Log.e(TAG,
|
|
"Internal state of GestureSelectionHelper out of sync w/ SelectionTracker "
|
|
+ "(isRangeActive is false). Ignoring event and resetting state.");
|
|
endSelection();
|
|
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();
|
|
}
|
|
|
|
// 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);
|
|
|
|
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) {
|
|
int lastGlidedItemPos = mView.getLastGlidedItemPosition(e);
|
|
if (mSelectionPredicate.canSetStateAtPosition(lastGlidedItemPos, true)) {
|
|
extendSelection(lastGlidedItemPos);
|
|
}
|
|
|
|
mScroller.scroll(MotionEvents.getOrigin(e));
|
|
}
|
|
|
|
// 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 SelectionPredicate<?> selectionPredicate,
|
|
@NonNull RecyclerView recyclerView,
|
|
@NonNull AutoScroller scroller,
|
|
@NonNull OperationMonitor lock) {
|
|
|
|
return new GestureSelectionHelper(
|
|
selectionMgr,
|
|
selectionPredicate,
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|