mirror of
https://github.com/M66B/FairEmail.git
synced 2026-05-08 00:26:50 +02:00
Added patched recyclerview-selection 1.0.0
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user