Updated AndroidX

This commit is contained in:
M66B 2019-12-08 19:33:02 +01:00
parent 2c46f3d9ff
commit 8c555c684d
29 changed files with 785 additions and 440 deletions

View File

@ -181,17 +181,18 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
def core_version = "1.2.0-rc01" def core_version = "1.2.0-rc01"
def appcompat_version = "1.1.0" def appcompat_version = "1.2.0-alpha01"
def fragment_version = "1.2.0-rc02" def fragment_version = "1.2.0-rc03"
def recyclerview_version = "1.1.0" def recyclerview_version = "1.1.0"
def coordinatorlayout_version = "1.1.0-rc01" def coordinatorlayout_version = "1.1.0"
def constraintlayout_version = "2.0.0-beta3" def constraintlayout_version = "2.0.0-beta3"
def material_version = "1.2.0-alpha02" def material_version = "1.2.0-alpha02"
def browser_version = "1.2.0-beta01" def browser_version = "1.2.0-rc01"
def lbm_version = "1.0.0" def lbm_version = "1.0.0"
def swiperefresh_version = "1.0.0" def swiperefresh_version = "1.0.0"
def documentfile_version = "1.0.1" def documentfile_version = "1.0.1"
def lifecycle_version = "2.2.0-rc02" def lifecycle_version = "2.2.0-rc03"
def sqlite_version = "2.1.0-beta01"
def room_version = "2.2.2" def room_version = "2.2.2"
def paging_version = "2.1.0" def paging_version = "2.1.0"
def preference_version = "1.1.0" def preference_version = "1.1.0"
@ -226,7 +227,7 @@ dependencies {
// https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview // https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview
// https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview-selection // https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview-selection
implementation "androidx.recyclerview:recyclerview:$recyclerview_version" implementation "androidx.recyclerview:recyclerview:$recyclerview_version"
//implementation "androidx.recyclerview:recyclerview-selection:1.1.0-alpha06" //implementation "androidx.recyclerview:recyclerview-selection:1.1.0-beta01"
// https://mvnrepository.com/artifact/androidx.coordinatorlayout/coordinatorlayout // https://mvnrepository.com/artifact/androidx.coordinatorlayout/coordinatorlayout
implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorlayout_version" implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorlayout_version"
@ -260,7 +261,7 @@ dependencies {
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-common:$room_version" // because of exclude implementation "androidx.room:room-common:$room_version" // because of exclude
// https://mvnrepository.com/artifact/androidx.sqlite/sqlite-framework // https://mvnrepository.com/artifact/androidx.sqlite/sqlite-framework
implementation "androidx.sqlite:sqlite-framework:2.1.0-alpha01" // because of exclude implementation "androidx.sqlite:sqlite-framework:$sqlite_version" // because of exclude
annotationProcessor "androidx.room:room-compiler:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version"
// https://mvnrepository.com/artifact/androidx.paging/paging-runtime // https://mvnrepository.com/artifact/androidx.paging/paging-runtime

View File

@ -40,7 +40,7 @@ public abstract class BandPredicate {
/** /**
* @return true if band selection can be initiated in response to the {@link MotionEvent}. * @return true if band selection can be initiated in response to the {@link MotionEvent}.
*/ */
public abstract boolean canInitiate(MotionEvent e); public abstract boolean canInitiate(@NonNull MotionEvent e);
@SuppressWarnings("WeakerAccess") /* synthetic access */ @SuppressWarnings("WeakerAccess") /* synthetic access */
static boolean hasSupportedLayoutManager(@NonNull RecyclerView recyclerView) { static boolean hasSupportedLayoutManager(@NonNull RecyclerView recyclerView) {
@ -107,16 +107,16 @@ public abstract class BandPredicate {
public static final class NonDraggableArea extends BandPredicate { public static final class NonDraggableArea extends BandPredicate {
private final RecyclerView mRecyclerView; private final RecyclerView mRecyclerView;
private final ItemDetailsLookup mDetailsLookup; private final ItemDetailsLookup<?> mDetailsLookup;
/** /**
* Creates a new instance. * Creates a new instance.
* *
* @param recyclerView the owner RecyclerView * @param recyclerView the owner RecyclerView
* @param detailsLookup provides access to item details. * @param detailsLookup provides access to item details.
*/ */
public NonDraggableArea( public NonDraggableArea(
@NonNull RecyclerView recyclerView, @NonNull ItemDetailsLookup detailsLookup) { @NonNull RecyclerView recyclerView, @NonNull ItemDetailsLookup<?> detailsLookup) {
checkArgument(recyclerView != null); checkArgument(recyclerView != null);
checkArgument(detailsLookup != null); checkArgument(detailsLookup != null);
@ -132,7 +132,8 @@ public abstract class BandPredicate {
return false; return false;
} }
@Nullable ItemDetailsLookup.ItemDetails details = mDetailsLookup.getItemDetails(e); @Nullable ItemDetailsLookup.ItemDetails<?> details =
mDetailsLookup.getItemDetails(e);
return (details == null) || !details.inDragRegion(e); return (details == null) || !details.inDragRegion(e);
} }
} }

View File

@ -28,7 +28,6 @@ import android.view.MotionEvent;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate; import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
@ -48,17 +47,14 @@ import java.util.Set;
* the user interacts with items using their pointer (and the band). Selectable items that intersect * the user interacts with items using their pointer (and the band). Selectable items that intersect
* with the band, both on and off screen, are selected on pointer up. * with the band, both on and off screen, are selected on pointer up.
* *
* @see SelectionTracker.Builder#withPointerTooltypes(int...) for details on the specific
* tooltypes routed to this helper.
*
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types. * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*/ */
class BandSelectionHelper<K> implements OnItemTouchListener { class BandSelectionHelper<K> implements OnItemTouchListener, Resettable {
static final String TAG = "BandSelectionHelper"; static final String TAG = "BandSelectionHelper";
static final boolean DEBUG = false; static final boolean DEBUG = false;
private final BandHost mHost; private final BandHost<K> mHost;
private final ItemKeyProvider<K> mKeyProvider; private final ItemKeyProvider<K> mKeyProvider;
@SuppressWarnings("WeakerAccess") /* synthetic access */ @SuppressWarnings("WeakerAccess") /* synthetic access */
final SelectionTracker<K> mSelectionTracker; final SelectionTracker<K> mSelectionTracker;
@ -66,17 +62,17 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
private final FocusDelegate<K> mFocusDelegate; private final FocusDelegate<K> mFocusDelegate;
private final OperationMonitor mLock; private final OperationMonitor mLock;
private final AutoScroller mScroller; private final AutoScroller mScroller;
private final GridModel.SelectionObserver mGridObserver; private final GridModel.SelectionObserver<K> mGridObserver;
private @Nullable Point mCurrentPosition; private @Nullable Point mCurrentPosition;
private @Nullable Point mOrigin; private @Nullable Point mOrigin;
private @Nullable GridModel mModel; private @Nullable GridModel<K> mModel;
/** /**
* See {@link BandSelectionHelper#create}. * See {@link BandSelectionHelper#create}.
*/ */
BandSelectionHelper( BandSelectionHelper(
@NonNull BandHost host, @NonNull BandHost<K> host,
@NonNull AutoScroller scroller, @NonNull AutoScroller scroller,
@NonNull ItemKeyProvider<K> keyProvider, @NonNull ItemKeyProvider<K> keyProvider,
@NonNull SelectionTracker<K> selectionTracker, @NonNull SelectionTracker<K> selectionTracker,
@ -122,7 +118,7 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
* *
* @return new BandSelectionHelper instance. * @return new BandSelectionHelper instance.
*/ */
static <K> BandSelectionHelper create( static <K> BandSelectionHelper<K> create(
@NonNull RecyclerView recyclerView, @NonNull RecyclerView recyclerView,
@NonNull AutoScroller scroller, @NonNull AutoScroller scroller,
@DrawableRes int bandOverlayId, @DrawableRes int bandOverlayId,
@ -143,24 +139,23 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
lock); lock);
} }
@VisibleForTesting private boolean isActive() {
boolean isActive() { boolean started = mModel != null;
boolean active = mModel != null; if (DEBUG) mLock.checkStarted(started);
if (DEBUG && active) { return started;
mLock.checkStarted();
}
return active;
} }
/** /**
* Clients must call reset when there are any material changes to the layout of items * Clients must call reset when there are any material changes to the layout of items
* in RecyclerView. * in RecyclerView.
*/ */
void reset() { @Override
public void reset() {
if (!isActive()) { if (!isActive()) {
if (DEBUG) Log.d(TAG, "Ignoring reset request, not active.");
return; return;
} }
if (DEBUG) Log.d(TAG, "Handling reset request.");
mHost.hideBand(); mHost.hideBand();
if (mModel != null) { if (mModel != null) {
mModel.stopCapturing(); mModel.stopCapturing();
@ -171,11 +166,15 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
mOrigin = null; mOrigin = null;
mScroller.reset(); mScroller.reset();
mLock.stop(); // mLock is reset by reset manager.
} }
@VisibleForTesting @Override
boolean shouldStart(@NonNull MotionEvent e) { public boolean isResetRequired() {
return isActive();
}
private boolean shouldStart(@NonNull MotionEvent e) {
// b/30146357 && b/23793622. onInterceptTouchEvent does not dispatch events to onTouchEvent // b/30146357 && b/23793622. onInterceptTouchEvent does not dispatch events to onTouchEvent
// unless the event is != ACTION_DOWN. Thus, we need to actually start band selection when // unless the event is != ACTION_DOWN. Thus, we need to actually start band selection when
// mouse moves. // mouse moves.
@ -185,12 +184,8 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
&& !isActive(); && !isActive();
} }
@VisibleForTesting private boolean shouldStop(@NonNull MotionEvent e) {
boolean shouldStop(@NonNull MotionEvent e) { return isActive() && MotionEvents.isActionUp(e);
return isActive()
&& (MotionEvents.isActionUp(e)
|| MotionEvents.isActionPointerUp(e)
|| MotionEvents.isActionCancel(e));
} }
@Override @Override
@ -242,7 +237,9 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
* Starts band select by adding the drawable to the RecyclerView's overlay. * Starts band select by adding the drawable to the RecyclerView's overlay.
*/ */
private void startBandSelect(@NonNull MotionEvent e) { private void startBandSelect(@NonNull MotionEvent e) {
checkState(!isActive()); if (DEBUG) {
checkState(!isActive());
}
if (!MotionEvents.isCtrlKeyPressed(e)) { if (!MotionEvents.isCtrlKeyPressed(e)) {
mSelectionTracker.clearSelection(); mSelectionTracker.clearSelection();
@ -303,7 +300,18 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
} }
mSelectionTracker.mergeProvisionalSelection(); mSelectionTracker.mergeProvisionalSelection();
reset(); mLock.stop();
mHost.hideBand();
if (mModel != null) {
mModel.stopCapturing();
mModel.onDestroy();
}
mModel = null;
mOrigin = null;
mScroller.reset();
} }
/** /**
@ -348,8 +356,6 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
/** /**
* Add a listener to be notified on scroll events. * Add a listener to be notified on scroll events.
*
* @param listener
*/ */
abstract void addOnScrollListener(@NonNull OnScrollListener listener); abstract void addOnScrollListener(@NonNull OnScrollListener listener);
} }

View File

@ -26,6 +26,7 @@ import android.view.View;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate; import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@ -53,7 +54,7 @@ final class DefaultBandHost<K> extends GridModel.GridHost<K> {
checkArgument(recyclerView != null); checkArgument(recyclerView != null);
mRecyclerView = recyclerView; mRecyclerView = recyclerView;
mBand = mRecyclerView.getContext().getResources().getDrawable(bandOverlayId); mBand = ContextCompat.getDrawable(mRecyclerView.getContext(), bandOverlayId);
checkArgument(mBand != null); checkArgument(mBand != null);
checkArgument(keyProvider != null); checkArgument(keyProvider != null);

View File

@ -46,17 +46,17 @@ import java.util.Set;
* {@link SelectionPredicate#canSelectMultiple()}. * {@link SelectionPredicate#canSelectMultiple()}.
* *
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types. * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*
* @hide * @hide
*/ */
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
public class DefaultSelectionTracker<K> extends SelectionTracker<K> { @SuppressWarnings("unchecked")
public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements Resettable {
private static final String TAG = "DefaultSelectionTracker"; private static final String TAG = "DefaultSelectionTracker";
private static final String EXTRA_SELECTION_PREFIX = "androidx.recyclerview.selection"; private static final String EXTRA_SELECTION_PREFIX = "androidx.recyclerview.selection";
private final Selection<K> mSelection = new Selection<>(); private final Selection<K> mSelection = new Selection<>();
private final List<SelectionObserver> mObservers = new ArrayList<>(1); private final List<SelectionObserver<K>> mObservers = new ArrayList<>(1);
private final ItemKeyProvider<K> mKeyProvider; private final ItemKeyProvider<K> mKeyProvider;
private final SelectionPredicate<K> mSelectionPredicate; private final SelectionPredicate<K> mSelectionPredicate;
private final StorageStrategy<K> mStorage; private final StorageStrategy<K> mStorage;
@ -70,16 +70,16 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
/** /**
* Creates a new instance. * Creates a new instance.
* *
* @param selectionId A unique string identifying this selection in the context * @param selectionId A unique string identifying this selection in the context
* of the activity or fragment. * of the activity or fragment.
* @param keyProvider client supplied class providing access to stable ids. * @param keyProvider client supplied class providing access to stable ids.
* @param selectionPredicate A predicate allowing the client to disallow selection * @param selectionPredicate A predicate allowing the client to disallow selection
* @param storage Strategy for storing typed selection in bundle. * @param storage Strategy for storing typed selection in bundle.
*/ */
public DefaultSelectionTracker( public DefaultSelectionTracker(
@NonNull String selectionId, @NonNull String selectionId,
@NonNull ItemKeyProvider keyProvider, @NonNull ItemKeyProvider<K> keyProvider,
@NonNull SelectionPredicate selectionPredicate, @NonNull SelectionPredicate<K> selectionPredicate,
@NonNull StorageStrategy<K> storage) { @NonNull StorageStrategy<K> storage) {
checkArgument(selectionId != null); checkArgument(selectionId != null);
@ -101,23 +101,26 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
} }
@Override @Override
public void addObserver(@NonNull SelectionObserver callback) { public void addObserver(@NonNull SelectionObserver<K> callback) {
checkArgument(callback != null); checkArgument(callback != null);
mObservers.add(callback); mObservers.add(callback);
} }
/**
* @return true if there is a primary or previsional selection.
*/
@Override @Override
public boolean hasSelection() { public boolean hasSelection() {
return !mSelection.isEmpty(); return !mSelection.isEmpty();
} }
@Override @Override
public Selection getSelection() { public @NonNull Selection<K> getSelection() {
return mSelection; return mSelection;
} }
@Override @Override
public void copySelection(@NonNull MutableSelection dest) { public void copySelection(@NonNull MutableSelection<K> dest) {
dest.copyFrom(mSelection); dest.copyFrom(mSelection);
} }
@ -127,7 +130,7 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
} }
@Override @Override
protected void restoreSelection(@NonNull Selection other) { protected void restoreSelection(@NonNull Selection<K> other) {
checkArgument(other != null); checkArgument(other != null);
setItemsSelectedQuietly(other.mSelection, true); setItemsSelectedQuietly(other.mSelection, true);
// NOTE: We intentionally don't restore provisional selection. It's provisional. // NOTE: We intentionally don't restore provisional selection. It's provisional.
@ -143,7 +146,7 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
private boolean setItemsSelectedQuietly(@NonNull Iterable<K> keys, boolean selected) { private boolean setItemsSelectedQuietly(@NonNull Iterable<K> keys, boolean selected) {
boolean changed = false; boolean changed = false;
for (K key: keys) { for (K key : keys) {
boolean itemChanged = selected boolean itemChanged = selected
? canSetState(key, true) && mSelection.add(key) ? canSetState(key, true) && mSelection.add(key)
: canSetState(key, false) && mSelection.remove(key); : canSetState(key, false) && mSelection.remove(key);
@ -158,11 +161,15 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
@Override @Override
public boolean clearSelection() { public boolean clearSelection() {
if (!hasSelection()) { if (!hasSelection()) {
if (DEBUG) Log.d(TAG, "Ignoring clearSelection request. No selection.");
return false; return false;
} }
if (DEBUG) Log.d(TAG, "Handling clearSelection request.");
clearProvisionalSelection(); clearProvisionalSelection();
clearPrimarySelection(); clearPrimarySelection();
notifySelectionCleared();
return true; return true;
} }
@ -171,7 +178,7 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
return; return;
} }
Selection prev = clearSelectionQuietly(); Selection<K> prev = clearSelectionQuietly();
notifySelectionCleared(prev); notifySelectionCleared(prev);
notifySelectionChanged(); notifySelectionChanged();
} }
@ -181,10 +188,10 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
* Returns items in previous selection. Callers are responsible for notifying * Returns items in previous selection. Callers are responsible for notifying
* listeners about changes. * listeners about changes.
*/ */
private Selection clearSelectionQuietly() { private Selection<K> clearSelectionQuietly() {
mRange = null; mRange = null;
MutableSelection prevSelection = new MutableSelection(); MutableSelection<K> prevSelection = new MutableSelection();
if (hasSelection()) { if (hasSelection()) {
copySelection(prevSelection); copySelection(prevSelection);
mSelection.clear(); mSelection.clear();
@ -193,6 +200,18 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
return prevSelection; return prevSelection;
} }
@Override
public void reset() {
if (DEBUG) Log.d(TAG, "Received reset request.");
clearSelection();
mRange = null;
}
@Override
public boolean isResetRequired() {
return hasSelection() || isRangeActive();
}
@Override @Override
public boolean select(@NonNull K key) { public boolean select(@NonNull K key) {
checkArgument(key != null); checkArgument(key != null);
@ -208,7 +227,7 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
// Enforce single selection policy. // Enforce single selection policy.
if (mSingleSelect && hasSelection()) { if (mSingleSelect && hasSelection()) {
Selection prev = clearSelectionQuietly(); Selection<K> prev = clearSelectionQuietly();
notifySelectionCleared(prev); notifySelectionCleared(prev);
} }
@ -277,8 +296,10 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
return; return;
} }
if (DEBUG) Log.i(TAG, "Extending provision range to position: " + position); if (DEBUG) {
checkState(isRangeActive(), "Range start point not set."); Log.i(TAG, "Extending provision range to position: " + position);
checkState(isRangeActive(), "Range start point not set.");
}
extendRange(position, Range.TYPE_PROVISIONAL); extendRange(position, Range.TYPE_PROVISIONAL);
} }
@ -291,13 +312,23 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
* point before calling on {@link #endRange()}. * point before calling on {@link #endRange()}.
* *
* @param position The new end position for the selection range. * @param position The new end position for the selection range.
* @param type The type of selection the range should utilize. * @param type The type of selection the range should utilize.
*/ */
private void extendRange(int position, @RangeType int type) { private void extendRange(int position, @RangeType int type) {
checkState(isRangeActive(), "Range start point not set."); if (!isRangeActive()) {
Log.e(TAG, "Ignoring attempt to extend unestablished range. Ignoring.");
if (DEBUG) {
throw new IllegalStateException("Attempted to extend unestablished range.");
}
return;
}
if (position == RecyclerView.NO_POSITION) { if (position == RecyclerView.NO_POSITION) {
Log.w(TAG, "Invalid position: Cannot extend selection to: " + position); Log.w(TAG, "Ignoring attempt to extend range to invalid position: " + position);
if (DEBUG) {
throw new IllegalStateException(
"Attempting to extend range to invalid position: " + position);
}
return; return;
} }
@ -316,7 +347,7 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
} }
Map<K, Boolean> delta = mSelection.setProvisionalSelection(newSelection); Map<K, Boolean> delta = mSelection.setProvisionalSelection(newSelection);
for (Map.Entry<K, Boolean> entry: delta.entrySet()) { for (Map.Entry<K, Boolean> entry : delta.entrySet()) {
notifyItemStateChanged(entry.getKey(), entry.getValue()); notifyItemStateChanged(entry.getKey(), entry.getValue());
} }
@ -348,20 +379,16 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
return mRange != null; return mRange != null;
} }
boolean isOverlapping(int position, int count) {
return (mRange != null && mRange.isOverlapping(position, count));
}
private boolean canSetState(@NonNull K key, boolean nextState) { private boolean canSetState(@NonNull K key, boolean nextState) {
return mSelectionPredicate.canSetStateForKey(key, nextState); return mSelectionPredicate.canSetStateForKey(key, nextState);
} }
@Override @Override
protected AdapterDataObserver getAdapterDataObserver() { protected @NonNull AdapterDataObserver getAdapterDataObserver() {
return mAdapterObserver; return mAdapterObserver;
} }
@SuppressWarnings("WeakerAccess") /* synthetic access */ @SuppressWarnings({"WeakerAccess", "unchecked"}) /* synthetic access */
void onDataSetChanged() { void onDataSetChanged() {
mSelection.clearProvisionalSelection(); mSelection.clearProvisionalSelection();
@ -409,11 +436,17 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
} }
} }
private void notifySelectionCleared() {
for (SelectionObserver<K> observer : mObservers) {
observer.onSelectionCleared();
}
}
private void notifySelectionCleared(@NonNull Selection<K> selection) { private void notifySelectionCleared(@NonNull Selection<K> selection) {
for (K key: selection.mSelection) { for (K key : selection.mSelection) {
notifyItemStateChanged(key, false); notifyItemStateChanged(key, false);
} }
for (K key: selection.mProvisionalSelection) { for (K key : selection.mProvisionalSelection) {
notifyItemStateChanged(key, false); notifyItemStateChanged(key, false);
} }
} }
@ -445,19 +478,6 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
} }
} }
private void updateForRange(int begin, int end, boolean selected, @RangeType int type) {
switch (type) {
case Range.TYPE_PRIMARY:
updateForRegularRange(begin, end, selected);
break;
case Range.TYPE_PROVISIONAL:
updateForProvisionalRange(begin, end, selected);
break;
default:
throw new IllegalArgumentException("Invalid range type: " + type);
}
}
@SuppressWarnings("WeakerAccess") /* synthetic access */ @SuppressWarnings("WeakerAccess") /* synthetic access */
void updateForRegularRange(int begin, int end, boolean selected) { void updateForRegularRange(int begin, int end, boolean selected) {
checkArgument(end >= begin); checkArgument(end >= begin);
@ -514,7 +534,6 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
} }
@Override @Override
@SuppressWarnings("unchecked")
public final void onSaveInstanceState(@NonNull Bundle state) { public final void onSaveInstanceState(@NonNull Bundle state) {
if (mSelection.isEmpty()) { if (mSelection.isEmpty()) {
return; return;
@ -582,21 +601,17 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
@Override @Override
public void onItemRangeInserted(int startPosition, int itemCount) { public void onItemRangeInserted(int startPosition, int itemCount) {
if (mSelectionTracker.isOverlapping(startPosition, itemCount)) mSelectionTracker.endRange();
mSelectionTracker.endRange();
} }
@Override @Override
public void onItemRangeRemoved(int startPosition, int itemCount) { public void onItemRangeRemoved(int startPosition, int itemCount) {
if (mSelectionTracker.isOverlapping(startPosition, itemCount)) mSelectionTracker.endRange();
mSelectionTracker.endRange();
} }
@Override @Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
if (mSelectionTracker.isOverlapping(fromPosition, itemCount) || mSelectionTracker.endRange();
mSelectionTracker.isOverlapping(toPosition, itemCount))
mSelectionTracker.endRange();
} }
} }
} }

View File

@ -0,0 +1,43 @@
/*
* 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 android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
/**
* No-op implementation of OnItemTouchListener suitable for use as a default
* handler w/ ToolHandlerRegistery, or in tests.
*/
final class DummyOnItemTouchListener implements RecyclerView.OnItemTouchListener {
@Override
public boolean onInterceptTouchEvent(
@NonNull RecyclerView unused, @NonNull MotionEvent e) {
return false;
}
@Override
public void onTouchEvent(
@NonNull RecyclerView unused, @NonNull MotionEvent e) {
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
}

View File

@ -0,0 +1,69 @@
/*
* 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 androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
/**
* A class responsible for routing MotionEvents to tool-type specific handlers.
* Individual tool-type specific handlers are added after the class is constructed.
*
* <p>
* EventRouter takes its name from
* {@link RecyclerView#addOnItemTouchListener(OnItemTouchListener)}. Despite "Touch"
* being in the name, it receives MotionEvents for all types of tools.
*/
final class EventRouter implements OnItemTouchListener {
private final ToolHandlerRegistry<OnItemTouchListener> mDelegates;
EventRouter() {
mDelegates = new ToolHandlerRegistry<>(new DummyOnItemTouchListener());
}
/**
* @param toolType See MotionEvent for details on available types.
* @param delegate An {@link OnItemTouchListener} to receive events
* of {@code toolType}.
*/
void set(int toolType, @NonNull OnItemTouchListener delegate) {
checkArgument(delegate != null);
mDelegates.set(toolType, delegate);
}
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
return mDelegates.get(e).onInterceptTouchEvent(rv, e);
}
@Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
mDelegates.get(e).onTouchEvent(rv, e);
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
// TODO(b/139141511): Handle onRequestDisallowInterceptTouchEvent.
}
}

View File

@ -61,7 +61,22 @@ public abstract class FocusDelegate<K> {
public abstract boolean hasFocusedItem(); public abstract boolean hasFocusedItem();
/** /**
* @return the position of the currently focused item, if any. * Returns the position of the currently focused item, or
* {@link RecyclerView#NO_POSITION} if nothing is focused.
*
* <p>You must implement this feature if you intend your app
* to work well with mouse and keyboard. Selection
* ranges are inferred from focused item when there is
* no explicit last-selected item.
*
* <p>You can manage and advance focus using keyboard arrows,
* reflecting this state visibly in the view item.
* Use can then press shift, then click another item with
* their mouse to select all items between the focused
* item and the clicked item.
*
* @return the position of the currently focused item,
* or {@code RecyclerView#NO_POSITION} if none.
*/ */
public abstract int getFocusedPosition(); public abstract int getFocusedPosition();

View File

@ -0,0 +1,58 @@
/*
* 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) {
}
}

View File

@ -43,6 +43,7 @@ final class GestureRouter<T extends OnGestureListener & OnDoubleTapListener>
mDelegates = new ToolHandlerRegistry<>(defaultDelegate); mDelegates = new ToolHandlerRegistry<>(defaultDelegate);
} }
@SuppressWarnings("unchecked")
GestureRouter() { GestureRouter() {
this((T) new SimpleOnGestureListener()); this((T) new SimpleOnGestureListener());
} }

View File

@ -17,7 +17,8 @@
package androidx.recyclerview.selection; package androidx.recyclerview.selection;
import static androidx.core.util.Preconditions.checkArgument; import static androidx.core.util.Preconditions.checkArgument;
import static androidx.core.util.Preconditions.checkState; import static androidx.recyclerview.selection.Shared.DEBUG;
import static androidx.recyclerview.selection.Shared.VERBOSE;
import android.util.Log; import android.util.Log;
import android.view.MotionEvent; import android.view.MotionEvent;
@ -36,7 +37,7 @@ import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
* when used in conjunction with RecyclerView and other classes in the ReyclerView * when used in conjunction with RecyclerView and other classes in the ReyclerView
* selection support package. * selection support package.
*/ */
final class GestureSelectionHelper implements OnItemTouchListener { final class GestureSelectionHelper implements OnItemTouchListener, Resettable {
private static final String TAG = "GestureSelectionHelper"; private static final String TAG = "GestureSelectionHelper";
@ -76,16 +77,19 @@ final class GestureSelectionHelper implements OnItemTouchListener {
* Explicitly kicks off a gesture multi-select. * Explicitly kicks off a gesture multi-select.
*/ */
void start() { void start() {
checkState(!mStarted);
// Partner code in MotionInputHandler ensures items // Partner code in MotionInputHandler ensures items
// are selected and range anchor initialized prior to // are selected and range anchor initialized prior to
// start being called. // start being called.
// Verify the truth of that statement here // Verify the truth of that statement here
// to make the implicit coupling less of a time bomb. // to make the implicit coupling less of a time bomb.
checkState(mSelectionMgr.isRangeActive()); if (mStarted) {
if (DEBUG) {
mLock.checkStopped(); Log.e(TAG, "Attempting to start, but state is already=started.");
throw new IllegalStateException(
"Attempting to start, but state is already=started.");
}
return;
}
mStarted = true; mStarted = true;
mLock.start(); mLock.start();
@ -94,15 +98,20 @@ final class GestureSelectionHelper implements OnItemTouchListener {
@Override @Override
/** @hide */ /** @hide */
public boolean onInterceptTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) { 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
// lost if we don't act on them in this method.
//
// TODO(b/132447183): For some reason we're not receiving an ACTION_UP // TODO(b/132447183): For some reason we're not receiving an ACTION_UP
// event after a > long-press NOT followed by a ACTION_MOVE < event. // event after a > long-press NOT followed by a ACTION_MOVE < event.
if (mStarted) { if (mStarted) {
handleTouch(e); onTouchEvent(unused, e);
} }
// ACTION_CANCEL is associated with "TOOL_TYPE_UNKNOWN" and
// is handled in ResetManager.
switch (e.getActionMasked()) { switch (e.getActionMasked()) {
case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: case MotionEvent.ACTION_UP:
return mStarted; return mStarted;
default: default:
@ -113,27 +122,11 @@ final class GestureSelectionHelper implements OnItemTouchListener {
@Override @Override
/** @hide */ /** @hide */
public void onTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) { public void onTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
// See handleTouch(MotionEvent) javadoc for explanation as to why this is correct. if (!mStarted) {
handleTouch(e); if (VERBOSE) Log.i(TAG, "Ignoring input event. Not started.");
} return;
}
/**
* If selection has started, will handle all appropriate types of MotionEvents and will return
* true if this OnItemTouchListener should start intercepting the rest of the MotionEvents.
*
* <p>This code, and the fact that this method is used by both OnInterceptTouchEvent and
* OnTouchEvent, is correct and valid because:
* <ol>
* <li>MotionEvents that aren't ACTION_DOWN are only ever passed to either onInterceptTouchEvent
* or onTouchEvent; never to both. The MotionEvents we are handling in this method are not
* ACTION_DOWN, and therefore, its appropriate that both the onInterceptTouchEvent and
* onTouchEvent code paths cross this method.
* <li>This method returns true when we want to intercept MotionEvents. OnInterceptTouchEvent
* uses that information to determine its own return, and OnMotionEvent doesn't have a return
* so this methods return value is irrelevant to it.
* </ol>
*/
private void handleTouch(MotionEvent e) {
if (!mSelectionMgr.isRangeActive()) { if (!mSelectionMgr.isRangeActive()) {
Log.e(TAG, Log.e(TAG,
"Internal state of GestureSelectionHelper out of sync w/ SelectionTracker " "Internal state of GestureSelectionHelper out of sync w/ SelectionTracker "
@ -141,6 +134,8 @@ final class GestureSelectionHelper implements OnItemTouchListener {
endSelection(); endSelection();
} }
// ACTION_CANCEL is associated with "TOOL_TYPE_UNKNOWN" and
// is handled in ResetManager.
switch (e.getActionMasked()) { switch (e.getActionMasked()) {
case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_MOVE:
handleMoveEvent(e); handleMoveEvent(e);
@ -148,9 +143,6 @@ final class GestureSelectionHelper implements OnItemTouchListener {
case MotionEvent.ACTION_UP: case MotionEvent.ACTION_UP:
handleUpEvent(); handleUpEvent();
break; break;
case MotionEvent.ACTION_CANCEL:
handleCancelEvent();
break;
} }
} }
@ -167,17 +159,22 @@ final class GestureSelectionHelper implements OnItemTouchListener {
endSelection(); endSelection();
} }
// Called when ACTION_CANCEL event is to be handled. /**
// This means this gesture selection is aborted, so reset everything and abandon provisional * Immediately "Stops" active gesture selection, and resets all related state.
// selection. */
private void handleCancelEvent() { @Override
mSelectionMgr.clearProvisionalSelection(); public void reset() {
endSelection(); if (DEBUG) Log.d(TAG, "Received reset request.");
mStarted = false;
mScroller.reset();
}
@Override
public boolean isResetRequired() {
return mStarted;
} }
private void endSelection() { private void endSelection() {
checkState(mStarted);
mStarted = false; mStarted = false;
mScroller.reset(); mScroller.reset();
mLock.stop(); mLock.stop();
@ -186,6 +183,11 @@ final class GestureSelectionHelper implements OnItemTouchListener {
// Call when an intercepted ACTION_MOVE event is passed down. // Call when an intercepted ACTION_MOVE event is passed down.
// At this point, we are sure user wants to gesture multi-select. // At this point, we are sure user wants to gesture multi-select.
private void handleMoveEvent(@NonNull MotionEvent e) { private void handleMoveEvent(@NonNull MotionEvent e) {
if (!mStarted) {
Log.e(TAG, "Received event while not started.");
if (DEBUG) throw new IllegalStateException("Received event while not started.");
}
int lastGlidedItemPos = mView.getLastGlidedItemPosition(e); int lastGlidedItemPos = mView.getLastGlidedItemPosition(e);
if (mSelectionPredicate.canSetStateAtPosition(lastGlidedItemPos, true)) { if (mSelectionPredicate.canSetStateAtPosition(lastGlidedItemPos, true)) {
extendSelection(lastGlidedItemPos); extendSelection(lastGlidedItemPos);
@ -283,7 +285,7 @@ final class GestureSelectionHelper implements OnItemTouchListener {
// of items in the adapter. Using the adapter is the for sure way to get the actual last // of items in the adapter. Using the adapter is the for sure way to get the actual last
// item position. // item position.
final float inboundY = getInboundY(mRecyclerView.getHeight(), e.getY()); final float inboundY = getInboundY(mRecyclerView.getHeight(), e.getY());
return (pastLastItem) ? mRecyclerView.getAdapter().getItemCount() - 1 return pastLastItem ? mRecyclerView.getAdapter().getItemCount() - 1
: mRecyclerView.getChildAdapterPosition( : mRecyclerView.getChildAdapterPosition(
mRecyclerView.findChildViewUnder(e.getX(), inboundY)); mRecyclerView.findChildViewUnder(e.getX(), inboundY));
} }

View File

@ -63,7 +63,7 @@ final class GridModel<K> {
private final ItemKeyProvider<K> mKeyProvider; private final ItemKeyProvider<K> mKeyProvider;
private final SelectionPredicate<K> mSelectionPredicate; private final SelectionPredicate<K> mSelectionPredicate;
private final List<SelectionObserver> mOnSelectionChangedListeners = new ArrayList<>(); private final List<SelectionObserver<K>> mOnSelectionChangedListeners = new ArrayList<>();
// Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed // Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed
// by their y-offset. For example, if the first column of the view starts at an x-value of 5, // by their y-offset. For example, if the first column of the view starts at an x-value of 5,
@ -101,8 +101,9 @@ final class GridModel<K> {
private final OnScrollListener mScrollListener; private final OnScrollListener mScrollListener;
@SuppressWarnings("unchecked")
GridModel( GridModel(
GridHost host, GridHost<K> host,
ItemKeyProvider<K> keyProvider, ItemKeyProvider<K> keyProvider,
SelectionPredicate<K> selectionPredicate) { SelectionPredicate<K> selectionPredicate) {
@ -284,8 +285,9 @@ final class GridModel<K> {
* mSelection, so computeCurrentSelection() should be called before this * mSelection, so computeCurrentSelection() should be called before this
* function. * function.
*/ */
@SuppressWarnings("unchecked")
private void notifySelectionChanged() { private void notifySelectionChanged() {
for (SelectionObserver listener : mOnSelectionChangedListeners) { for (SelectionObserver<K> listener : mOnSelectionChangedListeners) {
listener.onSelectionChanged(mSelection); listener.onSelectionChanged(mSelection);
} }
} }
@ -401,7 +403,7 @@ final class GridModel<K> {
abstract void onSelectionChanged(Set<K> updatedSelection); abstract void onSelectionChanged(Set<K> updatedSelection);
} }
void addOnSelectionChangedListener(SelectionObserver listener) { void addOnSelectionChangedListener(SelectionObserver<K> listener) {
mOnSelectionChangedListeners.add(listener); mOnSelectionChangedListeners.add(listener);
} }

View File

@ -44,10 +44,10 @@ import androidx.recyclerview.widget.RecyclerView;
* mRecyclerView = recyclerView; * mRecyclerView = recyclerView;
* } * }
* *
* public ItemDetails<Uri> getItemDetails(MotionEvent e) { * public @Nullable ItemDetails<Uri> getItemDetails(@NonNull MotionEvent e) {
* View view = mRecView.findChildViewUnder(e.getX(), e.getY()); * View view = mRecyclerView.findChildViewUnder(e.getX(), e.getY());
* if (view != null) { * if (view != null) {
* ViewHolder holder = mRecView.getChildViewHolder(view); * ViewHolder holder = mRecyclerView.getChildViewHolder(view);
* if (holder instanceof MyHolder) { * if (holder instanceof MyHolder) {
* return ((MyHolder) holder).getItemDetails(); * return ((MyHolder) holder).getItemDetails();
* } * }
@ -110,10 +110,6 @@ public abstract class ItemDetailsLookup<K> {
return item != null && item.getSelectionKey() != null; return item != null && item.getSelectionKey() != null;
} }
private static boolean hasPosition(@Nullable ItemDetails<?> item) {
return item != null && item.getPosition() != RecyclerView.NO_POSITION;
}
/** /**
* @return the ItemDetails for the item under the event, or null. * @return the ItemDetails for the item under the event, or null.
*/ */
@ -241,10 +237,10 @@ public abstract class ItemDetailsLookup<K> {
@Override @Override
public boolean equals(@Nullable Object obj) { public boolean equals(@Nullable Object obj) {
return (obj instanceof ItemDetails) return (obj instanceof ItemDetails)
&& isEqualTo((ItemDetails) obj); && isEqualTo((ItemDetails<?>) obj);
} }
private boolean isEqualTo(@NonNull ItemDetails other) { private boolean isEqualTo(@NonNull ItemDetails<?> other) {
K key = getSelectionKey(); K key = getSelectionKey();
boolean sameKeys = false; boolean sameKeys = false;
if (key == null) { if (key == null) {

View File

@ -33,6 +33,14 @@ final class MotionEvents {
return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE; return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
} }
static boolean isFingerEvent(@NonNull MotionEvent e) {
return e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER;
}
static boolean isActionDown(@NonNull MotionEvent e) {
return e.getActionMasked() == MotionEvent.ACTION_DOWN;
}
static boolean isActionMove(@NonNull MotionEvent e) { static boolean isActionMove(@NonNull MotionEvent e) {
return e.getActionMasked() == MotionEvent.ACTION_MOVE; return e.getActionMasked() == MotionEvent.ACTION_MOVE;
} }

View File

@ -17,7 +17,6 @@
package androidx.recyclerview.selection; package androidx.recyclerview.selection;
import static androidx.core.util.Preconditions.checkArgument; import static androidx.core.util.Preconditions.checkArgument;
import static androidx.core.util.Preconditions.checkState;
import static androidx.recyclerview.selection.Shared.DEBUG; import static androidx.recyclerview.selection.Shared.DEBUG;
import static androidx.recyclerview.selection.Shared.VERBOSE; import static androidx.recyclerview.selection.Shared.VERBOSE;
@ -123,7 +122,11 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
// tap on an item when there is an existing selection. We could extend // tap on an item when there is an existing selection. We could extend
// a selection, we could clear selection (then launch) // a selection, we could clear selection (then launch)
private void onItemClick(@NonNull MotionEvent e, @NonNull ItemDetails<K> item) { private void onItemClick(@NonNull MotionEvent e, @NonNull ItemDetails<K> item) {
checkState(mSelectionTracker.hasSelection()); if (!mSelectionTracker.hasSelection()) {
Log.e(TAG, "Call to onItemClick w/o selection.");
if (DEBUG) throw new IllegalStateException("Call to onItemClick w/o selection.");
return;
}
checkArgument(item != null); checkArgument(item != null);
if (shouldExtendRange(e)) { if (shouldExtendRange(e)) {

View File

@ -16,6 +16,7 @@
package androidx.recyclerview.selection; package androidx.recyclerview.selection;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.core.util.Preconditions.checkArgument; import static androidx.core.util.Preconditions.checkArgument;
import static androidx.core.util.Preconditions.checkState; import static androidx.core.util.Preconditions.checkState;
import static androidx.recyclerview.selection.Shared.DEBUG; import static androidx.recyclerview.selection.Shared.DEBUG;
@ -24,7 +25,7 @@ import android.util.Log;
import androidx.annotation.MainThread; import androidx.annotation.MainThread;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.annotation.RestrictTo;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -36,7 +37,7 @@ import java.util.List;
* *
* <p> * <p>
* The host {@link android.app.Activity} or {@link android.app.Fragment} should avoid changing * The host {@link android.app.Activity} or {@link android.app.Fragment} should avoid changing
* {@link RecyclerView.Adapter Adapter} data while there * {@link androidx.recyclerview.widget.RecyclerView.Adapter Adapter} data while there
* are active selection operations, as this can result in a poor user experience. * are active selection operations, as this can result in a poor user experience.
* *
* <p> * <p>
@ -46,17 +47,33 @@ public final class OperationMonitor {
private static final String TAG = "OperationMonitor"; private static final String TAG = "OperationMonitor";
private final List<OnChangeListener> mListeners = new ArrayList<>();
// 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.
private final Resettable mResettable = new Resettable() {
@Override
public boolean isResetRequired() {
return OperationMonitor.this.isResetRequired();
}
@Override
public void reset() {
OperationMonitor.this.reset();
}
};
private int mNumOps = 0; private int mNumOps = 0;
private List<OnChangeListener> mListeners = new ArrayList<>();
@MainThread @MainThread
synchronized void start() { synchronized void start() {
mNumOps++; mNumOps++;
if (mNumOps == 1) { if (mNumOps == 1) {
for (OnChangeListener l : mListeners) { notifyStateChanged();
l.onChanged();
}
} }
if (DEBUG) Log.v(TAG, "Incremented content lock count to " + mNumOps + "."); if (DEBUG) Log.v(TAG, "Incremented content lock count to " + mNumOps + ".");
@ -64,29 +81,46 @@ public final class OperationMonitor {
@MainThread @MainThread
synchronized void stop() { synchronized void stop() {
checkState(mNumOps > 0); if (mNumOps == 0) {
if (DEBUG) Log.w(TAG, "Stop called whith opt count of 0.");
return;
}
mNumOps--; mNumOps--;
if (DEBUG) Log.v(TAG, "Decremented content lock count to " + mNumOps + "."); if (DEBUG) Log.v(TAG, "Decremented content lock count to " + mNumOps + ".");
if (mNumOps == 0) { if (mNumOps == 0) {
for (OnChangeListener l : mListeners) { notifyStateChanged();
l.onChanged();
}
} }
} }
/** @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.");
}
mNumOps = 0;
notifyStateChanged();
}
/** @hide */
@RestrictTo(LIBRARY)
synchronized boolean isResetRequired() {
return isStarted();
}
/** /**
* @return true if there are any running operations. * @return true if there are any running operations.
*/ */
@SuppressWarnings("unused")
public synchronized boolean isStarted() { public synchronized boolean isStarted() {
return mNumOps > 0; return mNumOps > 0;
} }
/** /**
* Registers supplied listener to be notified when operation status changes. * Registers supplied listener to be notified when operation status changes.
* @param listener
*/ */
public void addListener(@NonNull OnChangeListener listener) { public void addListener(@NonNull OnChangeListener listener) {
checkArgument(listener != null); checkArgument(listener != null);
@ -95,7 +129,6 @@ public final class OperationMonitor {
/** /**
* Unregisters listener for further notifications. * Unregisters listener for further notifications.
* @param listener
*/ */
public void removeListener(@NonNull OnChangeListener listener) { public void removeListener(@NonNull OnChangeListener listener) {
checkArgument(listener != null); checkArgument(listener != null);
@ -105,15 +138,27 @@ public final class OperationMonitor {
/** /**
* Allows other selection code to perform a precondition check asserting the state is locked. * Allows other selection code to perform a precondition check asserting the state is locked.
*/ */
void checkStarted() { void checkStarted(boolean started) {
checkState(mNumOps > 0); if (started) {
checkState(mNumOps > 0);
} else {
checkState(mNumOps == 0);
}
}
private void notifyStateChanged() {
for (OnChangeListener l : mListeners) {
l.onChanged();
}
} }
/** /**
* Allows other selection code to perform a precondition check asserting the state is unlocked. * Work around b/139109223.
* @hide
*/ */
void checkStopped() { @RestrictTo(LIBRARY)
checkState(mNumOps == 0); @NonNull Resettable asResettable() {
return mResettable;
} }
/** /**

View File

@ -25,19 +25,19 @@ import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
/** /**
* OnItemTouchListener that delegates drag events to a drag listener, * OnItemTouchListener that detects and delegates drag events to a drag listener,
* else sends event to fallback {@link OnItemTouchListener}. * else sends event to fallback {@link OnItemTouchListener}.
* *
* <p>See {@link OnDragInitiatedListener} for details on implementing drag and drop. * <p>See {@link OnDragInitiatedListener} for details on implementing drag and drop.
*/ */
final class PointerDragEventInterceptor implements OnItemTouchListener { final class PointerDragEventInterceptor implements OnItemTouchListener {
private final ItemDetailsLookup mEventDetailsLookup; private final ItemDetailsLookup<?> mEventDetailsLookup;
private final OnDragInitiatedListener mDragListener; private final OnDragInitiatedListener mDragListener;
private @Nullable OnItemTouchListener mDelegate; private OnItemTouchListener mDelegate;
PointerDragEventInterceptor( PointerDragEventInterceptor(
ItemDetailsLookup eventDetailsLookup, ItemDetailsLookup<?> eventDetailsLookup,
OnDragInitiatedListener dragListener, OnDragInitiatedListener dragListener,
@Nullable OnItemTouchListener delegate) { @Nullable OnItemTouchListener delegate) {
@ -46,30 +46,29 @@ final class PointerDragEventInterceptor implements OnItemTouchListener {
mEventDetailsLookup = eventDetailsLookup; mEventDetailsLookup = eventDetailsLookup;
mDragListener = dragListener; mDragListener = dragListener;
mDelegate = delegate;
if (delegate != null) {
mDelegate = delegate;
} else {
mDelegate = new DummyOnItemTouchListener();
}
} }
@Override @Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
if (MotionEvents.isPointerDragEvent(e) && mEventDetailsLookup.inItemDragRegion(e)) { if (MotionEvents.isPointerDragEvent(e) && mEventDetailsLookup.inItemDragRegion(e)) {
return mDragListener.onDragInitiated(e); return mDragListener.onDragInitiated(e);
} else if (mDelegate != null) {
return mDelegate.onInterceptTouchEvent(rv, e);
} }
return false; return mDelegate.onInterceptTouchEvent(rv, e);
} }
@Override @Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) { public void onTouchEvent(RecyclerView rv, MotionEvent e) {
if (mDelegate != null) { mDelegate.onTouchEvent(rv, e);
mDelegate.onTouchEvent(rv, e);
}
} }
@Override @Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (mDelegate != null) { mDelegate.onRequestDisallowInterceptTouchEvent(disallowIntercept);
mDelegate.onRequestDisallowInterceptTouchEvent(disallowIntercept);
}
} }
} }

View File

@ -54,12 +54,14 @@ final class Range {
* provisional selection will not affect the primary selection where the two may intersect. * provisional selection will not affect the primary selection where the two may intersect.
*/ */
static final int TYPE_PROVISIONAL = 1; static final int TYPE_PROVISIONAL = 1;
@IntDef({ @IntDef({
TYPE_PRIMARY, TYPE_PRIMARY,
TYPE_PROVISIONAL TYPE_PROVISIONAL
}) })
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@interface RangeType {} @interface RangeType {
}
private static final String TAG = "Range"; private static final String TAG = "Range";
@ -69,9 +71,6 @@ final class Range {
/** /**
* Creates a new range anchored at {@code position}. * Creates a new range anchored at {@code position}.
*
* @param position
* @param callbacks
*/ */
Range(int position, @NonNull Callbacks callbacks) { Range(int position, @NonNull Callbacks callbacks) {
mBegin = position; mBegin = position;
@ -118,7 +117,7 @@ final class Range {
} else if (mEnd < mBegin) { } else if (mEnd < mBegin) {
reviseDescending(position, type); reviseDescending(position, type);
} }
// the "else" case is covered by checkState at beginning of method. // the "else" case is covered by checkArgument at beginning of method.
mEnd = position; mEnd = position;
} }
@ -170,11 +169,6 @@ final class Range {
mCallbacks.updateForRange(begin, end, selected, type); mCallbacks.updateForRange(begin, end, selected, type);
} }
boolean isOverlapping(int position, int count) {
return (position >= mBegin && position <= mEnd) ||
(position + count >= mBegin && position + count <= mEnd);
}
@Override @Override
public String toString() { public String toString() {
return "Range{begin=" + mBegin + ", end=" + mEnd + "}"; return "Range{begin=" + mBegin + ", end=" + mEnd + "}";

View File

@ -0,0 +1,101 @@
/*
* 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.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 java.util.ArrayList;
import java.util.List;
/**
* Class managing resetting of library state in response to specific
* events like clearing of selection and MotionEvent.ACTION_CANCEL
* events.
*
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*/
final class ResetManager<K> {
private static final String TAG = "ResetManager";
private final List<Resettable> mResetHandlers = new ArrayList<>();
private final OnItemTouchListener mInputListener = new OnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView unused,
@NonNull MotionEvent e) {
if (MotionEvents.isActionCancel(e)) {
if (DEBUG) Log.d(TAG, "Received CANCEL event.");
callResetHandlers();
}
return false;
}
@Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
};
// Resettable interface has a #requiresReset method because DefaultSelectionTracker
// (owner of the state we observe with our SelectionObserver) is, itself,
// a Resettable. Such an arrangement introduces the real possibility of infinite recursion.
// When we call reset on DefaultSelectionTracker it'll eventually call back to
// notify us of the change via onSelectionCleared. We avoid recursion by
// checking #requiresReset before calling reset again.
private final SelectionObserver<K> mSelectionObserver = new SelectionObserver<K>() {
@Override
protected void onSelectionCleared() {
if (DEBUG) Log.d(TAG, "Received onSelectionCleared event.");
callResetHandlers();
}
};
SelectionObserver<K> getSelectionObserver() {
return mSelectionObserver;
}
OnItemTouchListener getInputListener() {
return mInputListener;
}
/**
* Registers a new Resettable.
*/
void addResetHandler(@NonNull Resettable handler) {
mResetHandlers.add(handler);
}
void callResetHandlers() {
for (Resettable handler : mResetHandlers) {
if (handler.isResetRequired()) {
handler.reset();
}
}
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.annotation.RestrictTo.Scope.LIBRARY;
import androidx.annotation.RestrictTo;
/**
* Represents an object that can be reset and can advise on it's
* need to be reset.
*
* <p>Calling {@link #isResetRequired()} on an instance of {@link Resettable}
* should always return false when called immediately after {@link #reset()}
* has been called.
*
* @hide
*/
@RestrictTo(LIBRARY)
public interface Resettable {
/**
* @return true if the object requires reset.
*/
boolean isResetRequired();
/**
* Resets the object state.
*/
void reset();
}

View File

@ -54,9 +54,8 @@ import java.util.Set;
* (which can be initiated by long pressing an unselected item while there is an * (which can be initiated by long pressing an unselected item while there is an
* existing selection). * existing selection).
* *
* @see MutableSelection
*
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types. * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
* @see MutableSelection
*/ */
public class Selection<K> implements Iterable<K> { public class Selection<K> implements Iterable<K> {
@ -78,7 +77,6 @@ public class Selection<K> implements Iterable<K> {
} }
/** /**
* @param key
* @return true if the position is currently selected. * @return true if the position is currently selected.
*/ */
public boolean contains(@Nullable K key) { public boolean contains(@Nullable K key) {
@ -92,7 +90,7 @@ public class Selection<K> implements Iterable<K> {
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public Iterator<K> iterator() { public @NonNull Iterator<K> iterator() {
return mSelection.iterator(); return mSelection.iterator();
} }
@ -114,12 +112,13 @@ public class Selection<K> implements Iterable<K> {
* Sets the provisional selection, which is a temporary selection that can be saved, * Sets the provisional selection, which is a temporary selection that can be saved,
* canceled, or adjusted at a later time. When a new provision selection is applied, the old * canceled, or adjusted at a later time. When a new provision selection is applied, the old
* one (if it exists) is abandoned. * one (if it exists) is abandoned.
*
* @return Map of ids added or removed. Added ids have a value of true, removed are false. * @return Map of ids added or removed. Added ids have a value of true, removed are false.
*/ */
Map<K, Boolean> setProvisionalSelection(@NonNull Set<K> newSelection) { Map<K, Boolean> setProvisionalSelection(@NonNull Set<K> newSelection) {
Map<K, Boolean> delta = new LinkedHashMap<>(); Map<K, Boolean> delta = new LinkedHashMap<>();
for (K key: mProvisionalSelection) { for (K key : mProvisionalSelection) {
// Mark each item that used to be in the provisional selection // Mark each item that used to be in the provisional selection
// but is not in the new provisional selection. // but is not in the new provisional selection.
if (!newSelection.contains(key) && !mSelection.contains(key)) { if (!newSelection.contains(key) && !mSelection.contains(key)) {
@ -127,7 +126,7 @@ public class Selection<K> implements Iterable<K> {
} }
} }
for (K key: mSelection) { for (K key : mSelection) {
// Mark each item that in the selection but is not in the new // Mark each item that in the selection but is not in the new
// provisional selection. // provisional selection.
if (!newSelection.contains(key)) { if (!newSelection.contains(key)) {
@ -135,7 +134,7 @@ public class Selection<K> implements Iterable<K> {
} }
} }
for (K key: newSelection) { for (K key : newSelection) {
// Mark each item that was not previously in the selection but is in the new // Mark each item that was not previously in the selection but is in the new
// provisional selection. // provisional selection.
if (!mSelection.contains(key) && !mProvisionalSelection.contains(key)) { if (!mSelection.contains(key) && !mProvisionalSelection.contains(key)) {
@ -146,7 +145,7 @@ public class Selection<K> implements Iterable<K> {
// Now, iterate through the changes and actually add/remove them to/from the current // Now, iterate through the changes and actually add/remove them to/from the current
// selection. This could not be done in the previous loops because changing the size of // selection. This could not be done in the previous loops because changing the size of
// the selection mid-iteration changes iteration order erroneously. // the selection mid-iteration changes iteration order erroneously.
for (Map.Entry<K, Boolean> entry: delta.entrySet()) { for (Map.Entry<K, Boolean> entry : delta.entrySet()) {
K key = entry.getKey(); K key = entry.getKey();
if (entry.getValue()) { if (entry.getValue()) {
mProvisionalSelection.add(key); mProvisionalSelection.add(key);
@ -221,11 +220,11 @@ public class Selection<K> implements Iterable<K> {
StringBuilder buffer = new StringBuilder(size() * 28); StringBuilder buffer = new StringBuilder(size() * 28);
buffer.append("Selection{") buffer.append("Selection{")
.append("primary{size=" + mSelection.size()) .append("primary{size=" + mSelection.size())
.append(", entries=" + mSelection) .append(", entries=" + mSelection)
.append("}, provisional{size=" + mProvisionalSelection.size()) .append("}, provisional{size=" + mProvisionalSelection.size())
.append(", entries=" + mProvisionalSelection) .append(", entries=" + mProvisionalSelection)
.append("}}"); .append("}}");
return buffer.toString(); return buffer.toString();
} }
@ -237,10 +236,10 @@ public class Selection<K> implements Iterable<K> {
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
return (this == other) return (this == other)
|| (other instanceof Selection && isEqualTo((Selection) other)); || (other instanceof Selection && isEqualTo((Selection<?>) other));
} }
private boolean isEqualTo(Selection other) { private boolean isEqualTo(Selection<?> other) {
return mSelection.equals(other.mSelection) return mSelection.equals(other.mSelection)
&& mProvisionalSelection.equals(other.mProvisionalSelection); && mProvisionalSelection.equals(other.mProvisionalSelection);
} }

View File

@ -34,7 +34,7 @@ public final class SelectionPredicates {
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types. * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
* @return * @return
*/ */
public static <K> SelectionPredicate<K> createSelectAnything() { public static @NonNull <K> SelectionPredicate<K> createSelectAnything() {
return new SelectionPredicate<K>() { return new SelectionPredicate<K>() {
@Override @Override
public boolean canSetStateForKey(@NonNull K key, boolean nextState) { public boolean canSetStateForKey(@NonNull K key, boolean nextState) {
@ -60,7 +60,7 @@ public final class SelectionPredicates {
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types. * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
* @return * @return
*/ */
public static <K> SelectionPredicate<K> createSelectSingleAnything() { public static @NonNull <K> SelectionPredicate<K> createSelectSingleAnything() {
return new SelectionPredicate<K>() { return new SelectionPredicate<K>() {
@Override @Override
public boolean canSetStateForKey(@NonNull K key, boolean nextState) { public boolean canSetStateForKey(@NonNull K key, boolean nextState) {

View File

@ -22,6 +22,7 @@ import static androidx.core.util.Preconditions.checkArgument;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.util.Log;
import android.view.GestureDetector; import android.view.GestureDetector;
import android.view.HapticFeedbackConstants; import android.view.HapticFeedbackConstants;
import android.view.MotionEvent; import android.view.MotionEvent;
@ -89,12 +90,14 @@ import java.util.Set;
*/ */
public abstract class SelectionTracker<K> { public abstract class SelectionTracker<K> {
private static final String TAG = "SelectionTracker";
/** /**
* This value is included in the payload when SelectionTracker notifies RecyclerView * This value is included in the payload when SelectionTracker notifies RecyclerView
* of changes to selection. Look for this value in the {@code payload} * of changes to selection. Look for this value in the {@code payload}
* Object argument supplied to * Object argument supplied to
* {@link RecyclerView.Adapter#onBindViewHolder * {@link RecyclerView.Adapter#onBindViewHolder
* Adapter#onBindViewHolder}. * Adapter#onBindViewHolder}.
* If present the call is occurring in response to a selection state change. * If present the call is occurring in response to a selection state change.
* This would be a good opportunity to animate changes between unselected and selected state. * This would be a good opportunity to animate changes between unselected and selected state.
* When state is being restored, this argument will not be present. * When state is being restored, this argument will not be present.
@ -110,7 +113,7 @@ public abstract class SelectionTracker<K> {
* may use an observer to control the enabled status of menu items, * may use an observer to control the enabled status of menu items,
* or to initiate {@link android.view.ActionMode}. * or to initiate {@link android.view.ActionMode}.
*/ */
public abstract void addObserver(SelectionObserver observer); public abstract void addObserver(@NonNull SelectionObserver<K> observer);
/** @return true if has a selection */ /** @return true if has a selection */
public abstract boolean hasSelection(); public abstract boolean hasSelection();
@ -123,7 +126,7 @@ public abstract class SelectionTracker<K> {
* of the selection that will not reflect future changes * of the selection that will not reflect future changes
* to selection. * to selection.
*/ */
public abstract Selection<K> getSelection(); public abstract @NonNull Selection<K> getSelection();
/** /**
* Updates {@code dest} to reflect the current selection. * Updates {@code dest} to reflect the current selection.
@ -144,9 +147,8 @@ public abstract class SelectionTracker<K> {
* This affords clients the ability to restore selection from selection saved * This affords clients the ability to restore selection from selection saved
* in Activity state. * in Activity state.
* *
* @see StorageStrategy details on selection state support.
*
* @param selection selection being restored. * @param selection selection being restored.
* @see StorageStrategy details on selection state support.
*/ */
protected abstract void restoreSelection(@NonNull Selection<K> selection); protected abstract void restoreSelection(@NonNull Selection<K> selection);
@ -181,7 +183,7 @@ public abstract class SelectionTracker<K> {
/** @hide */ /** @hide */
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
protected abstract AdapterDataObserver getAdapterDataObserver(); protected abstract @NonNull AdapterDataObserver getAdapterDataObserver();
/** /**
* Attempts to establish a range selection at {@code position}, selecting the item * Attempts to establish a range selection at {@code position}, selecting the item
@ -203,9 +205,9 @@ public abstract class SelectionTracker<K> {
* (see {@link #isRangeActive()}. Items in the range [anchor, end] will be * (see {@link #isRangeActive()}. Items in the range [anchor, end] will be
* selected after consulting SelectionPredicate. * selected after consulting SelectionPredicate.
* *
* @param position The new end position for the selection range. * @param position The new end position for the selection range.
* @throws IllegalStateException if a range selection is not active. Range selection * @throws IllegalStateException if a range selection is not active. Range selection
* must have been started by a call to {@link #startRange(int)}. * must have been started by a call to {@link #startRange(int)}.
* @hide * @hide
*/ */
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
@ -215,6 +217,7 @@ public abstract class SelectionTracker<K> {
* Clears an in-progress range selection. Provisional range selection established * Clears an in-progress range selection. Provisional range selection established
* using {@link #extendProvisionalRange(int)} will be cleared (unless * using {@link #extendProvisionalRange(int)} will be cleared (unless
* {@link #mergeProvisionalSelection()} is called first.) * {@link #mergeProvisionalSelection()} is called first.)
*
* @hide * @hide
*/ */
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
@ -251,7 +254,7 @@ public abstract class SelectionTracker<K> {
/** /**
* Sets the provisional selection, replacing any existing selection. * Sets the provisional selection, replacing any existing selection.
* @param newSelection *
* @hide * @hide
*/ */
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
@ -259,6 +262,7 @@ public abstract class SelectionTracker<K> {
/** /**
* Clears any existing provisional selection * Clears any existing provisional selection
*
* @hide * @hide
*/ */
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
@ -267,6 +271,7 @@ public abstract class SelectionTracker<K> {
/** /**
* Converts the provisional selection into primary selection, then clears * Converts the provisional selection into primary selection, then clears
* provisional selection. * provisional selection.
*
* @hide * @hide
*/ */
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
@ -300,6 +305,16 @@ public abstract class SelectionTracker<K> {
public void onItemStateChanged(@NonNull K key, boolean selected) { public void onItemStateChanged(@NonNull K key, boolean selected) {
} }
/**
* Called when Selection is cleared.
* TODO(smckay): Make public in a future public API.
*
* @hide
*/
@RestrictTo(LIBRARY)
protected void onSelectionCleared() {
}
/** /**
* Called when the underlying data set has changed. After this method is called * Called when the underlying data set has changed. After this method is called
* SelectionTracker will traverse the existing selection, * SelectionTracker will traverse the existing selection,
@ -338,7 +353,7 @@ public abstract class SelectionTracker<K> {
/** /**
* Validates a change to selection for a specific key. * Validates a change to selection for a specific key.
* *
* @param key the item key * @param key the item key
* @param nextState the next potential selected/unselected state * @param nextState the next potential selected/unselected state
* @return true if the item at {@code id} can be set to {@code nextState}. * @return true if the item at {@code id} can be set to {@code nextState}.
*/ */
@ -348,7 +363,7 @@ public abstract class SelectionTracker<K> {
* Validates a change to selection for a specific position. If necessary * Validates a change to selection for a specific position. If necessary
* use {@link ItemKeyProvider} to identy associated key. * use {@link ItemKeyProvider} to identy associated key.
* *
* @param position the item position * @param position the item position
* @param nextState the next potential selected/unselected state * @param nextState the next potential selected/unselected state
* @return true if the item at {@code id} can be set to {@code nextState}. * @return true if the item at {@code id} can be set to {@code nextState}.
*/ */
@ -384,7 +399,7 @@ public abstract class SelectionTracker<K> {
* new MyDetailsLookup(recyclerView), * new MyDetailsLookup(recyclerView),
* StorageStrategy.createParcelableStorage(Uri.class)) * StorageStrategy.createParcelableStorage(Uri.class))
* .build(); * .build();
*</pre> * </pre>
* *
* <p> * <p>
* <b>Restricting which items can be selected and limiting selection size</b> * <b>Restricting which items can be selected and limiting selection size</b>
@ -399,14 +414,14 @@ public abstract class SelectionTracker<K> {
* by supplying {@link SelectionPredicates#createSelectSingleAnything()}. * by supplying {@link SelectionPredicates#createSelectSingleAnything()}.
* *
* SelectionTracker<String> tracker = new SelectionTracker.Builder<>( * SelectionTracker<String> tracker = new SelectionTracker.Builder<>(
* "my-string-selection", * "my-string-selection",
* recyclerView, * recyclerView,
* new DemoStableIdProvider(recyclerView.getAdapter()), * new DemoStableIdProvider(recyclerView.getAdapter()),
* new MyDetailsLookup(recyclerView), * new MyDetailsLookup(recyclerView),
* StorageStrategy.createStringStorage()) * StorageStrategy.createStringStorage())
* .withSelectionPredicate(SelectionPredicates#createSelectSingleAnything()) * .withSelectionPredicate(SelectionPredicates#createSelectSingleAnything())
* .build(); * .build();
*</pre> * </pre>
* <p> * <p>
* <b>Retaining state across Android lifecycle events</b> * <b>Retaining state across Android lifecycle events</b>
* *
@ -447,25 +462,25 @@ public abstract class SelectionTracker<K> {
* private SelectionTracker<Uri> mTracker; * private SelectionTracker<Uri> mTracker;
* *
* public void onCreate(Bundle savedInstanceState) { * public void onCreate(Bundle savedInstanceState) {
* // See above for details on constructing a SelectionTracker instance. * // See above for details on constructing a SelectionTracker instance.
* *
* if (savedInstanceState != null) { * if (savedInstanceState != null) {
* mTracker.onRestoreInstanceState(savedInstanceState); * mTracker.onRestoreInstanceState(savedInstanceState);
* } * }
* } * }
* *
* protected void onSaveInstanceState(Bundle outState) { * protected void onSaveInstanceState(Bundle outState) {
* super.onSaveInstanceState(outState); * super.onSaveInstanceState(outState);
* mTracker.onSaveInstanceState(outState); * mTracker.onSaveInstanceState(outState);
* } * }
* </pre> * </pre>
* *
* @param <K> Selection key type. Built in support is provided for {@link String}, * @param <K> Selection key type. Built in support is provided for {@link String},
* {@link Long}, and {@link Parcelable}. {@link StorageStrategy} * {@link Long}, and {@link Parcelable}. {@link StorageStrategy}
* provides factory methods for each type: * provides factory methods for each type:
* {@link StorageStrategy#createStringStorage()}, * {@link StorageStrategy#createStringStorage()},
* {@link StorageStrategy#createParcelableStorage(Class)}, * {@link StorageStrategy#createParcelableStorage(Class)},
* {@link StorageStrategy#createLongStorage()} * {@link StorageStrategy#createLongStorage()}
*/ */
public static final class Builder<K> { public static final class Builder<K> {
@ -490,12 +505,12 @@ public abstract class SelectionTracker<K> {
private BandPredicate mBandPredicate; private BandPredicate mBandPredicate;
private int mBandOverlayId = eu.faircode.email.R.drawable.selection_band_overlay; private int mBandOverlayId = eu.faircode.email.R.drawable.selection_band_overlay;
private int[] mGestureToolTypes = new int[] { // TODO(b/144500333): Remove support for overriding gesture and pointer tooltypes.
MotionEvent.TOOL_TYPE_FINGER, private int[] mGestureToolTypes = new int[]{
MotionEvent.TOOL_TYPE_UNKNOWN MotionEvent.TOOL_TYPE_FINGER
}; };
private int[] mPointerToolTypes = new int[] { private int[] mPointerToolTypes = new int[]{
MotionEvent.TOOL_TYPE_MOUSE MotionEvent.TOOL_TYPE_MOUSE
}; };
@ -503,13 +518,13 @@ public abstract class SelectionTracker<K> {
* Creates a new SelectionTracker.Builder useful for configuring and creating * Creates a new SelectionTracker.Builder useful for configuring and creating
* a new SelectionTracker for use with your {@link RecyclerView}. * a new SelectionTracker for use with your {@link RecyclerView}.
* *
* @param selectionId A unique string identifying this selection in the context * @param selectionId A unique string identifying this selection in the context
* of the activity or fragment. * of the activity or fragment.
* @param recyclerView the owning RecyclerView * @param recyclerView the owning RecyclerView
* @param keyProvider the source of selection keys * @param keyProvider the source of selection keys
* @param detailsLookup the source of information about RecyclerView items. * @param detailsLookup the source of information about RecyclerView items.
* @param storage Strategy for type-safe storage of selection state in * @param storage Strategy for type-safe storage of selection state in
* {@link Bundle}. * {@link Bundle}.
*/ */
public Builder( public Builder(
@NonNull String selectionId, @NonNull String selectionId,
@ -545,7 +560,7 @@ public abstract class SelectionTracker<K> {
* @param predicate the predicate to be used. * @param predicate the predicate to be used.
* @return this * @return this
*/ */
public Builder<K> withSelectionPredicate( public @NonNull Builder<K> withSelectionPredicate(
@NonNull SelectionPredicate<K> predicate) { @NonNull SelectionPredicate<K> predicate) {
checkArgument(predicate != null); checkArgument(predicate != null);
@ -560,7 +575,7 @@ public abstract class SelectionTracker<K> {
* @param monitor the monitor to be used * @param monitor the monitor to be used
* @return this * @return this
*/ */
public Builder<K> withOperationMonitor( public @NonNull Builder<K> withOperationMonitor(
@NonNull OperationMonitor monitor) { @NonNull OperationMonitor monitor) {
checkArgument(monitor != null); checkArgument(monitor != null);
@ -574,7 +589,7 @@ public abstract class SelectionTracker<K> {
* @param delegate the delegate to be used * @param delegate the delegate to be used
* @return this * @return this
*/ */
public Builder<K> withFocusDelegate(@NonNull FocusDelegate<K> delegate) { public @NonNull Builder<K> withFocusDelegate(@NonNull FocusDelegate<K> delegate) {
checkArgument(delegate != null); checkArgument(delegate != null);
mFocusDelegate = delegate; mFocusDelegate = delegate;
return this; return this;
@ -586,7 +601,7 @@ public abstract class SelectionTracker<K> {
* @param listener the listener to be used * @param listener the listener to be used
* @return this * @return this
*/ */
public Builder<K> withOnItemActivatedListener( public @NonNull Builder<K> withOnItemActivatedListener(
@NonNull OnItemActivatedListener<K> listener) { @NonNull OnItemActivatedListener<K> listener) {
checkArgument(listener != null); checkArgument(listener != null);
@ -601,7 +616,7 @@ public abstract class SelectionTracker<K> {
* @param listener the listener to be used * @param listener the listener to be used
* @return this * @return this
*/ */
public Builder<K> withOnContextClickListener( public @NonNull Builder<K> withOnContextClickListener(
@NonNull OnContextClickListener listener) { @NonNull OnContextClickListener listener) {
checkArgument(listener != null); checkArgument(listener != null);
@ -616,7 +631,7 @@ public abstract class SelectionTracker<K> {
* @param listener the listener to be used * @param listener the listener to be used
* @return this * @return this
*/ */
public Builder<K> withOnDragInitiatedListener( public @NonNull Builder<K> withOnDragInitiatedListener(
@NonNull OnDragInitiatedListener listener) { @NonNull OnDragInitiatedListener listener) {
checkArgument(listener != null); checkArgument(listener != null);
@ -627,12 +642,17 @@ public abstract class SelectionTracker<K> {
/** /**
* Replaces default tap and gesture tool-types. Defaults are: * Replaces default tap and gesture tool-types. Defaults are:
* {@link MotionEvent#TOOL_TYPE_FINGER} and {@link MotionEvent#TOOL_TYPE_UNKNOWN}. * {@link MotionEvent#TOOL_TYPE_FINGER}.
* *
* @param toolTypes the tool types to be used * @param toolTypes the tool types to be used
* @return this * @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.
*/ */
public Builder<K> withGestureTooltypes(int... toolTypes) { @Deprecated
public @NonNull Builder<K> withGestureTooltypes(@NonNull int... toolTypes) {
Log.w(TAG, "Setting gestureTooltypes is likely to result in unexpected behavior.");
mGestureToolTypes = toolTypes; mGestureToolTypes = toolTypes;
return this; return this;
} }
@ -640,22 +660,19 @@ public abstract class SelectionTracker<K> {
/** /**
* Replaces default band overlay. * Replaces default band overlay.
* *
* @param bandOverlayId
* @return this * @return this
*/ */
public Builder<K> withBandOverlay(@DrawableRes int bandOverlayId) { public @NonNull Builder<K> withBandOverlay(@DrawableRes int bandOverlayId) {
mBandOverlayId = bandOverlayId; mBandOverlayId = bandOverlayId;
return this; return this;
} }
/** /**
* Replaces default band predicate. * Replaces default band predicate.
* @param bandPredicate *
* @return this * @return this
*/ */
public Builder<K> withBandPredicate(@NonNull BandPredicate bandPredicate) { public @NonNull Builder<K> withBandPredicate(@NonNull BandPredicate bandPredicate) {
checkArgument(bandPredicate != null);
mBandPredicate = bandPredicate; mBandPredicate = bandPredicate;
return this; return this;
} }
@ -668,8 +685,13 @@ public abstract class SelectionTracker<K> {
* *
* @param toolTypes the tool types to be used * @param toolTypes the tool types to be used
* @return this * @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.
*/ */
public Builder<K> withPointerTooltypes(int... toolTypes) { @Deprecated
public @NonNull Builder<K> withPointerTooltypes(@NonNull int... toolTypes) {
Log.w(TAG, "Setting pointerTooltypes is likely to result in unexpected behavior.");
mPointerToolTypes = toolTypes; mPointerToolTypes = toolTypes;
return this; return this;
} }
@ -679,9 +701,9 @@ public abstract class SelectionTracker<K> {
* *
* @return this * @return this
*/ */
public SelectionTracker<K> build() { public @NonNull SelectionTracker<K> build() {
SelectionTracker<K> tracker = new DefaultSelectionTracker<>( DefaultSelectionTracker<K> tracker = new DefaultSelectionTracker<>(
mSelectionId, mKeyProvider, mSelectionPredicate, mStorage); mSelectionId, mKeyProvider, mSelectionPredicate, mStorage);
// Event glue between RecyclerView and SelectionTracker keeps the classes separate // Event glue between RecyclerView and SelectionTracker keeps the classes separate
@ -689,6 +711,8 @@ public abstract class SelectionTracker<K> {
// represent the same data in different ways. // represent the same data in different ways.
EventBridge.install(mAdapter, tracker, mKeyProvider); EventBridge.install(mAdapter, tracker, mKeyProvider);
// Scroller is stateful and can be reset, but we don't manage it directly.
// GestureSelectionHelper will reset scroller when it is reset.
AutoScroller scroller = AutoScroller scroller =
new ViewAutoScroller(ViewAutoScroller.createScrollHost(mRecyclerView)); new ViewAutoScroller(ViewAutoScroller.createScrollHost(mRecyclerView));
@ -698,14 +722,10 @@ public abstract class SelectionTracker<K> {
// GestureRouter is responsible for routing GestureDetector events // GestureRouter is responsible for routing GestureDetector events
// to tool-type specific handlers. // to tool-type specific handlers.
GestureRouter<MotionInputHandler> gestureRouter = new GestureRouter<>(); GestureRouter<MotionInputHandler<K>> gestureRouter = new GestureRouter<>();
GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter);
// TouchEventRouter takes its name from RecyclerView#OnItemTouchListener. // GestureDetector cancels itself in response to ACTION_CANCEL events.
// Despite "Touch" being in the name, it receives events for all types of tools. GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter);
// This class is responsible for routing events to tool-type specific handlers,
// and if not handled by a handler, on to a GestureDetector for analysis.
TouchEventRouter eventRouter = new TouchEventRouter(gestureDetector);
// GestureSelectionHelper provides logic that interprets a combination // GestureSelectionHelper provides logic that interprets a combination
// of motions and gestures in order to provide gesture driven selection support // of motions and gestures in order to provide gesture driven selection support
@ -713,8 +733,37 @@ public abstract class SelectionTracker<K> {
final GestureSelectionHelper gestureHelper = GestureSelectionHelper.create( final GestureSelectionHelper gestureHelper = GestureSelectionHelper.create(
tracker, mSelectionPredicate, mRecyclerView, scroller, mMonitor); tracker, mSelectionPredicate, mRecyclerView, scroller, mMonitor);
// Finally hook the framework up to listening to recycle view events. // EventRouter receives events for RecyclerView, dispatching to handlers
// registered by tool-type.
EventRouter eventRouter = new EventRouter();
// Finally hook the framework up to listening to RecycleView events.
mRecyclerView.addOnItemTouchListener(eventRouter); mRecyclerView.addOnItemTouchListener(eventRouter);
mRecyclerView.addOnItemTouchListener(
new GestureDetectorOnItemTouchListenerAdapter(gestureDetector));
// Reset manager listens for cancel events from RecyclerView. In response to that it
// advises other classes it is time to reset state.
ResetManager<K> resetMgr = new ResetManager<>();
// Register ResetManager to:
//
// 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());
// 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());
resetMgr.addResetHandler(tracker);
resetMgr.addResetHandler(mMonitor.asResettable());
resetMgr.addResetHandler(gestureHelper);
// But before you move on, there's more work to do. Event plumbing has been // 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. // installed, but we haven't registered any of our helpers or callbacks.
@ -757,7 +806,7 @@ public abstract class SelectionTracker<K> {
// Provides high level glue for binding touch events // Provides high level glue for binding touch events
// and gestures to selection framework. // and gestures to selection framework.
TouchInputHandler<K> touchHandler = new TouchInputHandler<K>( TouchInputHandler<K> touchHandler = new TouchInputHandler<>(
tracker, tracker,
mKeyProvider, mKeyProvider,
mDetailsLookup, mDetailsLookup,
@ -768,8 +817,8 @@ public abstract class SelectionTracker<K> {
if (mSelectionPredicate.canSelectMultiple()) { if (mSelectionPredicate.canSelectMultiple()) {
try { try {
gestureHelper.start(); gestureHelper.start();
} catch (IllegalStateException ex) { } catch (Throwable ex) {
eu.faircode.email.Log.w(ex); eu.faircode.email.Log.e(ex);
} }
} }
} }
@ -786,7 +835,7 @@ public abstract class SelectionTracker<K> {
for (int toolType : mGestureToolTypes) { for (int toolType : mGestureToolTypes) {
gestureRouter.register(toolType, touchHandler); gestureRouter.register(toolType, touchHandler);
eventRouter.register(toolType, gestureHelper); eventRouter.set(toolType, gestureHelper);
} }
// Provides high level glue for binding mouse events and gestures // Provides high level glue for binding mouse events and gestures
@ -803,7 +852,7 @@ public abstract class SelectionTracker<K> {
gestureRouter.register(toolType, mouseHandler); gestureRouter.register(toolType, mouseHandler);
} }
@Nullable BandSelectionHelper bandHelper = null; @Nullable BandSelectionHelper<K> bandHelper = null;
// Band selection not supported in single select mode, or when key access // Band selection not supported in single select mode, or when key access
// is limited to anything less than the entire corpus. // is limited to anything less than the entire corpus.
@ -824,14 +873,14 @@ public abstract class SelectionTracker<K> {
mBandPredicate, mBandPredicate,
mFocusDelegate, mFocusDelegate,
mMonitor); mMonitor);
resetMgr.addResetHandler(bandHelper);
} }
OnItemTouchListener pointerEventHandler = new PointerDragEventInterceptor( OnItemTouchListener pointerEventHandler = new PointerDragEventInterceptor(
mDetailsLookup, mOnDragInitiatedListener, bandHelper); mDetailsLookup, mOnDragInitiatedListener, bandHelper);
for (int toolType : mPointerToolTypes) { eventRouter.set(MotionEvent.TOOL_TYPE_MOUSE, pointerEventHandler);
eventRouter.register(toolType, pointerEventHandler);
}
return tracker; return tracker;
} }

View File

@ -39,11 +39,11 @@ import java.util.ArrayList;
* for more detailed advice on which key type to use for your selection keys. * for more detailed advice on which key type to use for your selection keys.
* *
* @param <K> Selection key type. Built in support is provided for String, Long, and Parcelable * @param <K> Selection key type. Built in support is provided for String, Long, and Parcelable
* types. Use the respective factory method to create a StorageStrategy instance * types. Use the respective factory method to create a StorageStrategy instance
* appropriate to the desired type. * appropriate to the desired type.
* {@link #createStringStorage()}, * {@link #createStringStorage()},
* {@link #createParcelableStorage(Class)}, * {@link #createParcelableStorage(Class)},
* {@link #createLongStorage()} * {@link #createLongStorage()}
*/ */
public abstract class StorageStrategy<K> { public abstract class StorageStrategy<K> {
@ -69,7 +69,6 @@ public abstract class StorageStrategy<K> {
* Create a {@link Selection} from supplied {@link Bundle}. * Create a {@link Selection} from supplied {@link Bundle}.
* *
* @param state Bundle instance that may contain parceled Selection instance. * @param state Bundle instance that may contain parceled Selection instance.
* @return
*/ */
public abstract @Nullable Selection<K> asSelection(@NonNull Bundle state); public abstract @Nullable Selection<K> asSelection(@NonNull Bundle state);
@ -77,7 +76,6 @@ public abstract class StorageStrategy<K> {
* Creates a {@link Bundle} from supplied {@link Selection}. * Creates a {@link Bundle} from supplied {@link Selection}.
* *
* @param selection The selection to asBundle. * @param selection The selection to asBundle.
* @return
*/ */
public abstract @NonNull Bundle asBundle(@NonNull Selection<K> selection); public abstract @NonNull Bundle asBundle(@NonNull Selection<K> selection);
@ -89,21 +87,22 @@ public abstract class StorageStrategy<K> {
* @return StorageStrategy suitable for use with {@link Parcelable} keys * @return StorageStrategy suitable for use with {@link Parcelable} keys
* (like {@link android.net.Uri}). * (like {@link android.net.Uri}).
*/ */
public static <K extends Parcelable> StorageStrategy<K> createParcelableStorage(Class<K> type) { public static @NonNull <K extends Parcelable> StorageStrategy<K> createParcelableStorage(
return new ParcelableStorageStrategy(type); @NonNull Class<K> type) {
return new ParcelableStorageStrategy<>(type);
} }
/** /**
* @return StorageStrategy suitable for use with {@link String} keys. * @return StorageStrategy suitable for use with {@link String} keys.
*/ */
public static StorageStrategy<String> createStringStorage() { public static @NonNull StorageStrategy<String> createStringStorage() {
return new StringStorageStrategy(); return new StringStorageStrategy();
} }
/** /**
* @return StorageStrategy suitable for use with {@link Long} keys. * @return StorageStrategy suitable for use with {@link Long} keys.
*/ */
public static StorageStrategy<Long> createLongStorage() { public static @NonNull StorageStrategy<Long> createLongStorage() {
return new LongStorageStrategy(); return new LongStorageStrategy();
} }
@ -191,7 +190,7 @@ public abstract class StorageStrategy<K> {
private static class ParcelableStorageStrategy<K extends Parcelable> private static class ParcelableStorageStrategy<K extends Parcelable>
extends StorageStrategy<K> { extends StorageStrategy<K> {
ParcelableStorageStrategy(Class<K> type) { ParcelableStorageStrategy(@NonNull Class<K> type) {
super(type); super(type);
checkArgument(Parcelable.class.isAssignableFrom(type)); checkArgument(Parcelable.class.isAssignableFrom(type));
} }

View File

@ -30,29 +30,27 @@ import java.util.List;
/** /**
* Registry for tool specific event handler. This provides map like functionality, * Registry for tool specific event handler. This provides map like functionality,
* along with fallback to a default handler, while avoiding auto-boxing of tool * along with fallback to a default handler, while avoiding auto-boxing of tool
* type values that would be necessitated where a Map used. * 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. * @param <T> type of item being registered.
*/ */
final class ToolHandlerRegistry<T> { final class ToolHandlerRegistry<T> {
// Currently there are four known input types. ERASER is the last one, so has the // list with one null entry for each known tooltype (0-4).
// highest value. UNKNOWN is zero, so we add one. This allows delegates to be // See MotionEvent.TOOL_TYPE_ERASER for details. We're using a list here because
// registered by type, and avoid the auto-boxing that would be necessary were we // it is parameterized type friendly, and a natural container given that
// to store delegates in a Map<Integer, Delegate>. // the index values are 0-based ints.
private static final int NUM_INPUT_TYPES = MotionEvent.TOOL_TYPE_ERASER + 1;
private final List<T> mHandlers = Arrays.asList(null, null, null, null, null); private final List<T> mHandlers = Arrays.asList(null, null, null, null, null);
private final T mDefault; private final T mDefault;
ToolHandlerRegistry(@NonNull T defaultDelegate) { ToolHandlerRegistry(@NonNull T defaultDelegate) {
checkArgument(defaultDelegate != null); checkArgument(defaultDelegate != null);
mDefault = defaultDelegate; mDefault = defaultDelegate;
// Initialize all values to null.
for (int i = 0; i < NUM_INPUT_TYPES; i++) {
mHandlers.set(i, null);
}
} }
/** /**

View File

@ -1,113 +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 android.view.GestureDetector;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
/**
* A class responsible for routing MotionEvents to tool-type specific handlers,
* and if not handled by a handler, on to a {@link GestureDetector} for further
* processing.
*
* <p>
* TouchEventRouter takes its name from
* {@link RecyclerView#addOnItemTouchListener(OnItemTouchListener)}. Despite "Touch"
* being in the name, it receives MotionEvents for all types of tools.
*/
final class TouchEventRouter implements OnItemTouchListener {
private static final String TAG = "TouchEventRouter";
private final GestureDetector mDetector;
private final ToolHandlerRegistry<OnItemTouchListener> mDelegates;
TouchEventRouter(
@NonNull GestureDetector detector, @NonNull OnItemTouchListener defaultDelegate) {
checkArgument(detector != null);
checkArgument(defaultDelegate != null);
mDetector = detector;
mDelegates = new ToolHandlerRegistry<>(defaultDelegate);
}
TouchEventRouter(@NonNull GestureDetector detector) {
this(
detector,
// Supply a fallback listener does nothing...because the caller
// didn't supply a fallback.
new OnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent(
@NonNull RecyclerView unused, @NonNull MotionEvent e) {
return false;
}
@Override
public void onTouchEvent(
@NonNull RecyclerView unused, @NonNull MotionEvent e) {
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
});
}
/**
* @param toolType See MotionEvent for details on available types.
* @param delegate An {@link OnItemTouchListener} to receive events
* of {@code toolType}.
*/
void register(int toolType, @NonNull OnItemTouchListener delegate) {
checkArgument(delegate != null);
mDelegates.set(toolType, delegate);
}
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
boolean handled = mDelegates.get(e).onInterceptTouchEvent(rv, e);
// Forward all events to UserInputHandler.
// This is necessary since UserInputHandler needs to always see the first DOWN event. Or
// else all future UP events will be tossed.
handled |= mDetector.onTouchEvent(e);
return handled;
}
@Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
mDelegates.get(e).onTouchEvent(rv, e);
// Note: even though this event is being handled as part of gestures such as drag and band,
// continue forwarding to the GestureDetector. The detector needs to see the entire cluster
// of events in order to properly interpret other gestures, such as long press.
mDetector.onTouchEvent(e);
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
}

View File

@ -17,6 +17,7 @@
package androidx.recyclerview.selection; package androidx.recyclerview.selection;
import static androidx.core.util.Preconditions.checkArgument; import static androidx.core.util.Preconditions.checkArgument;
import static androidx.recyclerview.selection.Shared.DEBUG;
import android.util.Log; import android.util.Log;
import android.view.MotionEvent; import android.view.MotionEvent;
@ -36,7 +37,6 @@ import androidx.recyclerview.widget.RecyclerView;
final class TouchInputHandler<K> extends MotionInputHandler<K> { final class TouchInputHandler<K> extends MotionInputHandler<K> {
private static final String TAG = "TouchInputDelegate"; private static final String TAG = "TouchInputDelegate";
private static final boolean DEBUG = false;
private final ItemDetailsLookup<K> mDetailsLookup; private final ItemDetailsLookup<K> mDetailsLookup;
private final SelectionPredicate<K> mSelectionPredicate; private final SelectionPredicate<K> mSelectionPredicate;
@ -75,6 +75,11 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
@Override @Override
public boolean onSingleTapUp(@NonNull MotionEvent e) { public boolean onSingleTapUp(@NonNull MotionEvent e) {
if (DEBUG) {
checkArgument(MotionEvents.isFingerEvent(e));
checkArgument(MotionEvents.isActionUp(e));
}
if (!mDetailsLookup.overItemWithSelectionKey(e)) { if (!mDetailsLookup.overItemWithSelectionKey(e)) {
if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection."); if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection.");
mSelectionTracker.clearSelection(); mSelectionTracker.clearSelection();
@ -113,6 +118,11 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
@Override @Override
public void onLongPress(@NonNull MotionEvent e) { public void onLongPress(@NonNull MotionEvent e) {
if (DEBUG) {
checkArgument(MotionEvents.isFingerEvent(e));
checkArgument(MotionEvents.isActionDown(e));
}
if (!mDetailsLookup.overItemWithSelectionKey(e)) { if (!mDetailsLookup.overItemWithSelectionKey(e)) {
if (DEBUG) Log.d(TAG, "Ignoring LongPress on non-model-backed item."); if (DEBUG) Log.d(TAG, "Ignoring LongPress on non-model-backed item.");
return; return;
@ -124,8 +134,6 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
return; return;
} }
boolean handled = false;
if (shouldExtendRange(e)) { if (shouldExtendRange(e)) {
extendSelectionRange(item); extendSelectionRange(item);
mHapticPerformer.run(); mHapticPerformer.run();

View File

@ -97,7 +97,7 @@
* </b> * </b>
* *
* <p> * <p>
* Example usage (with {@code Long} selection keys: * Example usage (with {@code Long} selection keys):
* <pre>SelectionTracker<Long> tracker = new SelectionTracker.Builder<>( * <pre>SelectionTracker<Long> tracker = new SelectionTracker.Builder<>(
* "my-selection-id", * "my-selection-id",
* recyclerView, * recyclerView,

View File

@ -1,34 +1,34 @@
diff --git a/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java b/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java diff --git a/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java b/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
index 76087ea4..121fbd14 100644 index 50cc4ceb7..910935a0a 100644
--- a/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java --- a/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
+++ b/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java +++ b/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
@@ -488,7 +488,7 @@ public abstract class SelectionTracker<K> { @@ -503,7 +503,7 @@ public abstract class SelectionTracker<K> {
private OnContextClickListener mOnContextClickListener; private OnContextClickListener mOnContextClickListener;
private BandPredicate mBandPredicate; private BandPredicate mBandPredicate;
- private int mBandOverlayId = R.drawable.selection_band_overlay; - private int mBandOverlayId = R.drawable.selection_band_overlay;
+ private int mBandOverlayId = eu.faircode.email.R.drawable.selection_band_overlay; + private int mBandOverlayId = eu.faircode.email.R.drawable.selection_band_overlay;
private int[] mGestureToolTypes = new int[] { // TODO(b/144500333): Remove support for overriding gesture and pointer tooltypes.
MotionEvent.TOOL_TYPE_FINGER, private int[] mGestureToolTypes = new int[]{
@@ -766,7 +766,11 @@ public abstract class SelectionTracker<K> { @@ -815,7 +815,11 @@ public abstract class SelectionTracker<K> {
@Override @Override
public void run() { public void run() {
if (mSelectionPredicate.canSelectMultiple()) { if (mSelectionPredicate.canSelectMultiple()) {
- gestureHelper.start(); - gestureHelper.start();
+ try { + try {
+ gestureHelper.start(); + gestureHelper.start();
+ } catch (IllegalStateException ex) { + } catch (Throwable ex) {
+ ex.printStackTrace(); + eu.faircode.email.Log.e(ex);
+ } + }
} }
} }
}, },
diff --git a/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java b/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java diff --git a/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java b/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java
index d82812cc..48db78ec 100644 index be4c3fa0b..c434ced07 100644
--- a/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java --- a/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java
+++ b/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java +++ b/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java
@@ -107,6 +107,11 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> { @@ -112,6 +112,11 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
} }
@Override @Override
@ -38,5 +38,5 @@ index d82812cc..48db78ec 100644
+ +
+ @Override + @Override
public void onLongPress(@NonNull MotionEvent e) { public void onLongPress(@NonNull MotionEvent e) {
if (!mDetailsLookup.overItemWithSelectionKey(e)) { if (DEBUG) {
if (DEBUG) Log.d(TAG, "Ignoring LongPress on non-model-backed item."); checkArgument(MotionEvents.isFingerEvent(e));