mirror of
https://github.com/M66B/FairEmail.git
synced 2025-12-10 17:00:38 +01:00
Updated AndroidX
This commit is contained in:
@@ -551,16 +551,16 @@ dependencies {
|
||||
def appcompat_version = "1.7.0"
|
||||
def emoji_version = "1.5.0"
|
||||
def flatbuffers_version = "2.0.0"
|
||||
def activity_version = "1.10.0" // 1.11.0-rc01
|
||||
def fragment_version = "1.8.6"
|
||||
def windows_version = "1.3.0" // 1.4.0-rc02/1.5.0-alpha02
|
||||
def webkit_version = "1.13.0" // 1.14.0-beta01
|
||||
def activity_version = "1.10.0" // 1.11.0-rc01//1.12.0-alpha01
|
||||
def fragment_version = "1.8.7"
|
||||
def windows_version = "1.4.0" // 1.5.0-alpha02
|
||||
def webkit_version = "1.13.0" // 1.14.0-rc01
|
||||
def recyclerview_version = "1.4.0"
|
||||
def coordinatorlayout_version = "1.2.0" // 1.3.0-rc01
|
||||
def constraintlayout_version = "2.2.0"
|
||||
def viewpager_version = "1.1.0-beta01" // 1.1.0
|
||||
def material_version = "1.12.0" // 1.13.0-alpha03
|
||||
def browser_version = "1.8.0" // 1.9.0-alpha03
|
||||
def browser_version = "1.8.0" // 1.9.0-alpha04
|
||||
def lbm_version = "1.1.0"
|
||||
def swiperefresh_version = "1.2.0-beta01"
|
||||
def documentfile_version = "1.1.0"
|
||||
@@ -573,7 +573,7 @@ dependencies {
|
||||
def preference_version = "1.2.1"
|
||||
def work_version = "2.10.1"
|
||||
def exif_version = "1.4.1"
|
||||
def biometric_version = "1.2.0-alpha05" // 1.4.0-alpha03
|
||||
def biometric_version = "1.2.0-alpha05" // 1.4.0-alpha04
|
||||
def billingclient_version = "6.0.1" // 6.2.0
|
||||
def playservicesbasement_version = "18.5.0";
|
||||
def transparency_version = "2.5.73"
|
||||
@@ -644,7 +644,7 @@ dependencies {
|
||||
// https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview
|
||||
// https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview-selection
|
||||
implementation "androidx.recyclerview:recyclerview:$recyclerview_version"
|
||||
//implementation "androidx.recyclerview:recyclerview-selection:1.1.0" // 1.2.0-rc01
|
||||
//implementation "androidx.recyclerview:recyclerview-selection:1.2.0"
|
||||
|
||||
// https://mvnrepository.com/artifact/androidx.coordinatorlayout/coordinatorlayout
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorlayout_version"
|
||||
@@ -862,7 +862,7 @@ dependencies {
|
||||
implementation "com.github.seancfoley:ipaddress:$ipaddress_version"
|
||||
|
||||
// https://mvnrepository.com/artifact/androidx.car.app/app?repo=google
|
||||
// implementation "androidx.car.app:app:1.4.0-rc02"
|
||||
// implementation "androidx.car.app:app:1.4.0-rc02" // 1.8.0-alpha01
|
||||
|
||||
// https://github.com/square/leakcanary
|
||||
// https://square.github.io/leakcanary/getting_started/
|
||||
|
||||
@@ -17,21 +17,18 @@
|
||||
package androidx.recyclerview.selection;
|
||||
|
||||
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
|
||||
import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
|
||||
|
||||
import android.graphics.Point;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* Provides support for auto-scrolling a view.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(LIBRARY)
|
||||
@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
|
||||
public abstract class AutoScroller {
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,12 +21,12 @@ 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;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* Provides a means of controlling when and where band selection can be initiated.
|
||||
*
|
||||
@@ -132,7 +132,7 @@ public abstract class BandPredicate {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable ItemDetailsLookup.ItemDetails<?> details =
|
||||
ItemDetailsLookup.ItemDetails<?> details =
|
||||
mDetailsLookup.getItemDetails(e);
|
||||
return (details == null) || !details.inDragRegion(e);
|
||||
}
|
||||
|
||||
@@ -26,13 +26,14 @@ import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -210,7 +211,7 @@ class BandSelectionHelper<K> implements OnItemTouchListener, Resettable {
|
||||
}
|
||||
|
||||
// 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.
|
||||
// but it turns out 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;
|
||||
@@ -254,6 +255,8 @@ class BandSelectionHelper<K> implements OnItemTouchListener, Resettable {
|
||||
mLock.start();
|
||||
mFocusDelegate.clearFocus();
|
||||
mOrigin = origin;
|
||||
mCurrentPosition = 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.
|
||||
@@ -323,6 +326,22 @@ class BandSelectionHelper<K> implements OnItemTouchListener, Resettable {
|
||||
return;
|
||||
}
|
||||
|
||||
// mOrigin and mCurrentPosition should never be null when onScrolled is called,
|
||||
// but "never say never" increasingly looks like a motto to follow.
|
||||
// For this reason we guard those specific cases and provide a clear
|
||||
// error message in the logs.
|
||||
if (mOrigin == null) {
|
||||
Log.e(TAG, "onScrolled called while mOrigin null.");
|
||||
if (DEBUG) throw new IllegalStateException("mOrigin is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (mCurrentPosition == null) {
|
||||
Log.e(TAG, "onScrolled called while mCurrentPosition null.");
|
||||
if (DEBUG) throw new IllegalStateException("mCurrentPosition is null.");
|
||||
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;
|
||||
|
||||
@@ -25,7 +25,6 @@ import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
@@ -33,6 +32,8 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* RecyclerView backed {@link BandSelectionHelper.BandHost}.
|
||||
*/
|
||||
|
||||
@@ -24,14 +24,15 @@ 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 org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -46,7 +47,6 @@ import java.util.Set;
|
||||
* {@link SelectionPredicate#canSelectMultiple()}.
|
||||
*
|
||||
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(LIBRARY)
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -191,7 +191,7 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
|
||||
private Selection<K> clearSelectionQuietly() {
|
||||
mRange = null;
|
||||
|
||||
MutableSelection<K> prevSelection = new MutableSelection();
|
||||
MutableSelection<K> prevSelection = new MutableSelection<>();
|
||||
if (hasSelection()) {
|
||||
copySelection(prevSelection);
|
||||
mSelection.clear();
|
||||
@@ -394,6 +394,11 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
|
||||
|
||||
@SuppressWarnings({"WeakerAccess", "unchecked"}) /* synthetic access */
|
||||
void onDataSetChanged() {
|
||||
if (mSelection.isEmpty()) {
|
||||
Log.d(TAG, "Ignoring onDataSetChange. No active selection.");
|
||||
return;
|
||||
}
|
||||
|
||||
//mSelection.clearProvisionalSelection();
|
||||
|
||||
notifySelectionRefresh();
|
||||
@@ -401,10 +406,12 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
|
||||
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.
|
||||
// selection we must re-verify that the items are present
|
||||
// and if so, can still be selected.
|
||||
// Why? Because if the dataset has changed, then maybe the
|
||||
// selectability of an item has changed.
|
||||
if (!canSetState(key, true)) {
|
||||
// selectability of an item has changed, or item disappeared.
|
||||
if (mKeyProvider.getPosition(key) == RecyclerView.NO_POSITION
|
||||
|| !canSetState(key, true)) {
|
||||
if (toRemove == null) {
|
||||
toRemove = new ArrayList<>();
|
||||
}
|
||||
@@ -420,10 +427,13 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
|
||||
|
||||
if (toRemove != null) {
|
||||
for (K key : toRemove) {
|
||||
// TODO(b/163840879): Calling deselect fires onSelectionChanged
|
||||
// once per call. Meaning we're firing it n+1 times when deselecting.
|
||||
deselect(key);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Send onSelectionCleared if empty in 2.0 release.
|
||||
notifySelectionChanged();
|
||||
}
|
||||
|
||||
@@ -552,7 +562,7 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
|
||||
return;
|
||||
}
|
||||
|
||||
@Nullable Bundle selectionState = state.getBundle(getInstanceStateKey());
|
||||
Bundle selectionState = state.getBundle(getInstanceStateKey());
|
||||
if (selectionState == null) {
|
||||
return;
|
||||
}
|
||||
@@ -613,6 +623,10 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
|
||||
public void onItemRangeRemoved(int startPosition, int itemCount) {
|
||||
if (mSelectionTracker.isOverlapping(startPosition, itemCount))
|
||||
mSelectionTracker.endRange();
|
||||
// Since SelectionTracker deals in keys, not positions, we turn
|
||||
// to the `onDataSetChanged` sledge hammer.
|
||||
// DefaultSelectionTracker will validate and update it's selection.
|
||||
mSelectionTracker.onDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2020 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.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* Wrapper class that regulates delivery of MotionEvents to delegate listeners, uniformly
|
||||
* honoring requests to onRequestDisallowInterceptTouchEvent.
|
||||
* Wrap this class around other OnItemTouchListeners to bestow them with
|
||||
* proper support for onRequestDisallowInterceptTouchEvent.
|
||||
*/
|
||||
// TODO: Replace in-situ "disallow" implementation in EventRouter, ResetManager,
|
||||
// BandSelectionHelper, GestureSelectionHelper by wrapping w/ this class.
|
||||
class DisallowInterceptFilter implements
|
||||
OnItemTouchListener, Resettable {
|
||||
|
||||
private final OnItemTouchListener mDelegate;
|
||||
private boolean mDisallowIntercept;
|
||||
|
||||
DisallowInterceptFilter(@NonNull OnItemTouchListener delegate) {
|
||||
mDelegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
|
||||
// Reset disallow when the event is down as advised in http://b/139141511#comment20.
|
||||
if (mDisallowIntercept && MotionEvents.isActionDown(e)) {
|
||||
mDisallowIntercept = false;
|
||||
}
|
||||
return !mDisallowIntercept && mDelegate.onInterceptTouchEvent(rv, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
|
||||
mDelegate.onInterceptTouchEvent(rv, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
|
||||
mDisallowIntercept = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResetRequired() {
|
||||
return mDisallowIntercept;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
mDisallowIntercept = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2020 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.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* OnItemTouchListener that claims all ACTION_UP events in streams that have otherwise gone
|
||||
* unclaimed after a LongPress has been detected by GestureDetector.
|
||||
* This addresses issue described in b/166836317, where child view
|
||||
* OnClickListeners were being called unexpectedly.
|
||||
*/
|
||||
class EventBackstop implements OnItemTouchListener, Resettable {
|
||||
|
||||
private boolean mLongPressFired;
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
|
||||
// We only claim ACTION_UP events after a LongPress event. Were we to claim
|
||||
// all ACTION_UP events we'd deprive RecyclerView of the signal it needs to
|
||||
// initiate fling scrolling.
|
||||
if (MotionEvents.isActionUp(e) && mLongPressFired) {
|
||||
mLongPressFired = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Recover from disallow state.
|
||||
if (MotionEvents.isActionDown(e) && isResetRequired()) {
|
||||
reset();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
|
||||
// We should never receive any events, but were we to...we want to ignore them.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
|
||||
throw new UnsupportedOperationException("Wrap me in an InterceptFilter.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResetRequired() {
|
||||
return mLongPressFired;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
mLongPressFired = false;
|
||||
}
|
||||
|
||||
void onLongPress() {
|
||||
mLongPressFired = true;
|
||||
}
|
||||
}
|
||||
@@ -17,17 +17,17 @@
|
||||
package androidx.recyclerview.selection;
|
||||
|
||||
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
|
||||
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.core.util.Consumer;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* Provides the necessary glue to notify RecyclerView when selection data changes,
|
||||
* and to notify SelectionTracker when the underlying RecyclerView.Adapter data changes.
|
||||
@@ -36,10 +36,8 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
* with multiple RecyclerView instances. This may be necessary when multiple
|
||||
* different views of data are presented to the user.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(LIBRARY)
|
||||
@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
|
||||
public class EventBridge {
|
||||
|
||||
private static final String TAG = "EventsRelays";
|
||||
@@ -50,37 +48,45 @@ public class EventBridge {
|
||||
* @param adapter
|
||||
* @param selectionTracker
|
||||
* @param keyProvider
|
||||
* @param runner Callback allowing operation to be run at next opportune time.
|
||||
* Implementation could be {@link RecyclerView#postOnAnimation(Runnable)}.
|
||||
*
|
||||
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
*/
|
||||
public static <K> void install(
|
||||
@NonNull RecyclerView.Adapter<?> adapter,
|
||||
RecyclerView.@NonNull Adapter<?> adapter,
|
||||
@NonNull SelectionTracker<K> selectionTracker,
|
||||
@NonNull ItemKeyProvider<K> keyProvider) {
|
||||
@NonNull ItemKeyProvider<K> keyProvider,
|
||||
@NonNull Consumer<Runnable> runner) {
|
||||
|
||||
// setup bridges to relay selection and adapter events
|
||||
new TrackerToAdapterBridge<>(selectionTracker, keyProvider, adapter);
|
||||
new TrackerToAdapterBridge<>(selectionTracker, keyProvider, adapter, runner);
|
||||
adapter.registerAdapterDataObserver(selectionTracker.getAdapterDataObserver());
|
||||
}
|
||||
|
||||
private static final class TrackerToAdapterBridge<K>
|
||||
extends SelectionTracker.SelectionObserver<K> {
|
||||
|
||||
// Non-private as necessary to avoid synthetic accessors for inner classes.
|
||||
final RecyclerView.Adapter<?> mAdapter;
|
||||
private final ItemKeyProvider<K> mKeyProvider;
|
||||
private final RecyclerView.Adapter<?> mAdapter;
|
||||
private final Consumer<Runnable> mRunner;
|
||||
|
||||
TrackerToAdapterBridge(
|
||||
@NonNull SelectionTracker<K> selectionTracker,
|
||||
@NonNull ItemKeyProvider<K> keyProvider,
|
||||
@NonNull RecyclerView.Adapter<?> adapter) {
|
||||
RecyclerView.@NonNull Adapter<?> adapter,
|
||||
Consumer<Runnable> runner) {
|
||||
|
||||
selectionTracker.addObserver(this);
|
||||
|
||||
checkArgument(keyProvider != null);
|
||||
checkArgument(adapter != null);
|
||||
checkArgument(runner != null);
|
||||
|
||||
mKeyProvider = keyProvider;
|
||||
mAdapter = adapter;
|
||||
mRunner = runner;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +102,12 @@ public class EventBridge {
|
||||
return;
|
||||
}
|
||||
|
||||
mAdapter.notifyItemChanged(position, SelectionTracker.SELECTION_CHANGED_MARKER);
|
||||
mRunner.accept(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mAdapter.notifyItemChanged(position, SelectionTracker.SELECTION_CHANGED_MARKER);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,10 +20,11 @@ import static androidx.core.util.Preconditions.checkArgument;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* A class responsible for routing MotionEvents to tool-type specific handlers.
|
||||
* Individual tool-type specific handlers are added after the class is constructed.
|
||||
@@ -33,37 +34,62 @@ import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
|
||||
* {@link RecyclerView#addOnItemTouchListener(OnItemTouchListener)}. Despite "Touch"
|
||||
* being in the name, it receives MotionEvents for all types of tools.
|
||||
*/
|
||||
final class EventRouter implements OnItemTouchListener {
|
||||
final class EventRouter implements OnItemTouchListener, Resettable {
|
||||
|
||||
private final ToolHandlerRegistry<OnItemTouchListener> mDelegates;
|
||||
private final ToolSourceHandlerRegistry<OnItemTouchListener> mDelegates;
|
||||
private boolean mDisallowIntercept;
|
||||
|
||||
EventRouter() {
|
||||
mDelegates = new ToolHandlerRegistry<>(new DummyOnItemTouchListener());
|
||||
mDelegates = new ToolSourceHandlerRegistry<>(new StubOnItemTouchListener());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param toolType See MotionEvent for details on available types.
|
||||
* @param delegate An {@link OnItemTouchListener} to receive events
|
||||
* of {@code toolType}.
|
||||
* @param key Either a TOOL_TYPE or a combination of TOOL_TYPE and SOURCE
|
||||
* @param delegate An {@link OnItemTouchListener} to receive events of {@code ToolSourceKey}.
|
||||
*/
|
||||
void set(int toolType, @NonNull OnItemTouchListener delegate) {
|
||||
void set(@NonNull ToolSourceKey key, @NonNull OnItemTouchListener delegate) {
|
||||
checkArgument(delegate != null);
|
||||
|
||||
mDelegates.set(toolType, delegate);
|
||||
mDelegates.set(key, delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
|
||||
return mDelegates.get(e).onInterceptTouchEvent(rv, e);
|
||||
// Reset disallow when the event is down as advised in http://b/139141511#comment20.
|
||||
if (mDisallowIntercept && MotionEvents.isActionDown(e)) {
|
||||
mDisallowIntercept = false;
|
||||
}
|
||||
return !mDisallowIntercept && mDelegates.get(e).onInterceptTouchEvent(rv, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
|
||||
mDelegates.get(e).onTouchEvent(rv, e);
|
||||
if (!mDisallowIntercept) {
|
||||
mDelegates.get(e).onTouchEvent(rv, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
|
||||
// TODO(b/139141511): Handle onRequestDisallowInterceptTouchEvent.
|
||||
if (!disallowIntercept) {
|
||||
return; // Ignore as advised in http://b/139141511#comment20
|
||||
}
|
||||
|
||||
// Some types of views, such as HorizontalScrollView, may want
|
||||
// to take over the input stream. In this case they'll call this method
|
||||
// with disallowIntercept=true. mDisallowIntercept is reset on UP or CANCEL
|
||||
// events in onInterceptTouchEvent.
|
||||
mDisallowIntercept = disallowIntercept;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isResetRequired() {
|
||||
return mDisallowIntercept;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
mDisallowIntercept = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,11 @@
|
||||
|
||||
package androidx.recyclerview.selection;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* Override methods in this class to provide application specific behaviors
|
||||
* related to focusing item.
|
||||
@@ -28,7 +29,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
*/
|
||||
public abstract class FocusDelegate<K> {
|
||||
|
||||
static <K> FocusDelegate<K> dummy() {
|
||||
static <K> FocusDelegate<K> stub() {
|
||||
return new FocusDelegate<K>() {
|
||||
@Override
|
||||
public void focusItem(@NonNull ItemDetails<K> item) {
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 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;
|
||||
|
||||
/**
|
||||
* Class allowing GestureDetector to listen directly to RecyclerView touch events.
|
||||
*/
|
||||
final class GestureDetectorOnItemTouchListenerAdapter implements RecyclerView.OnItemTouchListener {
|
||||
|
||||
private final GestureDetector mDetector;
|
||||
|
||||
GestureDetectorOnItemTouchListenerAdapter(@NonNull GestureDetector detector) {
|
||||
checkArgument(detector != null);
|
||||
|
||||
mDetector = detector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
|
||||
// While the idea of "intercepting" an event stream isn't consistent
|
||||
// with the world-view of GestureDetector, failure to return true here
|
||||
// resulted in a bug where a context menu shown on an item view was not
|
||||
// visible...despite returning reporting that the menu was shown.
|
||||
// See b/143494310 for further details.
|
||||
return mDetector.onTouchEvent(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2019 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.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* A wrapper class for GestureDetector allowing it interact with SelectionTracker
|
||||
* and its dependencies (like RecyclerView) on terms more amenable to SelectionTracker.
|
||||
*/
|
||||
final class GestureDetectorWrapper implements RecyclerView.OnItemTouchListener, Resettable {
|
||||
|
||||
private final GestureDetector mDetector;
|
||||
private boolean mDisallowIntercept;
|
||||
|
||||
GestureDetectorWrapper(@NonNull GestureDetector detector) {
|
||||
checkArgument(detector != null);
|
||||
|
||||
mDetector = detector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
|
||||
// Reset disallow when the event is down as advised in http://b/139141511#comment20.
|
||||
if (mDisallowIntercept && MotionEvents.isActionDown(e)) {
|
||||
mDisallowIntercept = false;
|
||||
}
|
||||
|
||||
// While the idea of "intercepting" an event stream isn't consistent
|
||||
// with the world-view of GestureDetector, failure to return true here
|
||||
// resulted in a bug where a context menu shown on an item view was not
|
||||
// visible...despite returning reporting that the menu was shown.
|
||||
// See b/143494310 for further details.
|
||||
return !mDisallowIntercept && mDetector.onTouchEvent(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
|
||||
if (!disallowIntercept) {
|
||||
return; // Ignore as advised in http://b/139141511#comment20
|
||||
}
|
||||
|
||||
// Some types of views, such as HorizontalScrollView, may want
|
||||
// to take over the input stream. In this case they'll call this method
|
||||
// with disallowIntercept=true. mDisallowIntercept is reset on UP or CANCEL
|
||||
// events in onInterceptTouchEvent.
|
||||
mDisallowIntercept = disallowIntercept;
|
||||
|
||||
|
||||
// GestureDetector may have internal state (such as timers) that can
|
||||
// result in subsequent event handlers being called, even after
|
||||
// we receive a request to disallow intercept (e.g. LONG_PRESS).
|
||||
// For that reason we proactively reset GestureDetector.
|
||||
sendCancelEvent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResetRequired() {
|
||||
// Always resettable as we don't know the specifics of GD's internal state.
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
mDisallowIntercept = false;
|
||||
sendCancelEvent();
|
||||
}
|
||||
|
||||
private void sendCancelEvent() {
|
||||
// GestureDetector does not provide a public affordance for resetting
|
||||
// it's internal state, so we send it a synthetic ACTION_CANCEL event.
|
||||
mDetector.onTouchEvent(MotionEvents.createCancelEvent());
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,8 @@ import android.view.GestureDetector.OnGestureListener;
|
||||
import android.view.GestureDetector.SimpleOnGestureListener;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* GestureRouter is responsible for routing gestures detected by a GestureDetector
|
||||
@@ -36,11 +36,11 @@ import androidx.annotation.Nullable;
|
||||
final class GestureRouter<T extends OnGestureListener & OnDoubleTapListener>
|
||||
implements OnGestureListener, OnDoubleTapListener {
|
||||
|
||||
private final ToolHandlerRegistry<T> mDelegates;
|
||||
private final ToolSourceHandlerRegistry<T> mDelegates;
|
||||
|
||||
GestureRouter(@NonNull T defaultDelegate) {
|
||||
checkArgument(defaultDelegate != null);
|
||||
mDelegates = new ToolHandlerRegistry<>(defaultDelegate);
|
||||
mDelegates = new ToolSourceHandlerRegistry<>(defaultDelegate);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -49,11 +49,11 @@ final class GestureRouter<T extends OnGestureListener & OnDoubleTapListener>
|
||||
}
|
||||
|
||||
/**
|
||||
* @param toolType
|
||||
* @param key
|
||||
* @param delegate the delegate, or null to unregister.
|
||||
*/
|
||||
public void register(int toolType, @Nullable T delegate) {
|
||||
mDelegates.set(toolType, delegate);
|
||||
public void register(@NonNull ToolSourceKey key, @Nullable T delegate) {
|
||||
mDelegates.set(key, delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -24,13 +24,13 @@ 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;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* GestureSelectionHelper provides logic that interprets a combination
|
||||
* of motions and gestures in order to provide gesture driven selection support
|
||||
@@ -96,7 +96,6 @@ final class GestureSelectionHelper implements OnItemTouchListener, Resettable {
|
||||
}
|
||||
|
||||
@Override
|
||||
/** @hide */
|
||||
public boolean onInterceptTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
|
||||
// MotionEvents that aren't ACTION_DOWN are only ever passed to either onInterceptTouchEvent
|
||||
// or onTouchEvent; never to both, so events delivered to this method are effectively
|
||||
@@ -120,7 +119,6 @@ final class GestureSelectionHelper implements OnItemTouchListener, Resettable {
|
||||
}
|
||||
|
||||
@Override
|
||||
/** @hide */
|
||||
public void onTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
|
||||
if (!mStarted) {
|
||||
if (VERBOSE) Log.i(TAG, "Ignoring input event. Not started.");
|
||||
@@ -147,7 +145,6 @@ final class GestureSelectionHelper implements OnItemTouchListener, Resettable {
|
||||
}
|
||||
|
||||
@Override
|
||||
/** @hide */
|
||||
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
|
||||
}
|
||||
|
||||
@@ -268,11 +265,12 @@ final class GestureSelectionHelper implements OnItemTouchListener, Resettable {
|
||||
|
||||
@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
|
||||
// If user has moved their 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);
|
||||
int direction = mRecyclerView.getLayoutDirection();
|
||||
final boolean pastLastItem = isPastLastItem(lastItem.getTop(),
|
||||
lastItem.getLeft(),
|
||||
lastItem.getRight(),
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package androidx.recyclerview.selection;
|
||||
|
||||
import static androidx.core.util.Preconditions.checkArgument;
|
||||
import static androidx.core.util.Preconditions.checkState;
|
||||
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
@@ -25,12 +26,13 @@ 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 org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -145,6 +147,7 @@ final class GridModel<K> {
|
||||
|
||||
mIsActive = true;
|
||||
mPointer = mHost.createAbsolutePoint(relativeOrigin);
|
||||
|
||||
mRelOrigin = createRelativePoint(mPointer);
|
||||
mRelPointer = createRelativePoint(mPointer);
|
||||
computeCurrentSelection();
|
||||
@@ -171,7 +174,11 @@ final class GridModel<K> {
|
||||
*/
|
||||
void resizeSelection(Point relativePointer) {
|
||||
mPointer = mHost.createAbsolutePoint(relativePointer);
|
||||
updateModel();
|
||||
// Should probably never been empty at this point, yet we guard against
|
||||
// known exceptions because wholesome goodness.
|
||||
if (!isEmpty()) {
|
||||
updateModel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,7 +198,12 @@ final class GridModel<K> {
|
||||
mPointer.x += dx;
|
||||
mPointer.y += dy;
|
||||
recordVisibleChildren();
|
||||
updateModel();
|
||||
|
||||
// Should probably never been empty at this point, yet we guard against
|
||||
// known exceptions because wholesome goodness.
|
||||
if (!isEmpty()) {
|
||||
updateModel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,7 +270,9 @@ final class GridModel<K> {
|
||||
* in a selection change and, if it has, notifies listeners of this change.
|
||||
*/
|
||||
private void updateModel() {
|
||||
checkState(!isEmpty());
|
||||
RelativePoint old = mRelPointer;
|
||||
|
||||
mRelPointer = createRelativePoint(mPointer);
|
||||
if (mRelPointer.equals(old)) {
|
||||
return;
|
||||
@@ -590,6 +604,11 @@ final class GridModel<K> {
|
||||
}
|
||||
|
||||
RelativePoint createRelativePoint(Point point) {
|
||||
// mColumnBounds and mRowBounds is empty when there are no items in the view.
|
||||
// Clients have to verify items exist before calling this method.
|
||||
checkState(!mColumnBounds.isEmpty(), "Column bounds not established.");
|
||||
checkState(!mRowBounds.isEmpty(), "Row bounds not established.");
|
||||
|
||||
return new RelativePoint(
|
||||
new RelativeCoordinate(mColumnBounds, point.x),
|
||||
new RelativeCoordinate(mRowBounds, point.y));
|
||||
@@ -604,14 +623,6 @@ final class GridModel<K> {
|
||||
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;
|
||||
|
||||
@@ -20,11 +20,12 @@ import static androidx.annotation.RestrictTo.Scope.LIBRARY;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* The Selection library calls {@link #getItemDetails(MotionEvent)} when it needs
|
||||
* access to information about the area and/or {@link ItemDetails} under a {@link MotionEvent}.
|
||||
@@ -70,7 +71,6 @@ public abstract class ItemDetailsLookup<K> {
|
||||
|
||||
/**
|
||||
* @return true if there is an item w/ a stable ID at the event coordinates.
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(LIBRARY)
|
||||
protected boolean overItemWithSelectionKey(@NonNull MotionEvent e) {
|
||||
@@ -100,7 +100,7 @@ public abstract class ItemDetailsLookup<K> {
|
||||
* @return the adapter position of the item at the event coordinates.
|
||||
*/
|
||||
final int getItemPosition(@NonNull MotionEvent e) {
|
||||
@Nullable ItemDetails<?> item = getItemDetails(e);
|
||||
ItemDetails<?> item = getItemDetails(e);
|
||||
return item != null
|
||||
? item.getPosition()
|
||||
: RecyclerView.NO_POSITION;
|
||||
@@ -172,7 +172,8 @@ public abstract class ItemDetailsLookup<K> {
|
||||
|
||||
/**
|
||||
* Returns the adapter position of the item. See
|
||||
* {@link RecyclerView.ViewHolder#getAdapterPosition() ViewHolder.getAdapterPosition}
|
||||
* {@link RecyclerView.ViewHolder#getAbsoluteAdapterPosition() ViewHolder
|
||||
* .getAbsoluteAdapterPosition}
|
||||
*
|
||||
* @return the position of an item.
|
||||
*/
|
||||
|
||||
@@ -19,10 +19,11 @@ 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 org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@@ -79,7 +80,8 @@ public abstract class ItemKeyProvider<K> {
|
||||
public abstract @Nullable K getKey(int position);
|
||||
|
||||
/**
|
||||
* @return the position corresponding to the selection key, or RecyclerView.NO_POSITION.
|
||||
* @return the position corresponding to the selection key, or RecyclerView.NO_POSITION
|
||||
* if the key is unrecognized.
|
||||
*/
|
||||
public abstract int getPosition(@NonNull K key);
|
||||
}
|
||||
|
||||
@@ -17,17 +17,27 @@
|
||||
package androidx.recyclerview.selection;
|
||||
|
||||
import android.graphics.Point;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* Utility methods for working with {@link MotionEvent} instances.
|
||||
*/
|
||||
final class MotionEvents {
|
||||
|
||||
private MotionEvents() {}
|
||||
private MotionEvents() {
|
||||
}
|
||||
|
||||
static boolean isTouchpadEvent(@NonNull MotionEvent e) {
|
||||
// ChromeOS ARC devices with touchpads emit their events with
|
||||
// {@link MotionEvent#TOOL_TYPE_MOUSE}, so this is specifically capturing non-ARC devices
|
||||
// with touchpads (e.g. attachable keyboards with touchpads on Android tablets).
|
||||
return e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER
|
||||
&& e.getSource() == InputDevice.SOURCE_MOUSE;
|
||||
}
|
||||
|
||||
static boolean isMouseEvent(@NonNull MotionEvent e) {
|
||||
return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
|
||||
@@ -101,20 +111,22 @@ final class MotionEvents {
|
||||
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);
|
||||
return (isTouchpadEvent(e) || isMouseEvent(e)) && isActionMove(e)
|
||||
&& e.getButtonState() == 0;
|
||||
}
|
||||
|
||||
private static boolean hasBit(int metaState, int bit) {
|
||||
return (metaState & bit) != 0;
|
||||
}
|
||||
|
||||
static MotionEvent createCancelEvent() {
|
||||
return MotionEvent.obtain(
|
||||
0, // down time
|
||||
1, // event time
|
||||
MotionEvent.ACTION_CANCEL,
|
||||
0, // x
|
||||
0, // y
|
||||
0 // metaState
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,12 @@ 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;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Base class for handlers that can be registered w/ {@link GestureRouter}.
|
||||
*/
|
||||
|
||||
@@ -23,11 +23,11 @@ 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;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* A MotionInputHandler that provides the high-level glue for mouse driven selection. This
|
||||
* class works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper}
|
||||
@@ -35,7 +35,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
*/
|
||||
final class MouseInputHandler<K> extends MotionInputHandler<K> {
|
||||
|
||||
private static final String TAG = "MouseInputDelegate";
|
||||
private static final String TAG = "MouseInputHandler";
|
||||
|
||||
private final ItemDetailsLookup<K> mDetailsLookup;
|
||||
private final OnContextClickListener mOnContextClickListener;
|
||||
@@ -170,7 +170,7 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
|
||||
ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
|
||||
if (item == null || !item.hasSelectionKey()) {
|
||||
return false;
|
||||
}
|
||||
@@ -204,7 +204,7 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
|
||||
|
||||
private boolean onRightClick(@NonNull MotionEvent e) {
|
||||
if (mDetailsLookup.overItemWithSelectionKey(e)) {
|
||||
@Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
|
||||
ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
|
||||
if (item != null && !mSelectionTracker.isSelected(item.getSelectionKey())) {
|
||||
mSelectionTracker.clearSelection();
|
||||
selectItem(item);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package androidx.recyclerview.selection;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* Subclass of {@link Selection} exposing public support for mutating the underlying
|
||||
|
||||
@@ -18,7 +18,7 @@ package androidx.recyclerview.selection;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* Override methods in this class to provide application specific behaviors
|
||||
|
||||
@@ -22,7 +22,7 @@ import android.content.ClipData;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* Register an OnDragInitiatedListener to be notified when user intent to perform drag and drop
|
||||
|
||||
@@ -18,9 +18,10 @@ package androidx.recyclerview.selection;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* Register an OnItemActivatedListener to be notified when an item is activated
|
||||
* (tapped or double clicked).
|
||||
@@ -31,7 +32,7 @@ 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
|
||||
* exists and the user taps an item with their finger, or double clicks an item with a
|
||||
* pointing device like a Mouse.
|
||||
*
|
||||
* @param item details of the item.
|
||||
|
||||
@@ -24,9 +24,10 @@ import static androidx.recyclerview.selection.Shared.DEBUG;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RestrictTo;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -52,7 +53,7 @@ public final class OperationMonitor {
|
||||
// Ideally OperationMonitor would implement Resettable
|
||||
// directly, but Metalava couldn't understand that
|
||||
// `OperationMonitor` was public API while `Resettable` was
|
||||
// not. This is our klunkuy workaround.
|
||||
// not. This is our clever workaround :)
|
||||
private final Resettable mResettable = new Resettable() {
|
||||
|
||||
@Override
|
||||
@@ -68,55 +69,65 @@ public final class OperationMonitor {
|
||||
|
||||
private int mNumOps = 0;
|
||||
|
||||
private final Object mLock = new Object();
|
||||
|
||||
@MainThread
|
||||
synchronized void start() {
|
||||
mNumOps++;
|
||||
void start() {
|
||||
synchronized (mLock) {
|
||||
mNumOps++;
|
||||
|
||||
if (mNumOps == 1) {
|
||||
notifyStateChanged();
|
||||
if (mNumOps == 1) {
|
||||
notifyStateChanged();
|
||||
}
|
||||
|
||||
if (DEBUG) Log.v(TAG, "Incremented content lock count to " + mNumOps + ".");
|
||||
}
|
||||
|
||||
if (DEBUG) Log.v(TAG, "Incremented content lock count to " + mNumOps + ".");
|
||||
}
|
||||
|
||||
@MainThread
|
||||
synchronized void stop() {
|
||||
if (mNumOps == 0) {
|
||||
if (DEBUG) Log.w(TAG, "Stop called whith opt count of 0.");
|
||||
return;
|
||||
void stop() {
|
||||
synchronized (mLock) {
|
||||
if (mNumOps == 0) {
|
||||
if (DEBUG) Log.w(TAG, "Stop called whith opt count of 0.");
|
||||
return;
|
||||
}
|
||||
|
||||
mNumOps--;
|
||||
if (DEBUG) Log.v(TAG, "Decremented content lock count to " + mNumOps + ".");
|
||||
|
||||
if (mNumOps == 0) {
|
||||
notifyStateChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mNumOps--;
|
||||
if (DEBUG) Log.v(TAG, "Decremented content lock count to " + mNumOps + ".");
|
||||
|
||||
if (mNumOps == 0) {
|
||||
@RestrictTo(LIBRARY)
|
||||
@MainThread
|
||||
void reset() {
|
||||
synchronized (mLock) {
|
||||
if (DEBUG) Log.d(TAG, "Received reset request.");
|
||||
if (mNumOps > 0) {
|
||||
Log.w(TAG, "Resetting OperationMonitor with " + mNumOps + " active operations.");
|
||||
}
|
||||
mNumOps = 0;
|
||||
notifyStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@RestrictTo(LIBRARY)
|
||||
@MainThread
|
||||
synchronized void reset() {
|
||||
if (DEBUG) Log.d(TAG, "Received reset request.");
|
||||
if (mNumOps > 0) {
|
||||
Log.w(TAG, "Resetting OperationMonitor with " + mNumOps + " active operations.");
|
||||
boolean isResetRequired() {
|
||||
synchronized (mLock) {
|
||||
return isStarted();
|
||||
}
|
||||
mNumOps = 0;
|
||||
notifyStateChanged();
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@RestrictTo(LIBRARY)
|
||||
synchronized boolean isResetRequired() {
|
||||
return isStarted();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if there are any running operations.
|
||||
*/
|
||||
public synchronized boolean isStarted() {
|
||||
return mNumOps > 0;
|
||||
public boolean isStarted() {
|
||||
synchronized (mLock) {
|
||||
return mNumOps > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,7 +165,6 @@ public final class OperationMonitor {
|
||||
|
||||
/**
|
||||
* Work around b/139109223.
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(LIBRARY)
|
||||
@NonNull Resettable asResettable() {
|
||||
|
||||
@@ -19,14 +19,16 @@ package androidx.recyclerview.selection;
|
||||
import static androidx.core.util.Preconditions.checkArgument;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* OnItemTouchListener that detects and delegates drag events to a drag listener,
|
||||
* else sends event to fallback {@link OnItemTouchListener}.
|
||||
* OnItemTouchListener that detects and 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.
|
||||
*/
|
||||
@@ -35,6 +37,9 @@ final class PointerDragEventInterceptor implements OnItemTouchListener {
|
||||
private final ItemDetailsLookup<?> mEventDetailsLookup;
|
||||
private final OnDragInitiatedListener mDragListener;
|
||||
private OnItemTouchListener mDelegate;
|
||||
private float mDownX;
|
||||
private float mDownY;
|
||||
private boolean mDownInItemDragRegion;
|
||||
|
||||
PointerDragEventInterceptor(
|
||||
ItemDetailsLookup<?> eventDetailsLookup,
|
||||
@@ -50,14 +55,28 @@ final class PointerDragEventInterceptor implements OnItemTouchListener {
|
||||
if (delegate != null) {
|
||||
mDelegate = delegate;
|
||||
} else {
|
||||
mDelegate = new DummyOnItemTouchListener();
|
||||
mDelegate = new StubOnItemTouchListener();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
|
||||
if (MotionEvents.isPointerDragEvent(e) && mEventDetailsLookup.inItemDragRegion(e)) {
|
||||
return mDragListener.onDragInitiated(e);
|
||||
if (MotionEvents.isPrimaryMouseButtonPressed(e)) {
|
||||
float x = e.getX();
|
||||
float y = e.getY();
|
||||
if (MotionEvents.isActionDown(e)) {
|
||||
mDownX = x;
|
||||
mDownY = y;
|
||||
mDownInItemDragRegion = mEventDetailsLookup.inItemDragRegion(e);
|
||||
} else if (mDownInItemDragRegion && MotionEvents.isActionMove(e)) {
|
||||
int touchSlop = ViewConfiguration.get(rv.getContext()).getScaledTouchSlop();
|
||||
float dx = x - mDownX;
|
||||
float dy = y - mDownY;
|
||||
float distanceSquared = (dx * dx) + (dy * dy);
|
||||
if (distanceSquared > (touchSlop * touchSlop)) {
|
||||
return mDragListener.onDragInitiated(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mDelegate.onInterceptTouchEvent(rv, e);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@@ -21,11 +21,12 @@ import static androidx.recyclerview.selection.Shared.DEBUG;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.selection.SelectionTracker.SelectionObserver;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ import androidx.annotation.RestrictTo;
|
||||
* should always return false when called immediately after {@link #reset()}
|
||||
* has been called.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(LIBRARY)
|
||||
public interface Resettable {
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
package androidx.recyclerview.selection;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
|
||||
package androidx.recyclerview.selection;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* Utility class for creating SelectionPredicate instances. Provides default
|
||||
* implementations for common cases like "single selection" and "select anything".
|
||||
@@ -34,7 +35,7 @@ public final class SelectionPredicates {
|
||||
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
* @return
|
||||
*/
|
||||
public static @NonNull <K> SelectionPredicate<K> createSelectAnything() {
|
||||
public static <K> @NonNull SelectionPredicate<K> createSelectAnything() {
|
||||
return new SelectionPredicate<K>() {
|
||||
@Override
|
||||
public boolean canSetStateForKey(@NonNull K key, boolean nextState) {
|
||||
@@ -60,7 +61,7 @@ public final class SelectionPredicates {
|
||||
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
||||
* @return
|
||||
*/
|
||||
public static @NonNull <K> SelectionPredicate<K> createSelectSingleAnything() {
|
||||
public static <K> @NonNull SelectionPredicate<K> createSelectSingleAnything() {
|
||||
return new SelectionPredicate<K>() {
|
||||
@Override
|
||||
public boolean canSetStateForKey(@NonNull K key, boolean nextState) {
|
||||
|
||||
@@ -19,22 +19,25 @@ package androidx.recyclerview.selection;
|
||||
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
|
||||
import static androidx.core.util.Preconditions.checkArgument;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.util.Log;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.InputDevice;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -90,8 +93,6 @@ import java.util.Set;
|
||||
*/
|
||||
public abstract class SelectionTracker<K> {
|
||||
|
||||
private static final String TAG = "SelectionTracker";
|
||||
|
||||
/**
|
||||
* This value is included in the payload when SelectionTracker notifies RecyclerView
|
||||
* of changes to selection. Look for this value in the {@code payload}
|
||||
@@ -103,6 +104,7 @@ public abstract class SelectionTracker<K> {
|
||||
* When state is being restored, this argument will not be present.
|
||||
*/
|
||||
public static final String SELECTION_CHANGED_MARKER = "Selection-Changed";
|
||||
private static final String TAG = "SelectionTracker";
|
||||
|
||||
/**
|
||||
* Adds {@code observer} to be notified when changes to selection occur.
|
||||
@@ -163,6 +165,7 @@ public abstract class SelectionTracker<K> {
|
||||
* Sets the selected state of the specified items if permitted after consulting
|
||||
* SelectionPredicate.
|
||||
*/
|
||||
@SuppressLint("LambdaLast")
|
||||
public abstract boolean setItemsSelected(@NonNull Iterable<K> keys, boolean selected);
|
||||
|
||||
/**
|
||||
@@ -181,7 +184,7 @@ public abstract class SelectionTracker<K> {
|
||||
*/
|
||||
public abstract boolean deselect(@NonNull K key);
|
||||
|
||||
/** @hide */
|
||||
@SuppressWarnings("HiddenAbstractMethod")
|
||||
@RestrictTo(LIBRARY)
|
||||
protected abstract @NonNull AdapterDataObserver getAdapterDataObserver();
|
||||
|
||||
@@ -192,8 +195,8 @@ public abstract class SelectionTracker<K> {
|
||||
* @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.
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings("HiddenAbstractMethod")
|
||||
@RestrictTo(LIBRARY)
|
||||
public abstract void startRange(int position);
|
||||
|
||||
@@ -208,8 +211,8 @@ public abstract class SelectionTracker<K> {
|
||||
* @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)}.
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings("HiddenAbstractMethod")
|
||||
@RestrictTo(LIBRARY)
|
||||
public abstract void extendRange(int position);
|
||||
|
||||
@@ -217,16 +220,15 @@ public abstract class SelectionTracker<K> {
|
||||
* Clears an in-progress range selection. Provisional range selection established
|
||||
* using {@link #extendProvisionalRange(int)} will be cleared (unless
|
||||
* {@link #mergeProvisionalSelection()} is called first.)
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings("HiddenAbstractMethod")
|
||||
@RestrictTo(LIBRARY)
|
||||
public abstract void endRange();
|
||||
|
||||
/**
|
||||
* @return Whether or not there is a current range selection active.
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings("HiddenAbstractMethod")
|
||||
@RestrictTo(LIBRARY)
|
||||
public abstract boolean isRangeActive();
|
||||
|
||||
@@ -238,8 +240,8 @@ public abstract class SelectionTracker<K> {
|
||||
* TODO: Reconcile this with startRange. Maybe just docs need to be updated.
|
||||
*
|
||||
* @param position the anchor position. Must already be selected.
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings("HiddenAbstractMethod")
|
||||
@RestrictTo(LIBRARY)
|
||||
public abstract void anchorRange(int position);
|
||||
|
||||
@@ -247,33 +249,30 @@ public abstract class SelectionTracker<K> {
|
||||
* Creates a provisional selection from anchor to {@code position}.
|
||||
*
|
||||
* @param position the end point.
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings("HiddenAbstractMethod")
|
||||
@RestrictTo(LIBRARY)
|
||||
protected abstract void extendProvisionalRange(int position);
|
||||
|
||||
/**
|
||||
* Sets the provisional selection, replacing any existing selection.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings("HiddenAbstractMethod")
|
||||
@RestrictTo(LIBRARY)
|
||||
protected abstract void setProvisionalSelection(@NonNull Set<K> newSelection);
|
||||
|
||||
/**
|
||||
* Clears any existing provisional selection
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings("HiddenAbstractMethod")
|
||||
@RestrictTo(LIBRARY)
|
||||
protected abstract void clearProvisionalSelection();
|
||||
|
||||
/**
|
||||
* Converts the provisional selection into primary selection, then clears
|
||||
* provisional selection.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings("HiddenAbstractMethod")
|
||||
@RestrictTo(LIBRARY)
|
||||
protected abstract void mergeProvisionalSelection();
|
||||
|
||||
@@ -308,8 +307,6 @@ public abstract class SelectionTracker<K> {
|
||||
/**
|
||||
* Called when Selection is cleared.
|
||||
* TODO(smckay): Make public in a future public API.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(LIBRARY)
|
||||
protected void onSelectionCleared() {
|
||||
@@ -327,8 +324,7 @@ public abstract class SelectionTracker<K> {
|
||||
|
||||
/**
|
||||
* Called immediately after completion of any set of changes, excluding
|
||||
* those resulting in calls to {@link #onSelectionRefresh()} and
|
||||
* {@link #onSelectionRestored()}.
|
||||
* those resulting in calls {@link #onSelectionRestored()}.
|
||||
*/
|
||||
public void onSelectionChanged() {
|
||||
}
|
||||
@@ -385,59 +381,64 @@ public abstract class SelectionTracker<K> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder is the primary mechanism for create a {@link SelectionTracker} that
|
||||
* Builder is the primary mechanism for creating 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.
|
||||
* manipulate a selection of items in a RecyclerView instance using a variety of
|
||||
* intuitive techniques like tap, gesture, and mouse-based band selection (aka '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>
|
||||
* Building a bare-bones instance:
|
||||
*
|
||||
* <pre>{@code
|
||||
* SelectionTracker<Uri> tracker = new SelectionTracker.Builder<>(
|
||||
* "my-uri-selection",
|
||||
* recyclerView,
|
||||
* new YourItemKeyProvider(recyclerView.getAdapter()),
|
||||
* new YourItemDetailsLookup(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()}.
|
||||
* {@link SelectionPredicate} and
|
||||
* {@link SelectionTracker.Builder#withSelectionPredicate(SelectionPredicate)}
|
||||
* together provide a mechanism for restricting which items can be selected and
|
||||
* limiting selection size. Use {@link SelectionPredicates#createSelectSingleAnything()}
|
||||
* for single-selection, or write your own {@link SelectionPredicate} if other
|
||||
* constraints are required.
|
||||
*
|
||||
* <pre>{@code
|
||||
* SelectionTracker<String> tracker = new SelectionTracker.Builder<>(
|
||||
* "my-string-selection",
|
||||
* recyclerView,
|
||||
* new DemoStableIdProvider(recyclerView.getAdapter()),
|
||||
* new MyDetailsLookup(recyclerView),
|
||||
* StorageStrategy.createStringStorage())
|
||||
* .withSelectionPredicate(SelectionPredicates#createSelectSingleAnything())
|
||||
* .build();
|
||||
* </pre>
|
||||
* "my-string-selection",
|
||||
* recyclerView,
|
||||
* new YourItemKeyProvider(recyclerView.getAdapter()),
|
||||
* new YourItemDetailsLookup(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.
|
||||
* Failure to include support for selection storage will result in selection
|
||||
* being lost when the Activity receives a configuration change (e.g. rotation),
|
||||
* or when the application is paused or stopped. For this reason
|
||||
* {@link StorageStrategy} is a required argument to obtain a {@link Builder}
|
||||
* instance.
|
||||
*
|
||||
* <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}.
|
||||
* A developer 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
|
||||
@@ -450,30 +451,55 @@ public abstract class SelectionTracker<K> {
|
||||
* {@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.
|
||||
* {@link Long}: Use Long when a project is already employing RecyclerView's built-in
|
||||
* support for stable ids. In this case you may choose to use {@link StableIdKeyProvider}
|
||||
* to supply selection keys to the SelectionTracker based on data already accessible
|
||||
* in RecyclerView and it's Adapter.
|
||||
*
|
||||
* See {@link StableIdKeyProvider} for important details and limitations (<i>and a suggestion
|
||||
* that you might just want to write your own {@link ItemKeyProvider}. It's easy!</i>)
|
||||
* See the "Gotchas" selection below for details on selection size limits.
|
||||
*
|
||||
* <p>
|
||||
* Usage:
|
||||
*
|
||||
* <pre>
|
||||
* private SelectionTracker<Uri> mTracker;
|
||||
* <pre>{@code
|
||||
* private SelectionTracker<Uri> tracker;
|
||||
*
|
||||
* public void onCreate(Bundle savedInstanceState) {
|
||||
* // See above for details on constructing a SelectionTracker instance.
|
||||
*
|
||||
* if (savedInstanceState != null) {
|
||||
* mTracker.onRestoreInstanceState(savedInstanceState);
|
||||
* }
|
||||
* if (savedInstanceState != null) {
|
||||
* tracker.onRestoreInstanceState(savedInstanceState);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* protected void onSaveInstanceState(Bundle outState) {
|
||||
* super.onSaveInstanceState(outState);
|
||||
* mTracker.onSaveInstanceState(outState);
|
||||
* super.onSaveInstanceState(outState);
|
||||
* tracker.onSaveInstanceState(outState);
|
||||
* }
|
||||
* </pre>
|
||||
* }</pre>
|
||||
*
|
||||
* <p>
|
||||
* <b>Gotchas</b>
|
||||
*
|
||||
* <p>TransactionTooLargeException:
|
||||
*
|
||||
* <p>Many factors affect the maximum number of items that can be persisted when the
|
||||
* application is paused or stopped. Unfortunately that number is not deterministic as it
|
||||
* depends on the size of the key type used for selection, the number of selected items, and
|
||||
* external demand on system resources. For that reason it is best to use the smallest viable
|
||||
* key type, and to enforce a limit on the number of items that can be selected.
|
||||
*
|
||||
* <p>Furthermore the inability to persist a selection during a lifecycle event will result
|
||||
* in a android.os.{@link android.os.TransactionTooLargeException}. See
|
||||
* http://issuetracker.google.com/168706011 for details.
|
||||
*
|
||||
* <p>ItemTouchHelper
|
||||
*
|
||||
* <p>When using {@link SelectionTracker} along side an
|
||||
* {@link androidx.recyclerview.widget.ItemTouchHelper} with the same RecyclerView instance
|
||||
* the SelectionTracker instance must be created and installed before the ItemTouchHelper.
|
||||
* Failure to do so will result in unintended selections during item drag operations, and
|
||||
* possibly other situations.
|
||||
*
|
||||
* @param <K> Selection key type. Built in support is provided for {@link String},
|
||||
* {@link Long}, and {@link Parcelable}. {@link StorageStrategy}
|
||||
@@ -496,7 +522,7 @@ public abstract class SelectionTracker<K> {
|
||||
private ItemKeyProvider<K> mKeyProvider;
|
||||
private ItemDetailsLookup<K> mDetailsLookup;
|
||||
|
||||
private FocusDelegate<K> mFocusDelegate = FocusDelegate.dummy();
|
||||
private FocusDelegate<K> mFocusDelegate = FocusDelegate.stub();
|
||||
|
||||
private OnItemActivatedListener<K> mOnItemActivatedListener;
|
||||
private OnDragInitiatedListener mOnDragInitiatedListener;
|
||||
@@ -646,12 +672,11 @@ public abstract class SelectionTracker<K> {
|
||||
*
|
||||
* @param toolTypes the tool types to be used
|
||||
* @return this
|
||||
*
|
||||
* @deprecated GestureSelection is best bound to {@link MotionEvent#TOOL_TYPE_FINGER},
|
||||
* and only that tool type. This method will be removed in a future release.
|
||||
*/
|
||||
//@Deprecated
|
||||
public @NonNull Builder<K> withGestureTooltypes(@NonNull int... toolTypes) {
|
||||
public @NonNull Builder<K> withGestureTooltypes(int @NonNull ... toolTypes) {
|
||||
Log.w(TAG, "Setting gestureTooltypes is likely to result in unexpected behavior.");
|
||||
mGestureToolTypes = toolTypes;
|
||||
return this;
|
||||
@@ -685,12 +710,11 @@ public abstract class SelectionTracker<K> {
|
||||
*
|
||||
* @param toolTypes the tool types to be used
|
||||
* @return this
|
||||
*
|
||||
* @deprecated PointerSelection is best bound to {@link MotionEvent#TOOL_TYPE_MOUSE},
|
||||
* and only that tool type. This method will be removed in a future release.
|
||||
*/
|
||||
@Deprecated
|
||||
public @NonNull Builder<K> withPointerTooltypes(@NonNull int... toolTypes) {
|
||||
//@Deprecated
|
||||
public @NonNull Builder<K> withPointerTooltypes(int @NonNull ... toolTypes) {
|
||||
Log.w(TAG, "Setting pointerTooltypes is likely to result in unexpected behavior.");
|
||||
mPointerToolTypes = toolTypes;
|
||||
return this;
|
||||
@@ -709,7 +733,7 @@ public abstract class SelectionTracker<K> {
|
||||
// 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);
|
||||
EventBridge.install(mAdapter, tracker, mKeyProvider, mRecyclerView::post);
|
||||
|
||||
// Scroller is stateful and can be reset, but we don't manage it directly.
|
||||
// GestureSelectionHelper will reset scroller when it is reset.
|
||||
@@ -728,19 +752,29 @@ public abstract class SelectionTracker<K> {
|
||||
GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter);
|
||||
|
||||
// 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(
|
||||
// of motions and gestures in order to provide fluid "long-press and drag"
|
||||
// finger driven selection support.
|
||||
final GestureSelectionHelper gestureSelectionHelper = GestureSelectionHelper.create(
|
||||
tracker, mSelectionPredicate, mRecyclerView, scroller, mMonitor);
|
||||
|
||||
// EventRouter receives events for RecyclerView, dispatching to handlers
|
||||
// registered by tool-type.
|
||||
EventRouter eventRouter = new EventRouter();
|
||||
GestureDetectorWrapper gestureDetectorWrapper =
|
||||
new GestureDetectorWrapper(gestureDetector);
|
||||
|
||||
// Temp fix for b/166836317.
|
||||
// TODO: Add support for multiple listeners per tool type to EventRouter, then
|
||||
// register backstop with primary router.
|
||||
EventRouter backstopRouter = new EventRouter();
|
||||
EventBackstop backstop = new EventBackstop();
|
||||
DisallowInterceptFilter backstopWrapper = new DisallowInterceptFilter(backstop);
|
||||
backstopRouter.set(new ToolSourceKey(MotionEvent.TOOL_TYPE_UNKNOWN), backstopWrapper);
|
||||
|
||||
// Finally hook the framework up to listening to RecycleView events.
|
||||
mRecyclerView.addOnItemTouchListener(eventRouter);
|
||||
mRecyclerView.addOnItemTouchListener(
|
||||
new GestureDetectorOnItemTouchListenerAdapter(gestureDetector));
|
||||
mRecyclerView.addOnItemTouchListener(gestureDetectorWrapper);
|
||||
mRecyclerView.addOnItemTouchListener(backstopRouter);
|
||||
|
||||
// Reset manager listens for cancel events from RecyclerView. In response to that it
|
||||
// advises other classes it is time to reset state.
|
||||
@@ -750,20 +784,27 @@ public abstract class SelectionTracker<K> {
|
||||
//
|
||||
// 1. Monitor selection reset which can be invoked by clients in response
|
||||
// to back key press and some application lifecycle events.
|
||||
//
|
||||
// 2. Monitor ACTION_CANCEL events (which arrive exclusively
|
||||
// via TOOL_TYPE_UNKNOWN).
|
||||
tracker.addObserver(resetMgr.getSelectionObserver());
|
||||
|
||||
// ...and 2. Monitor ACTION_CANCEL events (which arrive exclusively
|
||||
// via TOOL_TYPE_UNKNOWN).
|
||||
//
|
||||
// CAUTION! Registering resetMgr directly with RecyclerView#addOnItemTouchListener
|
||||
// will not work as expected. Once EventRouter returns true, RecyclerView will
|
||||
// no longer dispatch any events to other listeners for the duration of the
|
||||
// stream, not even ACTION_CANCEL events.
|
||||
eventRouter.set(MotionEvent.TOOL_TYPE_UNKNOWN, resetMgr.getInputListener());
|
||||
eventRouter.set(new ToolSourceKey(MotionEvent.TOOL_TYPE_UNKNOWN),
|
||||
resetMgr.getInputListener());
|
||||
|
||||
// Finally register all of the Resettables.
|
||||
resetMgr.addResetHandler(tracker);
|
||||
resetMgr.addResetHandler(mMonitor.asResettable());
|
||||
resetMgr.addResetHandler(gestureHelper);
|
||||
resetMgr.addResetHandler(gestureSelectionHelper);
|
||||
resetMgr.addResetHandler(gestureDetectorWrapper);
|
||||
resetMgr.addResetHandler(eventRouter);
|
||||
resetMgr.addResetHandler(backstopRouter);
|
||||
resetMgr.addResetHandler(backstop);
|
||||
resetMgr.addResetHandler(backstopWrapper);
|
||||
|
||||
// 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.
|
||||
@@ -774,7 +815,7 @@ public abstract class SelectionTracker<K> {
|
||||
// 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.
|
||||
// initialize stub instances if the developer didn't supply a real listener.
|
||||
mOnDragInitiatedListener = (mOnDragInitiatedListener != null)
|
||||
? mOnDragInitiatedListener
|
||||
: new OnDragInitiatedListener() {
|
||||
@@ -789,7 +830,7 @@ public abstract class SelectionTracker<K> {
|
||||
: new OnItemActivatedListener<K>() {
|
||||
@Override
|
||||
public boolean onItemActivated(
|
||||
@NonNull ItemDetailsLookup.ItemDetails<K> item,
|
||||
ItemDetailsLookup.@NonNull ItemDetails<K> item,
|
||||
@NonNull MotionEvent e) {
|
||||
return false;
|
||||
}
|
||||
@@ -811,18 +852,7 @@ public abstract class SelectionTracker<K> {
|
||||
mKeyProvider,
|
||||
mDetailsLookup,
|
||||
mSelectionPredicate,
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mSelectionPredicate.canSelectMultiple()) {
|
||||
try {
|
||||
gestureHelper.start();
|
||||
} catch (Throwable ex) {
|
||||
eu.faircode.email.Log.e(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
gestureSelectionHelper::start,
|
||||
mOnDragInitiatedListener,
|
||||
mOnItemActivatedListener,
|
||||
mFocusDelegate,
|
||||
@@ -831,11 +861,14 @@ public abstract class SelectionTracker<K> {
|
||||
public void run() {
|
||||
mRecyclerView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||
}
|
||||
});
|
||||
},
|
||||
// Provide temporary glue to address b/166836317
|
||||
backstop::onLongPress);
|
||||
|
||||
for (int toolType : mGestureToolTypes) {
|
||||
gestureRouter.register(toolType, touchHandler);
|
||||
eventRouter.set(toolType, gestureHelper);
|
||||
ToolSourceKey key = new ToolSourceKey(toolType);
|
||||
gestureRouter.register(key, touchHandler);
|
||||
eventRouter.set(key, gestureSelectionHelper);
|
||||
}
|
||||
|
||||
// Provides high level glue for binding mouse events and gestures
|
||||
@@ -849,10 +882,14 @@ public abstract class SelectionTracker<K> {
|
||||
mFocusDelegate);
|
||||
|
||||
for (int toolType : mPointerToolTypes) {
|
||||
gestureRouter.register(toolType, mouseHandler);
|
||||
gestureRouter.register(new ToolSourceKey(toolType), mouseHandler);
|
||||
}
|
||||
|
||||
@Nullable BandSelectionHelper<K> bandHelper = null;
|
||||
ToolSourceKey touchpadKey = new ToolSourceKey(MotionEvent.TOOL_TYPE_FINGER,
|
||||
InputDevice.SOURCE_MOUSE);
|
||||
gestureRouter.register(touchpadKey, mouseHandler);
|
||||
|
||||
BandSelectionHelper<K> bandHelper = null;
|
||||
|
||||
// Band selection not supported in single select mode, or when key access
|
||||
// is limited to anything less than the entire corpus.
|
||||
@@ -880,7 +917,8 @@ public abstract class SelectionTracker<K> {
|
||||
OnItemTouchListener pointerEventHandler = new PointerDragEventInterceptor(
|
||||
mDetailsLookup, mOnDragInitiatedListener, bandHelper);
|
||||
|
||||
eventRouter.set(MotionEvent.TOOL_TYPE_MOUSE, pointerEventHandler);
|
||||
eventRouter.set(new ToolSourceKey(MotionEvent.TOOL_TYPE_MOUSE), pointerEventHandler);
|
||||
eventRouter.set(touchpadKey, pointerEventHandler);
|
||||
|
||||
return tracker;
|
||||
}
|
||||
|
||||
@@ -16,27 +16,44 @@
|
||||
|
||||
package androidx.recyclerview.selection;
|
||||
|
||||
import static androidx.core.util.Preconditions.checkArgument;
|
||||
import static androidx.core.util.Preconditions.checkNotNull;
|
||||
import static androidx.recyclerview.selection.Shared.DEBUG;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.collection.LongSparseArray;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;
|
||||
import androidx.recyclerview.widget.RecyclerView.RecyclerListener;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* An {@link ItemKeyProvider} that provides item keys by way of native
|
||||
* {@link RecyclerView.Adapter} stable ids.
|
||||
*
|
||||
* <p>The corresponding RecyclerView.Adapter instance must:
|
||||
* <ol>
|
||||
* <li> Enable stable ids using {@link RecyclerView.Adapter#setHasStableIds(boolean)}
|
||||
* <li> Override {@link RecyclerView.Adapter#getItemId(int)} with a real implementation.
|
||||
* </ol>
|
||||
*
|
||||
* <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).
|
||||
* There are trade-offs with this implementation:
|
||||
* <ul>
|
||||
* <li>It necessarily auto-boxes {@code long} stable id values into {@code Long} values for
|
||||
* use as selection keys.
|
||||
* <li>It deprives Chromebook users (actually, any device with an attached pointer) of support
|
||||
* for band-selection.
|
||||
* </ul>
|
||||
*
|
||||
* <p>See com.example.android.supportv7.widget.selection.fancy.DemoAdapter.KeyProvider in the
|
||||
* SupportV7 Demos package for an example of how to implement a better ItemKeyProvider.
|
||||
*/
|
||||
public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
|
||||
|
||||
@@ -44,7 +61,32 @@ public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
|
||||
|
||||
private final SparseArray<Long> mPositionToKey = new SparseArray<>();
|
||||
private final LongSparseArray<Integer> mKeyToPosition = new LongSparseArray<>();
|
||||
private final RecyclerView mRecyclerView;
|
||||
private final ViewHost mHost;
|
||||
|
||||
StableIdKeyProvider(@NonNull ViewHost host) {
|
||||
// Provider is based on the stable ids provided by ViewHolders which
|
||||
// are only accessible when the holders are attached or yet-to-be-recycled.
|
||||
// For that reason we can only satisfy "CACHED" scope key access which
|
||||
// limits library features such as mouse-driven band selection.
|
||||
super(SCOPE_CACHED);
|
||||
|
||||
checkNotNull(host);
|
||||
mHost = host;
|
||||
|
||||
mHost.registerLifecycleListener(
|
||||
new ViewHost.LifecycleListener() {
|
||||
@Override
|
||||
public void onAttached(@NonNull View view) {
|
||||
StableIdKeyProvider.this.onAttached(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecycled(@NonNull View view) {
|
||||
StableIdKeyProvider.this.onRecycled(view);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new key provider that uses cached {@code long} stable ids associated
|
||||
@@ -53,39 +95,23 @@ public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
|
||||
* @param recyclerView the owner RecyclerView
|
||||
*/
|
||||
public StableIdKeyProvider(@NonNull RecyclerView recyclerView) {
|
||||
this(new DefaultViewHost(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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Adapters used w/ StableIdKeyProvider MUST have StableIds enabled.
|
||||
checkArgument(recyclerView.getAdapter().hasStableIds(), "RecyclerView"
|
||||
+ ".Adapter#hasStableIds must return true.");
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
void onAttached(@NonNull View view) {
|
||||
RecyclerView.ViewHolder holder = mRecyclerView.findContainingViewHolder(view);
|
||||
ViewHolder holder = mHost.findViewHolder(view);
|
||||
if (holder == null) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Unable to find ViewHolder for View. Ignoring onAttached event.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
int position = holder.getAbsoluteAdapterPosition();
|
||||
int position = mHost.getPosition(holder);
|
||||
long id = holder.getItemId();
|
||||
if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) {
|
||||
mPositionToKey.put(position, id);
|
||||
@@ -94,15 +120,15 @@ public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
void onDetached(@NonNull View view) {
|
||||
RecyclerView.ViewHolder holder = mRecyclerView.findContainingViewHolder(view);
|
||||
void onRecycled(@NonNull View view) {
|
||||
ViewHolder holder = mHost.findViewHolder(view);
|
||||
if (holder == null) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Unable to find ViewHolder for View. Ignoring onDetached event.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
int position = holder.getAbsoluteAdapterPosition();
|
||||
int position = mHost.getPosition(holder);
|
||||
long id = holder.getItemId();
|
||||
if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) {
|
||||
mPositionToKey.delete(position);
|
||||
@@ -112,6 +138,8 @@ public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
|
||||
|
||||
@Override
|
||||
public @Nullable Long getKey(int position) {
|
||||
// TODO: Consider using RecyclerView.NO_ID for consistency w/ getPosition impl.
|
||||
// Currently GridModel impl depends on null return values.
|
||||
return mPositionToKey.get(position, null);
|
||||
}
|
||||
|
||||
@@ -119,4 +147,91 @@ public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
|
||||
public int getPosition(@NonNull Long key) {
|
||||
return mKeyToPosition.get(key, RecyclerView.NO_POSITION);
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper interface for RecyclerView allowing for easy unit testing.
|
||||
*/
|
||||
interface ViewHost {
|
||||
/** Registers View{Holder} lifecycle event listener. */
|
||||
void registerLifecycleListener(@NonNull LifecycleListener listener);
|
||||
|
||||
/**
|
||||
* Returns the ViewHolder containing {@code View}.
|
||||
*/
|
||||
@Nullable ViewHolder findViewHolder(@NonNull View view);
|
||||
|
||||
/**
|
||||
* Returns the position of the ViewHolder, or RecyclerView.NO_POSITION
|
||||
* if unknown.
|
||||
*
|
||||
* This method supports testing of StableIdKeyProvider independent of
|
||||
* a real RecyclerView instance. The correct runtime implementation is
|
||||
* {@code return holder.getAbsoluteAdapterPosition}. This implementation
|
||||
* depends on a concrete RecyclerView instance, which isn't test friendly
|
||||
* given the testing approach in StableIdKeyProviderTest. Thus the
|
||||
* introduction of this interface method allowing a test double to
|
||||
* supply adapter position as needed to test.
|
||||
*/
|
||||
int getPosition(@NonNull ViewHolder holder);
|
||||
|
||||
/** A View{Holder} lifecycle listener interface. */
|
||||
interface LifecycleListener {
|
||||
|
||||
/** Called when view is attached. */
|
||||
void onAttached(@NonNull View view);
|
||||
|
||||
/** Called when view is recycled. */
|
||||
void onRecycled(@NonNull View view);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of ViewHost that wraps a RecyclerView instance.
|
||||
*/
|
||||
private static class DefaultViewHost implements ViewHost {
|
||||
private final @NonNull RecyclerView mRecyclerView;
|
||||
|
||||
DefaultViewHost(@NonNull RecyclerView recyclerView) {
|
||||
checkNotNull(recyclerView);
|
||||
mRecyclerView = recyclerView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerLifecycleListener(@NonNull LifecycleListener listener) {
|
||||
|
||||
mRecyclerView.addOnChildAttachStateChangeListener(
|
||||
new OnChildAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onChildViewAttachedToWindow(@NonNull View view) {
|
||||
listener.onAttached(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildViewDetachedFromWindow(@NonNull View view) {
|
||||
// Cached position <> key data is discarded only when
|
||||
// a view is recycled. See b/145767095 for details.
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
mRecyclerView.addRecyclerListener(
|
||||
new RecyclerListener() {
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull ViewHolder holder) {
|
||||
listener.onRecycled(holder.itemView);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ViewHolder findViewHolder(@NonNull View view) {
|
||||
return mRecyclerView.findContainingViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPosition(@NonNull ViewHolder holder) {
|
||||
return holder.getAbsoluteAdapterPosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,11 @@ 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 org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
@@ -87,7 +88,7 @@ public abstract class StorageStrategy<K> {
|
||||
* @return StorageStrategy suitable for use with {@link Parcelable} keys
|
||||
* (like {@link android.net.Uri}).
|
||||
*/
|
||||
public static @NonNull <K extends Parcelable> StorageStrategy<K> createParcelableStorage(
|
||||
public static <K extends Parcelable> @NonNull StorageStrategy<K> createParcelableStorage(
|
||||
@NonNull Class<K> type) {
|
||||
return new ParcelableStorageStrategy<>(type);
|
||||
}
|
||||
@@ -120,7 +121,7 @@ public abstract class StorageStrategy<K> {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable ArrayList<String> stored = state.getStringArrayList(SELECTION_ENTRIES);
|
||||
ArrayList<String> stored = state.getStringArrayList(SELECTION_ENTRIES);
|
||||
if (stored == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -158,7 +159,7 @@ public abstract class StorageStrategy<K> {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable long[] stored = state.getLongArray(SELECTION_ENTRIES);
|
||||
long[] stored = state.getLongArray(SELECTION_ENTRIES);
|
||||
if (stored == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -196,6 +197,7 @@ public abstract class StorageStrategy<K> {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public @Nullable Selection<K> asSelection(@NonNull Bundle state) {
|
||||
|
||||
String keyType = state.getString(SELECTION_KEY_TYPE, null);
|
||||
@@ -203,7 +205,7 @@ public abstract class StorageStrategy<K> {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable ArrayList<K> stored = state.getParcelableArrayList(SELECTION_ENTRIES);
|
||||
ArrayList<K> stored = state.getParcelableArrayList(SELECTION_ENTRIES);
|
||||
if (stored == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -18,14 +18,15 @@ package androidx.recyclerview.selection;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* No-op implementation of OnItemTouchListener suitable for use as a default
|
||||
* handler w/ ToolHandlerRegistery, or in tests.
|
||||
*/
|
||||
final class DummyOnItemTouchListener implements RecyclerView.OnItemTouchListener {
|
||||
final class StubOnItemTouchListener implements RecyclerView.OnItemTouchListener {
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(
|
||||
@NonNull RecyclerView unused, @NonNull MotionEvent e) {
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* 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 were a Map used.k
|
||||
*
|
||||
* <p>ToolHandlerRegistry guarantees that it will never return a null handler ensuring
|
||||
* client code isn't peppered with null checks. To that end a default handler
|
||||
* is required. This default handler will be returned when a handler matching
|
||||
* the event tooltype has not be registered using {@link #set(int, T)}.
|
||||
*
|
||||
* @param <T> type of item being registered.
|
||||
*/
|
||||
final class ToolHandlerRegistry<T> {
|
||||
|
||||
// list with one null entry for each known tooltype (0-4).
|
||||
// See MotionEvent.TOOL_TYPE_ERASER for details. We're using a list here because
|
||||
// it is parameterized type friendly, and a natural container given that
|
||||
// the index values are 0-based ints.
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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,82 @@
|
||||
/*
|
||||
* 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 org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Registry for event handlers. This is keyed by a ToolSourceKey which allows for searching for a
|
||||
* handler which handles both a specific SOURCE + TOOL and if none exists, will fall back to
|
||||
* TOOL only handlers. If none exists, a default handler will be returned instead.
|
||||
*
|
||||
* <p>ToolHandlerRegistry guarantees that it will never return a null handler ensuring
|
||||
* client code isn't peppered with null checks. To that end a default handler
|
||||
* is required. This default handler will be returned when a handler matching
|
||||
* the event ToolSourceKey has not be registered using
|
||||
* {@link ToolSourceHandlerRegistry#set(ToolSourceKey, T)}.
|
||||
*
|
||||
* @param <T> type of item being registered.
|
||||
*/
|
||||
final class ToolSourceHandlerRegistry<T> {
|
||||
|
||||
/**
|
||||
* A map that is keyed by a ToolSourceKey which contains either a TOOL + SOURCE or just a
|
||||
* TOOL. This allows for handlers to get routed to more specific handlers before falling back
|
||||
* to less specific and finally the default one.
|
||||
*/
|
||||
|
||||
private final Map<ToolSourceKey, T> mHandlers = new HashMap<ToolSourceKey, T>();
|
||||
|
||||
private final T mDefault;
|
||||
|
||||
ToolSourceHandlerRegistry(@NonNull T defaultDelegate) {
|
||||
checkArgument(defaultDelegate != null);
|
||||
mDefault = defaultDelegate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param delegate the delegate, or null to unregister.
|
||||
* @throws IllegalStateException if a key already has a registered handler.
|
||||
*/
|
||||
void set(@NonNull ToolSourceKey key, @Nullable T delegate) {
|
||||
if (delegate == null && mHandlers.containsKey(key)) {
|
||||
mHandlers.remove(key);
|
||||
return;
|
||||
}
|
||||
|
||||
mHandlers.put(key, delegate);
|
||||
}
|
||||
|
||||
T get(@NonNull MotionEvent e) {
|
||||
ToolSourceKey key = ToolSourceKey.fromMotionEvent(e);
|
||||
T d = mHandlers.get(key);
|
||||
if (d == null) {
|
||||
// If the map of handlers doesn't contain a specific MotionEventKey(tool, source)
|
||||
// then fallback to the less specific MotionEventKey(tool).
|
||||
d = mHandlers.get(new ToolSourceKey(key.getToolType()));
|
||||
}
|
||||
return d != null ? d : mDefault;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2024 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 android.view.InputDevice.SOURCE_MOUSE;
|
||||
import static android.view.InputDevice.SOURCE_UNKNOWN;
|
||||
import static android.view.MotionEvent.TOOL_TYPE_ERASER;
|
||||
import static android.view.MotionEvent.TOOL_TYPE_FINGER;
|
||||
import static android.view.MotionEvent.TOOL_TYPE_MOUSE;
|
||||
import static android.view.MotionEvent.TOOL_TYPE_STYLUS;
|
||||
import static android.view.MotionEvent.TOOL_TYPE_UNKNOWN;
|
||||
|
||||
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
|
||||
|
||||
import android.view.InputDevice;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.RestrictTo;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Enables storing multiple {@link MotionEvent} parameters (e.g.
|
||||
* {@link MotionEvent#getToolType(int)}) as a key in a map. This opens up the ability to map
|
||||
* these multiple parameters against their respective handlers. For example some events behave
|
||||
* differently based on their toolType and source where others just require toolType.
|
||||
*/
|
||||
@RestrictTo(LIBRARY)
|
||||
public class ToolSourceKey {
|
||||
private final @ToolType int mToolType;
|
||||
private final @Source int mSource;
|
||||
|
||||
ToolSourceKey(@ToolType int toolType) {
|
||||
mToolType = toolType;
|
||||
mSource = InputDevice.SOURCE_UNKNOWN;
|
||||
}
|
||||
|
||||
ToolSourceKey(@ToolType int toolType, @Source int source) {
|
||||
mToolType = toolType;
|
||||
mSource = source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `ToolSourceKey` from a supplied `MotionEvent`.
|
||||
*
|
||||
* @return {@link ToolSourceKey}
|
||||
*/
|
||||
public static @NonNull ToolSourceKey fromMotionEvent(@NonNull MotionEvent e) {
|
||||
return new ToolSourceKey(e.getToolType(0), e.getSource());
|
||||
}
|
||||
|
||||
public int getToolType() {
|
||||
return mToolType;
|
||||
}
|
||||
|
||||
public int getSource() {
|
||||
return mSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(mToolType, mSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof ToolSourceKey)) {
|
||||
return false;
|
||||
}
|
||||
ToolSourceKey matcher = (ToolSourceKey) obj;
|
||||
return mToolType == matcher.getToolType() && mSource == matcher.getSource();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.valueOf(mToolType) + "," + String.valueOf(mSource);
|
||||
}
|
||||
|
||||
@IntDef(value = {TOOL_TYPE_FINGER, TOOL_TYPE_MOUSE, TOOL_TYPE_ERASER, TOOL_TYPE_STYLUS,
|
||||
TOOL_TYPE_UNKNOWN})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface ToolType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Please add additional sources here from InputDevice.SOURCE_*.
|
||||
*/
|
||||
@IntDef(value = {SOURCE_MOUSE, SOURCE_UNKNOWN})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface Source {
|
||||
}
|
||||
}
|
||||
@@ -22,11 +22,12 @@ import static androidx.recyclerview.selection.Shared.DEBUG;
|
||||
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;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* A MotionInputHandler that provides the high-level glue for touch driven selection. This class
|
||||
* works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper} to
|
||||
@@ -36,7 +37,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
*/
|
||||
final class TouchInputHandler<K> extends MotionInputHandler<K> {
|
||||
|
||||
private static final String TAG = "TouchInputDelegate";
|
||||
private static final String TAG = "TouchInputHandler";
|
||||
|
||||
private final ItemDetailsLookup<K> mDetailsLookup;
|
||||
private final SelectionPredicate<K> mSelectionPredicate;
|
||||
@@ -44,6 +45,7 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
|
||||
private final OnDragInitiatedListener mOnDragInitiatedListener;
|
||||
private final Runnable mGestureStarter;
|
||||
private final Runnable mHapticPerformer;
|
||||
private final Runnable mLongPressCallback;
|
||||
|
||||
TouchInputHandler(
|
||||
@NonNull SelectionTracker<K> selectionTracker,
|
||||
@@ -54,7 +56,8 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
|
||||
@NonNull OnDragInitiatedListener onDragInitiatedListener,
|
||||
@NonNull OnItemActivatedListener<K> onItemActivatedListener,
|
||||
@NonNull FocusDelegate<K> focusDelegate,
|
||||
@NonNull Runnable hapticPerformer) {
|
||||
@NonNull Runnable hapticPerformer,
|
||||
@NonNull Runnable longPressCallback) {
|
||||
|
||||
super(selectionTracker, keyProvider, focusDelegate);
|
||||
|
||||
@@ -71,6 +74,7 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
|
||||
mOnItemActivatedListener = onItemActivatedListener;
|
||||
mOnDragInitiatedListener = onDragInitiatedListener;
|
||||
mHapticPerformer = hapticPerformer;
|
||||
mLongPressCallback = longPressCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,16 +84,10 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
|
||||
checkArgument(MotionEvents.isActionUp(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 (item == null || !item.hasSelectionKey()) {
|
||||
if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection.");
|
||||
return mSelectionTracker.clearSelection();
|
||||
}
|
||||
|
||||
if (mSelectionTracker.hasSelection()) {
|
||||
@@ -111,6 +109,25 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
|
||||
: mOnItemActivatedListener.onItemActivated(item, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTapEvent(MotionEvent e) {
|
||||
// Reinterpret an UP event in the double tap event stream as a singleTapUp.
|
||||
//
|
||||
// Background: GestureRouter is both an OnGestureListener and a OnDoubleTapListener,
|
||||
// which allows it to dispatch events based on tooltype to the Touch or Mouse
|
||||
// input handler.
|
||||
//
|
||||
// Turns out the act of instantiating GestureDetector with an OnDoubleTapListener
|
||||
// signals to it that we want onDoubleTap events rather than a series of individual
|
||||
// onSingleTapUp events, resulting in some touch input being mishandled
|
||||
// by TouchInputHandler. See b/161162268 for some supporting details.
|
||||
//
|
||||
// There are a variety of ways to work around this. Given long term plans
|
||||
// to replace GestureDetector (b/159025478), we'll just reroute
|
||||
// the second UP event to the onSingleTapUp handler.
|
||||
return MotionEvents.isActionUp(e) && onSingleTapUp(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(@NonNull MotionEvent e) {
|
||||
if (DEBUG) {
|
||||
@@ -129,26 +146,31 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Temprary fix to address b/166836317.
|
||||
mLongPressCallback.run();
|
||||
|
||||
if (shouldExtendRange(e)) {
|
||||
extendSelectionRange(item);
|
||||
mHapticPerformer.run();
|
||||
} else {
|
||||
if (mSelectionTracker.isSelected(item.getSelectionKey())) {
|
||||
// Long press on existing selected item initiates drag/drop.
|
||||
mOnDragInitiatedListener.onDragInitiated(e);
|
||||
mHapticPerformer.run();
|
||||
} else if (mSelectionPredicate.canSetStateForKey(item.getSelectionKey(), true)
|
||||
&& selectItem(item)) {
|
||||
// And finally if the item was selected && we can select multiple
|
||||
// we kick off gesture selection.
|
||||
// NOTE: isRangeActive should ALWAYS be true at this point, but there have
|
||||
// been reports indicating that assumption isn't correct. So we explicitly
|
||||
// check isRangeActive.
|
||||
if (mSelectionPredicate.canSelectMultiple() && mSelectionTracker.isRangeActive()) {
|
||||
mGestureStarter.run();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mSelectionTracker.isSelected(item.getSelectionKey())) {
|
||||
// Long press on existing selected item initiates drag/drop.
|
||||
if (mOnDragInitiatedListener.onDragInitiated(e)) {
|
||||
mHapticPerformer.run();
|
||||
}
|
||||
} else if (mSelectionPredicate.canSetStateForKey(item.getSelectionKey(), true)
|
||||
&& selectItem(item)) {
|
||||
// And finally if the item was selected && we can select multiple
|
||||
// we kick off gesture selection.
|
||||
// NOTE: isRangeActive should ALWAYS be true at this point, but there have
|
||||
// been reports indicating that assumption isn't correct. So we explicitly
|
||||
// check isRangeActive.
|
||||
if (mSelectionPredicate.canSelectMultiple() && mSelectionTracker.isRangeActive()) {
|
||||
mGestureStarter.run();
|
||||
}
|
||||
mHapticPerformer.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,13 @@ import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
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;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Provides auto-scrolling upon request when user's interaction with the application
|
||||
* introduces a natural intent to scroll. Used by BandSelectionHelper and GestureSelectionHelper,
|
||||
|
||||
149
patches/SelectionTracker_1.2.0.patch
Normal file
149
patches/SelectionTracker_1.2.0.patch
Normal file
@@ -0,0 +1,149 @@
|
||||
diff --git b/app/src/main/java/androidx/recyclerview/selection/DefaultSelectionTracker.java a/app/src/main/java/androidx/recyclerview/selection/DefaultSelectionTracker.java
|
||||
index 88418ace1d..ffc0f8736a 100644
|
||||
--- b/app/src/main/java/androidx/recyclerview/selection/DefaultSelectionTracker.java
|
||||
+++ a/app/src/main/java/androidx/recyclerview/selection/DefaultSelectionTracker.java
|
||||
@@ -379,6 +379,10 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
|
||||
return mRange != null;
|
||||
}
|
||||
|
||||
+ boolean isOverlapping(int position, int count) {
|
||||
+ return (mRange != null && mRange.isOverlapping(position, count));
|
||||
+ }
|
||||
+
|
||||
private boolean canSetState(@NonNull K key, boolean nextState) {
|
||||
return mSelectionPredicate.canSetStateForKey(key, nextState);
|
||||
}
|
||||
@@ -395,7 +399,7 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
|
||||
return;
|
||||
}
|
||||
|
||||
- mSelection.clearProvisionalSelection();
|
||||
+ //mSelection.clearProvisionalSelection();
|
||||
|
||||
notifySelectionRefresh();
|
||||
|
||||
@@ -611,12 +615,14 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(int startPosition, int itemCount) {
|
||||
- mSelectionTracker.endRange();
|
||||
+ if (mSelectionTracker.isOverlapping(startPosition, itemCount))
|
||||
+ mSelectionTracker.endRange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeRemoved(int startPosition, int itemCount) {
|
||||
- mSelectionTracker.endRange();
|
||||
+ if (mSelectionTracker.isOverlapping(startPosition, itemCount))
|
||||
+ mSelectionTracker.endRange();
|
||||
// Since SelectionTracker deals in keys, not positions, we turn
|
||||
// to the `onDataSetChanged` sledge hammer.
|
||||
// DefaultSelectionTracker will validate and update it's selection.
|
||||
@@ -625,7 +631,9 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
|
||||
|
||||
@Override
|
||||
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
|
||||
- mSelectionTracker.endRange();
|
||||
+ if (mSelectionTracker.isOverlapping(fromPosition, itemCount) ||
|
||||
+ mSelectionTracker.isOverlapping(toPosition, itemCount))
|
||||
+ mSelectionTracker.endRange();
|
||||
}
|
||||
}
|
||||
}
|
||||
diff --git b/app/src/main/java/androidx/recyclerview/selection/GestureRouter.java a/app/src/main/java/androidx/recyclerview/selection/GestureRouter.java
|
||||
index 9f6068f7bf..72e5c948cd 100644
|
||||
--- b/app/src/main/java/androidx/recyclerview/selection/GestureRouter.java
|
||||
+++ a/app/src/main/java/androidx/recyclerview/selection/GestureRouter.java
|
||||
@@ -94,7 +94,28 @@ final class GestureRouter<T extends OnGestureListener & OnDoubleTapListener>
|
||||
|
||||
@Override
|
||||
public void onLongPress(@NonNull MotionEvent e) {
|
||||
- mDelegates.get(e).onLongPress(e);
|
||||
+ try {
|
||||
+ mDelegates.get(e).onLongPress(e);
|
||||
+ } catch (Throwable ex) {
|
||||
+ eu.faircode.email.Log.w(ex);
|
||||
+ /*
|
||||
+ java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling eu.faircode.email.FixedRecyclerView{239c688b VFED.... ........ 0,0-800,1162 #7f0a04da app:id/rvMessage}, adapter:eu.faircode.email.AdapterMessage@209415c5, layout:eu.faircode.email.FragmentMessages$7@190d7b1a, context:eu.faircode.email.ActivityView@3e8522fb
|
||||
+ at androidx.recyclerview.widget.RecyclerView.assertNotInLayoutOrScroll(SourceFile:3)
|
||||
+ at androidx.recyclerview.widget.RecyclerView$RecyclerViewDataObserver.onItemRangeChanged(SourceFile:1)
|
||||
+ at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyItemRangeChanged(SourceFile:3)
|
||||
+ at androidx.recyclerview.widget.RecyclerView$Adapter.notifyItemChanged(SourceFile:2)
|
||||
+ at androidx.recyclerview.selection.EventBridge$TrackerToAdapterBridge.onItemStateChanged(SourceFile:3)
|
||||
+ at androidx.recyclerview.selection.DefaultSelectionTracker.notifyItemStateChanged(SourceFile:3)
|
||||
+ at androidx.recyclerview.selection.DefaultSelectionTracker.select(SourceFile:8)
|
||||
+ at androidx.recyclerview.selection.MotionInputHandler.selectItem(SourceFile:4)
|
||||
+ at androidx.recyclerview.selection.TouchInputHandler.onLongPress(SourceFile:10)
|
||||
+ at androidx.recyclerview.selection.GestureRouter.onLongPress(SourceFile:1)
|
||||
+ at android.view.GestureDetector.dispatchLongPress(GestureDetector.java:700)
|
||||
+ at android.view.GestureDetector.access$200(GestureDetector.java:40)
|
||||
+ at android.view.GestureDetector$GestureHandler.handleMessage(GestureDetector.java:273)
|
||||
+ at android.os.Handler.dispatchMessage(Handler.java:102)
|
||||
+ */
|
||||
+ }
|
||||
}
|
||||
|
||||
@Override
|
||||
diff --git b/app/src/main/java/androidx/recyclerview/selection/Range.java a/app/src/main/java/androidx/recyclerview/selection/Range.java
|
||||
index 6a53b1f4fc..dc372bad93 100644
|
||||
--- b/app/src/main/java/androidx/recyclerview/selection/Range.java
|
||||
+++ a/app/src/main/java/androidx/recyclerview/selection/Range.java
|
||||
@@ -170,6 +170,11 @@ final class Range {
|
||||
mCallbacks.updateForRange(begin, end, selected, type);
|
||||
}
|
||||
|
||||
+ boolean isOverlapping(int position, int count) {
|
||||
+ return (position >= mBegin && position <= mEnd) ||
|
||||
+ (position + count >= mBegin && position + count <= mEnd);
|
||||
+ }
|
||||
+
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Range{begin=" + mBegin + ", end=" + mEnd + "}";
|
||||
diff --git b/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java a/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
|
||||
index 41ddefa9b1..8c86d4adbc 100644
|
||||
--- b/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
|
||||
+++ a/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
|
||||
@@ -529,7 +529,7 @@ public abstract class SelectionTracker<K> {
|
||||
private OnContextClickListener mOnContextClickListener;
|
||||
|
||||
private BandPredicate mBandPredicate;
|
||||
- private int mBandOverlayId = R.drawable.selection_band_overlay;
|
||||
+ private int mBandOverlayId = eu.faircode.email.R.drawable.selection_band_overlay;
|
||||
|
||||
// TODO(b/144500333): Remove support for overriding gesture and pointer tooltypes.
|
||||
private int[] mGestureToolTypes = new int[]{
|
||||
@@ -675,7 +675,7 @@ public abstract class SelectionTracker<K> {
|
||||
* @deprecated GestureSelection is best bound to {@link MotionEvent#TOOL_TYPE_FINGER},
|
||||
* and only that tool type. This method will be removed in a future release.
|
||||
*/
|
||||
- @Deprecated
|
||||
+ //@Deprecated
|
||||
public @NonNull Builder<K> withGestureTooltypes(int @NonNull ... toolTypes) {
|
||||
Log.w(TAG, "Setting gestureTooltypes is likely to result in unexpected behavior.");
|
||||
mGestureToolTypes = toolTypes;
|
||||
@@ -713,7 +713,7 @@ public abstract class SelectionTracker<K> {
|
||||
* @deprecated PointerSelection is best bound to {@link MotionEvent#TOOL_TYPE_MOUSE},
|
||||
* and only that tool type. This method will be removed in a future release.
|
||||
*/
|
||||
- @Deprecated
|
||||
+ //@Deprecated
|
||||
public @NonNull Builder<K> withPointerTooltypes(int @NonNull ... toolTypes) {
|
||||
Log.w(TAG, "Setting pointerTooltypes is likely to result in unexpected behavior.");
|
||||
mPointerToolTypes = toolTypes;
|
||||
diff --git b/app/src/main/java/androidx/recyclerview/selection/ViewAutoScroller.java a/app/src/main/java/androidx/recyclerview/selection/ViewAutoScroller.java
|
||||
index 4701bef30c..7b6551f8e2 100644
|
||||
--- b/app/src/main/java/androidx/recyclerview/selection/ViewAutoScroller.java
|
||||
+++ a/app/src/main/java/androidx/recyclerview/selection/ViewAutoScroller.java
|
||||
@@ -108,6 +108,11 @@ final class ViewAutoScroller extends AutoScroller {
|
||||
|
||||
if (VERBOSE) Log.v(TAG, "Running in background using event location @ " + mLastLocation);
|
||||
|
||||
+ if (mLastLocation == null) {
|
||||
+ eu.faircode.email.Log.w("ViewAutoScroller.mLastLocation is null");
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
// 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
|
||||
Reference in New Issue
Block a user