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'])
def core_version = "1.2.0-rc01"
def appcompat_version = "1.1.0"
def fragment_version = "1.2.0-rc02"
def appcompat_version = "1.2.0-alpha01"
def fragment_version = "1.2.0-rc03"
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 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 swiperefresh_version = "1.0.0"
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 paging_version = "2.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-selection
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
implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorlayout_version"
@ -260,7 +261,7 @@ dependencies {
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-common:$room_version" // because of exclude
// 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"
// 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}.
*/
public abstract boolean canInitiate(MotionEvent e);
public abstract boolean canInitiate(@NonNull MotionEvent e);
@SuppressWarnings("WeakerAccess") /* synthetic access */
static boolean hasSupportedLayoutManager(@NonNull RecyclerView recyclerView) {
@ -107,16 +107,16 @@ public abstract class BandPredicate {
public static final class NonDraggableArea extends BandPredicate {
private final RecyclerView mRecyclerView;
private final ItemDetailsLookup mDetailsLookup;
private final ItemDetailsLookup<?> mDetailsLookup;
/**
* Creates a new instance.
*
* @param recyclerView the owner RecyclerView
* @param recyclerView the owner RecyclerView
* @param detailsLookup provides access to item details.
*/
public NonDraggableArea(
@NonNull RecyclerView recyclerView, @NonNull ItemDetailsLookup detailsLookup) {
@NonNull RecyclerView recyclerView, @NonNull ItemDetailsLookup<?> detailsLookup) {
checkArgument(recyclerView != null);
checkArgument(detailsLookup != null);
@ -132,7 +132,8 @@ public abstract class BandPredicate {
return false;
}
@Nullable ItemDetailsLookup.ItemDetails details = mDetailsLookup.getItemDetails(e);
@Nullable ItemDetailsLookup.ItemDetails<?> details =
mDetailsLookup.getItemDetails(e);
return (details == null) || !details.inDragRegion(e);
}
}

View File

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

View File

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

View File

@ -46,17 +46,17 @@ import java.util.Set;
* {@link SelectionPredicate#canSelectMultiple()}.
*
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*
* @hide
*/
@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 EXTRA_SELECTION_PREFIX = "androidx.recyclerview.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 SelectionPredicate<K> mSelectionPredicate;
private final StorageStrategy<K> mStorage;
@ -70,16 +70,16 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
/**
* Creates a new instance.
*
* @param selectionId A unique string identifying this selection in the context
* of the activity or fragment.
* @param keyProvider client supplied class providing access to stable ids.
* @param selectionId A unique string identifying this selection in the context
* of the activity or fragment.
* @param keyProvider client supplied class providing access to stable ids.
* @param selectionPredicate A predicate allowing the client to disallow selection
* @param storage Strategy for storing typed selection in bundle.
* @param storage Strategy for storing typed selection in bundle.
*/
public DefaultSelectionTracker(
@NonNull String selectionId,
@NonNull ItemKeyProvider keyProvider,
@NonNull SelectionPredicate selectionPredicate,
@NonNull ItemKeyProvider<K> keyProvider,
@NonNull SelectionPredicate<K> selectionPredicate,
@NonNull StorageStrategy<K> storage) {
checkArgument(selectionId != null);
@ -101,23 +101,26 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
}
@Override
public void addObserver(@NonNull SelectionObserver callback) {
public void addObserver(@NonNull SelectionObserver<K> callback) {
checkArgument(callback != null);
mObservers.add(callback);
}
/**
* @return true if there is a primary or previsional selection.
*/
@Override
public boolean hasSelection() {
return !mSelection.isEmpty();
}
@Override
public Selection getSelection() {
public @NonNull Selection<K> getSelection() {
return mSelection;
}
@Override
public void copySelection(@NonNull MutableSelection dest) {
public void copySelection(@NonNull MutableSelection<K> dest) {
dest.copyFrom(mSelection);
}
@ -127,7 +130,7 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
}
@Override
protected void restoreSelection(@NonNull Selection other) {
protected void restoreSelection(@NonNull Selection<K> other) {
checkArgument(other != null);
setItemsSelectedQuietly(other.mSelection, true);
// 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) {
boolean changed = false;
for (K key: keys) {
for (K key : keys) {
boolean itemChanged = selected
? canSetState(key, true) && mSelection.add(key)
: canSetState(key, false) && mSelection.remove(key);
@ -158,11 +161,15 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
@Override
public boolean clearSelection() {
if (!hasSelection()) {
if (DEBUG) Log.d(TAG, "Ignoring clearSelection request. No selection.");
return false;
}
if (DEBUG) Log.d(TAG, "Handling clearSelection request.");
clearProvisionalSelection();
clearPrimarySelection();
notifySelectionCleared();
return true;
}
@ -171,7 +178,7 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
return;
}
Selection prev = clearSelectionQuietly();
Selection<K> prev = clearSelectionQuietly();
notifySelectionCleared(prev);
notifySelectionChanged();
}
@ -181,10 +188,10 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
* Returns items in previous selection. Callers are responsible for notifying
* listeners about changes.
*/
private Selection clearSelectionQuietly() {
private Selection<K> clearSelectionQuietly() {
mRange = null;
MutableSelection prevSelection = new MutableSelection();
MutableSelection<K> prevSelection = new MutableSelection();
if (hasSelection()) {
copySelection(prevSelection);
mSelection.clear();
@ -193,6 +200,18 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
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
public boolean select(@NonNull K key) {
checkArgument(key != null);
@ -208,7 +227,7 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
// Enforce single selection policy.
if (mSingleSelect && hasSelection()) {
Selection prev = clearSelectionQuietly();
Selection<K> prev = clearSelectionQuietly();
notifySelectionCleared(prev);
}
@ -277,8 +296,10 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
return;
}
if (DEBUG) Log.i(TAG, "Extending provision range to position: " + position);
checkState(isRangeActive(), "Range start point not set.");
if (DEBUG) {
Log.i(TAG, "Extending provision range to position: " + position);
checkState(isRangeActive(), "Range start point not set.");
}
extendRange(position, Range.TYPE_PROVISIONAL);
}
@ -291,13 +312,23 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
* point before calling on {@link #endRange()}.
*
* @param position The new end position for the selection range.
* @param type The type of selection the range should utilize.
* @param type The type of selection the range should utilize.
*/
private void extendRange(int position, @RangeType int type) {
checkState(isRangeActive(), "Range start point not set.");
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) {
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;
}
@ -316,7 +347,7 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
}
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());
}
@ -348,20 +379,16 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
return mRange != null;
}
boolean isOverlapping(int position, int count) {
return (mRange != null && mRange.isOverlapping(position, count));
}
private boolean canSetState(@NonNull K key, boolean nextState) {
return mSelectionPredicate.canSetStateForKey(key, nextState);
}
@Override
protected AdapterDataObserver getAdapterDataObserver() {
protected @NonNull AdapterDataObserver getAdapterDataObserver() {
return mAdapterObserver;
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
@SuppressWarnings({"WeakerAccess", "unchecked"}) /* synthetic access */
void onDataSetChanged() {
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) {
for (K key: selection.mSelection) {
for (K key : selection.mSelection) {
notifyItemStateChanged(key, false);
}
for (K key: selection.mProvisionalSelection) {
for (K key : selection.mProvisionalSelection) {
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 */
void updateForRegularRange(int begin, int end, boolean selected) {
checkArgument(end >= begin);
@ -514,7 +534,6 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
}
@Override
@SuppressWarnings("unchecked")
public final void onSaveInstanceState(@NonNull Bundle state) {
if (mSelection.isEmpty()) {
return;
@ -582,21 +601,17 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
@Override
public void onItemRangeInserted(int startPosition, int itemCount) {
if (mSelectionTracker.isOverlapping(startPosition, itemCount))
mSelectionTracker.endRange();
mSelectionTracker.endRange();
}
@Override
public void onItemRangeRemoved(int startPosition, int itemCount) {
if (mSelectionTracker.isOverlapping(startPosition, itemCount))
mSelectionTracker.endRange();
mSelectionTracker.endRange();
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
if (mSelectionTracker.isOverlapping(fromPosition, itemCount) ||
mSelectionTracker.isOverlapping(toPosition, itemCount))
mSelectionTracker.endRange();
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();
/**
* @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();

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);
}
@SuppressWarnings("unchecked")
GestureRouter() {
this((T) new SimpleOnGestureListener());
}

View File

@ -17,7 +17,8 @@
package androidx.recyclerview.selection;
import static androidx.core.util.Preconditions.checkArgument;
import static androidx.core.util.Preconditions.checkState;
import static androidx.recyclerview.selection.Shared.DEBUG;
import static androidx.recyclerview.selection.Shared.VERBOSE;
import android.util.Log;
import android.view.MotionEvent;
@ -36,7 +37,7 @@ import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
* when used in conjunction with RecyclerView and other classes in the ReyclerView
* selection support package.
*/
final class GestureSelectionHelper implements OnItemTouchListener {
final class GestureSelectionHelper implements OnItemTouchListener, Resettable {
private static final String TAG = "GestureSelectionHelper";
@ -76,16 +77,19 @@ final class GestureSelectionHelper implements OnItemTouchListener {
* Explicitly kicks off a gesture multi-select.
*/
void start() {
checkState(!mStarted);
// Partner code in MotionInputHandler ensures items
// are selected and range anchor initialized prior to
// start being called.
// Verify the truth of that statement here
// to make the implicit coupling less of a time bomb.
checkState(mSelectionMgr.isRangeActive());
mLock.checkStopped();
if (mStarted) {
if (DEBUG) {
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;
mLock.start();
@ -94,15 +98,20 @@ final class GestureSelectionHelper implements OnItemTouchListener {
@Override
/** @hide */
public boolean onInterceptTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
// MotionEvents that aren't ACTION_DOWN are only ever passed to either onInterceptTouchEvent
// or onTouchEvent; never to both, so events delivered to this method are effectively
// lost if we don't act on them in this method.
//
// 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.
if (mStarted) {
handleTouch(e);
onTouchEvent(unused, e);
}
// ACTION_CANCEL is associated with "TOOL_TYPE_UNKNOWN" and
// is handled in ResetManager.
switch (e.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
return mStarted;
default:
@ -113,27 +122,11 @@ final class GestureSelectionHelper implements OnItemTouchListener {
@Override
/** @hide */
public void onTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
// See handleTouch(MotionEvent) javadoc for explanation as to why this is correct.
handleTouch(e);
}
if (!mStarted) {
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()) {
Log.e(TAG,
"Internal state of GestureSelectionHelper out of sync w/ SelectionTracker "
@ -141,6 +134,8 @@ final class GestureSelectionHelper implements OnItemTouchListener {
endSelection();
}
// ACTION_CANCEL is associated with "TOOL_TYPE_UNKNOWN" and
// is handled in ResetManager.
switch (e.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
handleMoveEvent(e);
@ -148,9 +143,6 @@ final class GestureSelectionHelper implements OnItemTouchListener {
case MotionEvent.ACTION_UP:
handleUpEvent();
break;
case MotionEvent.ACTION_CANCEL:
handleCancelEvent();
break;
}
}
@ -167,17 +159,22 @@ final class GestureSelectionHelper implements OnItemTouchListener {
endSelection();
}
// Called when ACTION_CANCEL event is to be handled.
// This means this gesture selection is aborted, so reset everything and abandon provisional
// selection.
private void handleCancelEvent() {
mSelectionMgr.clearProvisionalSelection();
endSelection();
/**
* Immediately "Stops" active gesture selection, and resets all related state.
*/
@Override
public void reset() {
if (DEBUG) Log.d(TAG, "Received reset request.");
mStarted = false;
mScroller.reset();
}
@Override
public boolean isResetRequired() {
return mStarted;
}
private void endSelection() {
checkState(mStarted);
mStarted = false;
mScroller.reset();
mLock.stop();
@ -186,6 +183,11 @@ final class GestureSelectionHelper implements OnItemTouchListener {
// Call when an intercepted ACTION_MOVE event is passed down.
// At this point, we are sure user wants to gesture multi-select.
private void handleMoveEvent(@NonNull MotionEvent e) {
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);
if (mSelectionPredicate.canSetStateAtPosition(lastGlidedItemPos, true)) {
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
// item position.
final float inboundY = getInboundY(mRecyclerView.getHeight(), e.getY());
return (pastLastItem) ? mRecyclerView.getAdapter().getItemCount() - 1
return pastLastItem ? mRecyclerView.getAdapter().getItemCount() - 1
: mRecyclerView.getChildAdapterPosition(
mRecyclerView.findChildViewUnder(e.getX(), inboundY));
}

View File

@ -63,7 +63,7 @@ final class GridModel<K> {
private final ItemKeyProvider<K> mKeyProvider;
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
// 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;
@SuppressWarnings("unchecked")
GridModel(
GridHost host,
GridHost<K> host,
ItemKeyProvider<K> keyProvider,
SelectionPredicate<K> selectionPredicate) {
@ -284,8 +285,9 @@ final class GridModel<K> {
* mSelection, so computeCurrentSelection() should be called before this
* function.
*/
@SuppressWarnings("unchecked")
private void notifySelectionChanged() {
for (SelectionObserver listener : mOnSelectionChangedListeners) {
for (SelectionObserver<K> listener : mOnSelectionChangedListeners) {
listener.onSelectionChanged(mSelection);
}
}
@ -401,7 +403,7 @@ final class GridModel<K> {
abstract void onSelectionChanged(Set<K> updatedSelection);
}
void addOnSelectionChangedListener(SelectionObserver listener) {
void addOnSelectionChangedListener(SelectionObserver<K> listener) {
mOnSelectionChangedListeners.add(listener);
}

View File

@ -44,10 +44,10 @@ import androidx.recyclerview.widget.RecyclerView;
* mRecyclerView = recyclerView;
* }
*
* public ItemDetails<Uri> getItemDetails(MotionEvent e) {
* View view = mRecView.findChildViewUnder(e.getX(), e.getY());
* public @Nullable ItemDetails<Uri> getItemDetails(@NonNull MotionEvent e) {
* View view = mRecyclerView.findChildViewUnder(e.getX(), e.getY());
* if (view != null) {
* ViewHolder holder = mRecView.getChildViewHolder(view);
* ViewHolder holder = mRecyclerView.getChildViewHolder(view);
* if (holder instanceof MyHolder) {
* return ((MyHolder) holder).getItemDetails();
* }
@ -110,10 +110,6 @@ public abstract class ItemDetailsLookup<K> {
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.
*/
@ -241,10 +237,10 @@ public abstract class ItemDetailsLookup<K> {
@Override
public boolean equals(@Nullable Object obj) {
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();
boolean sameKeys = false;
if (key == null) {

View File

@ -33,6 +33,14 @@ final class MotionEvents {
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) {
return e.getActionMasked() == MotionEvent.ACTION_MOVE;
}

View File

@ -17,7 +17,6 @@
package androidx.recyclerview.selection;
import static androidx.core.util.Preconditions.checkArgument;
import static androidx.core.util.Preconditions.checkState;
import static androidx.recyclerview.selection.Shared.DEBUG;
import static androidx.recyclerview.selection.Shared.VERBOSE;
@ -123,7 +122,11 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
// tap on an item when there is an existing selection. We could extend
// a selection, we could clear selection (then launch)
private void onItemClick(@NonNull MotionEvent e, @NonNull ItemDetails<K> item) {
checkState(mSelectionTracker.hasSelection());
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);
if (shouldExtendRange(e)) {

View File

@ -16,6 +16,7 @@
package androidx.recyclerview.selection;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.core.util.Preconditions.checkArgument;
import static androidx.core.util.Preconditions.checkState;
import static androidx.recyclerview.selection.Shared.DEBUG;
@ -24,7 +25,7 @@ import android.util.Log;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.annotation.RestrictTo;
import java.util.ArrayList;
import java.util.List;
@ -36,7 +37,7 @@ import java.util.List;
*
* <p>
* 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.
*
* <p>
@ -46,17 +47,33 @@ public final class 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 List<OnChangeListener> mListeners = new ArrayList<>();
@MainThread
synchronized void start() {
mNumOps++;
if (mNumOps == 1) {
for (OnChangeListener l : mListeners) {
l.onChanged();
}
notifyStateChanged();
}
if (DEBUG) Log.v(TAG, "Incremented content lock count to " + mNumOps + ".");
@ -64,29 +81,46 @@ public final class OperationMonitor {
@MainThread
synchronized void stop() {
checkState(mNumOps > 0);
if (mNumOps == 0) {
if (DEBUG) Log.w(TAG, "Stop called whith opt count of 0.");
return;
}
mNumOps--;
if (DEBUG) Log.v(TAG, "Decremented content lock count to " + mNumOps + ".");
if (mNumOps == 0) {
for (OnChangeListener l : mListeners) {
l.onChanged();
}
notifyStateChanged();
}
}
/** @hide */
@RestrictTo(LIBRARY)
@MainThread
synchronized void reset() {
if (DEBUG) Log.d(TAG, "Received reset request.");
if (mNumOps > 0) {
Log.w(TAG, "Resetting OperationMonitor with " + mNumOps + " active operations.");
}
mNumOps = 0;
notifyStateChanged();
}
/** @hide */
@RestrictTo(LIBRARY)
synchronized boolean isResetRequired() {
return isStarted();
}
/**
* @return true if there are any running operations.
*/
@SuppressWarnings("unused")
public synchronized boolean isStarted() {
return mNumOps > 0;
}
/**
* Registers supplied listener to be notified when operation status changes.
* @param listener
*/
public void addListener(@NonNull OnChangeListener listener) {
checkArgument(listener != null);
@ -95,7 +129,6 @@ public final class OperationMonitor {
/**
* Unregisters listener for further notifications.
* @param listener
*/
public void removeListener(@NonNull OnChangeListener listener) {
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.
*/
void checkStarted() {
checkState(mNumOps > 0);
void checkStarted(boolean started) {
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() {
checkState(mNumOps == 0);
@RestrictTo(LIBRARY)
@NonNull Resettable asResettable() {
return mResettable;
}
/**

View File

@ -25,19 +25,19 @@ import androidx.recyclerview.widget.RecyclerView;
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}.
*
* <p>See {@link OnDragInitiatedListener} for details on implementing drag and drop.
*/
final class PointerDragEventInterceptor implements OnItemTouchListener {
private final ItemDetailsLookup mEventDetailsLookup;
private final ItemDetailsLookup<?> mEventDetailsLookup;
private final OnDragInitiatedListener mDragListener;
private @Nullable OnItemTouchListener mDelegate;
private OnItemTouchListener mDelegate;
PointerDragEventInterceptor(
ItemDetailsLookup eventDetailsLookup,
ItemDetailsLookup<?> eventDetailsLookup,
OnDragInitiatedListener dragListener,
@Nullable OnItemTouchListener delegate) {
@ -46,30 +46,29 @@ final class PointerDragEventInterceptor implements OnItemTouchListener {
mEventDetailsLookup = eventDetailsLookup;
mDragListener = dragListener;
mDelegate = delegate;
if (delegate != null) {
mDelegate = delegate;
} else {
mDelegate = new DummyOnItemTouchListener();
}
}
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
if (MotionEvents.isPointerDragEvent(e) && mEventDetailsLookup.inItemDragRegion(e)) {
return mDragListener.onDragInitiated(e);
} else if (mDelegate != null) {
return mDelegate.onInterceptTouchEvent(rv, e);
}
return false;
return mDelegate.onInterceptTouchEvent(rv, e);
}
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
if (mDelegate != null) {
mDelegate.onTouchEvent(rv, e);
}
mDelegate.onTouchEvent(rv, e);
}
@Override
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.
*/
static final int TYPE_PROVISIONAL = 1;
@IntDef({
TYPE_PRIMARY,
TYPE_PROVISIONAL
})
@Retention(RetentionPolicy.SOURCE)
@interface RangeType {}
@interface RangeType {
}
private static final String TAG = "Range";
@ -69,9 +71,6 @@ final class Range {
/**
* Creates a new range anchored at {@code position}.
*
* @param position
* @param callbacks
*/
Range(int position, @NonNull Callbacks callbacks) {
mBegin = position;
@ -118,7 +117,7 @@ final class Range {
} else if (mEnd < mBegin) {
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;
}
@ -170,11 +169,6 @@ final class Range {
mCallbacks.updateForRange(begin, end, selected, type);
}
boolean isOverlapping(int position, int count) {
return (position >= mBegin && position <= mEnd) ||
(position + count >= mBegin && position + count <= mEnd);
}
@Override
public String toString() {
return "Range{begin=" + mBegin + ", end=" + mEnd + "}";

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
* existing selection).
*
* @see MutableSelection
*
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
* @see MutableSelection
*/
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.
*/
public boolean contains(@Nullable K key) {
@ -92,7 +90,7 @@ public class Selection<K> implements Iterable<K> {
* {@inheritDoc}
*/
@Override
public Iterator<K> iterator() {
public @NonNull Iterator<K> 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,
* canceled, or adjusted at a later time. When a new provision selection is applied, the old
* one (if it exists) is abandoned.
*
* @return Map of ids added or removed. Added ids have a value of true, removed are false.
*/
Map<K, Boolean> setProvisionalSelection(@NonNull Set<K> newSelection) {
Map<K, Boolean> delta = new LinkedHashMap<>();
for (K key: mProvisionalSelection) {
for (K key : mProvisionalSelection) {
// Mark each item that used to be in the provisional selection
// but is not in the new provisional selection.
if (!newSelection.contains(key) && !mSelection.contains(key)) {
@ -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
// provisional selection.
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
// provisional selection.
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
// selection. This could not be done in the previous loops because changing the size of
// the selection mid-iteration changes iteration order erroneously.
for (Map.Entry<K, Boolean> entry: delta.entrySet()) {
for (Map.Entry<K, Boolean> entry : delta.entrySet()) {
K key = entry.getKey();
if (entry.getValue()) {
mProvisionalSelection.add(key);
@ -221,11 +220,11 @@ public class Selection<K> implements Iterable<K> {
StringBuilder buffer = new StringBuilder(size() * 28);
buffer.append("Selection{")
.append("primary{size=" + mSelection.size())
.append(", entries=" + mSelection)
.append("}, provisional{size=" + mProvisionalSelection.size())
.append(", entries=" + mProvisionalSelection)
.append("}}");
.append("primary{size=" + mSelection.size())
.append(", entries=" + mSelection)
.append("}, provisional{size=" + mProvisionalSelection.size())
.append(", entries=" + mProvisionalSelection)
.append("}}");
return buffer.toString();
}
@ -237,10 +236,10 @@ public class Selection<K> implements Iterable<K> {
@Override
public boolean equals(Object 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)
&& 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.
* @return
*/
public static <K> SelectionPredicate<K> createSelectAnything() {
public static @NonNull <K> SelectionPredicate<K> createSelectAnything() {
return new SelectionPredicate<K>() {
@Override
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.
* @return
*/
public static <K> SelectionPredicate<K> createSelectSingleAnything() {
public static @NonNull <K> SelectionPredicate<K> createSelectSingleAnything() {
return new SelectionPredicate<K>() {
@Override
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.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
import android.view.GestureDetector;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
@ -89,12 +90,14 @@ import java.util.Set;
*/
public abstract class SelectionTracker<K> {
private static final String TAG = "SelectionTracker";
/**
* This value is included in the payload when SelectionTracker notifies RecyclerView
* of changes to selection. Look for this value in the {@code payload}
* Object argument supplied to
* {@link RecyclerView.Adapter#onBindViewHolder
* Adapter#onBindViewHolder}.
* Adapter#onBindViewHolder}.
* If present the call is occurring in response to a selection state change.
* This would be a good opportunity to animate changes between unselected and selected state.
* When state is being restored, this argument will not be present.
@ -110,7 +113,7 @@ public abstract class SelectionTracker<K> {
* may use an observer to control the enabled status of menu items,
* or to initiate {@link android.view.ActionMode}.
*/
public abstract void addObserver(SelectionObserver observer);
public abstract void addObserver(@NonNull SelectionObserver<K> observer);
/** @return true if has a selection */
public abstract boolean hasSelection();
@ -123,7 +126,7 @@ public abstract class SelectionTracker<K> {
* of the selection that will not reflect future changes
* to selection.
*/
public abstract Selection<K> getSelection();
public abstract @NonNull Selection<K> getSelection();
/**
* 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
* in Activity state.
*
* @see StorageStrategy details on selection state support.
*
* @param selection selection being restored.
* @see StorageStrategy details on selection state support.
*/
protected abstract void restoreSelection(@NonNull Selection<K> selection);
@ -181,7 +183,7 @@ public abstract class SelectionTracker<K> {
/** @hide */
@RestrictTo(LIBRARY)
protected abstract AdapterDataObserver getAdapterDataObserver();
protected abstract @NonNull AdapterDataObserver getAdapterDataObserver();
/**
* 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
* 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
* must have been started by a call to {@link #startRange(int)}.
* must have been started by a call to {@link #startRange(int)}.
* @hide
*/
@RestrictTo(LIBRARY)
@ -215,6 +217,7 @@ public abstract class SelectionTracker<K> {
* Clears an in-progress range selection. Provisional range selection established
* using {@link #extendProvisionalRange(int)} will be cleared (unless
* {@link #mergeProvisionalSelection()} is called first.)
*
* @hide
*/
@RestrictTo(LIBRARY)
@ -251,7 +254,7 @@ public abstract class SelectionTracker<K> {
/**
* Sets the provisional selection, replacing any existing selection.
* @param newSelection
*
* @hide
*/
@RestrictTo(LIBRARY)
@ -259,6 +262,7 @@ public abstract class SelectionTracker<K> {
/**
* Clears any existing provisional selection
*
* @hide
*/
@RestrictTo(LIBRARY)
@ -267,6 +271,7 @@ public abstract class SelectionTracker<K> {
/**
* Converts the provisional selection into primary selection, then clears
* provisional selection.
*
* @hide
*/
@RestrictTo(LIBRARY)
@ -300,6 +305,16 @@ public abstract class SelectionTracker<K> {
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
* SelectionTracker will traverse the existing selection,
@ -338,7 +353,7 @@ public abstract class SelectionTracker<K> {
/**
* 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
* @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
* 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
* @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),
* StorageStrategy.createParcelableStorage(Uri.class))
* .build();
*</pre>
* </pre>
*
* <p>
* <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()}.
*
* SelectionTracker<String> tracker = new SelectionTracker.Builder<>(
* "my-string-selection",
* recyclerView,
* new DemoStableIdProvider(recyclerView.getAdapter()),
* new MyDetailsLookup(recyclerView),
* StorageStrategy.createStringStorage())
* .withSelectionPredicate(SelectionPredicates#createSelectSingleAnything())
* .build();
*</pre>
* "my-string-selection",
* recyclerView,
* new DemoStableIdProvider(recyclerView.getAdapter()),
* new MyDetailsLookup(recyclerView),
* StorageStrategy.createStringStorage())
* .withSelectionPredicate(SelectionPredicates#createSelectSingleAnything())
* .build();
* </pre>
* <p>
* <b>Retaining state across Android lifecycle events</b>
*
@ -447,25 +462,25 @@ public abstract class SelectionTracker<K> {
* private SelectionTracker<Uri> mTracker;
*
* 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) {
* mTracker.onRestoreInstanceState(savedInstanceState);
* }
* if (savedInstanceState != null) {
* mTracker.onRestoreInstanceState(savedInstanceState);
* }
* }
*
* protected void onSaveInstanceState(Bundle outState) {
* super.onSaveInstanceState(outState);
* mTracker.onSaveInstanceState(outState);
* super.onSaveInstanceState(outState);
* mTracker.onSaveInstanceState(outState);
* }
* </pre>
*
* @param <K> Selection key type. Built in support is provided for {@link String},
* {@link Long}, and {@link Parcelable}. {@link StorageStrategy}
* provides factory methods for each type:
* {@link StorageStrategy#createStringStorage()},
* {@link StorageStrategy#createParcelableStorage(Class)},
* {@link StorageStrategy#createLongStorage()}
* {@link Long}, and {@link Parcelable}. {@link StorageStrategy}
* provides factory methods for each type:
* {@link StorageStrategy#createStringStorage()},
* {@link StorageStrategy#createParcelableStorage(Class)},
* {@link StorageStrategy#createLongStorage()}
*/
public static final class Builder<K> {
@ -490,12 +505,12 @@ public abstract class SelectionTracker<K> {
private BandPredicate mBandPredicate;
private int mBandOverlayId = eu.faircode.email.R.drawable.selection_band_overlay;
private int[] mGestureToolTypes = new int[] {
MotionEvent.TOOL_TYPE_FINGER,
MotionEvent.TOOL_TYPE_UNKNOWN
// TODO(b/144500333): Remove support for overriding gesture and pointer tooltypes.
private int[] mGestureToolTypes = new int[]{
MotionEvent.TOOL_TYPE_FINGER
};
private int[] mPointerToolTypes = new int[] {
private int[] mPointerToolTypes = new int[]{
MotionEvent.TOOL_TYPE_MOUSE
};
@ -503,13 +518,13 @@ public abstract class SelectionTracker<K> {
* Creates a new SelectionTracker.Builder useful for configuring and creating
* a new SelectionTracker for use with your {@link RecyclerView}.
*
* @param selectionId A unique string identifying this selection in the context
* of the activity or fragment.
* @param recyclerView the owning RecyclerView
* @param keyProvider the source of selection keys
* @param selectionId A unique string identifying this selection in the context
* of the activity or fragment.
* @param recyclerView the owning RecyclerView
* @param keyProvider the source of selection keys
* @param detailsLookup the source of information about RecyclerView items.
* @param storage Strategy for type-safe storage of selection state in
* {@link Bundle}.
* @param storage Strategy for type-safe storage of selection state in
* {@link Bundle}.
*/
public Builder(
@NonNull String selectionId,
@ -545,7 +560,7 @@ public abstract class SelectionTracker<K> {
* @param predicate the predicate to be used.
* @return this
*/
public Builder<K> withSelectionPredicate(
public @NonNull Builder<K> withSelectionPredicate(
@NonNull SelectionPredicate<K> predicate) {
checkArgument(predicate != null);
@ -560,7 +575,7 @@ public abstract class SelectionTracker<K> {
* @param monitor the monitor to be used
* @return this
*/
public Builder<K> withOperationMonitor(
public @NonNull Builder<K> withOperationMonitor(
@NonNull OperationMonitor monitor) {
checkArgument(monitor != null);
@ -574,7 +589,7 @@ public abstract class SelectionTracker<K> {
* @param delegate the delegate to be used
* @return this
*/
public Builder<K> withFocusDelegate(@NonNull FocusDelegate<K> delegate) {
public @NonNull Builder<K> withFocusDelegate(@NonNull FocusDelegate<K> delegate) {
checkArgument(delegate != null);
mFocusDelegate = delegate;
return this;
@ -586,7 +601,7 @@ public abstract class SelectionTracker<K> {
* @param listener the listener to be used
* @return this
*/
public Builder<K> withOnItemActivatedListener(
public @NonNull Builder<K> withOnItemActivatedListener(
@NonNull OnItemActivatedListener<K> listener) {
checkArgument(listener != null);
@ -601,7 +616,7 @@ public abstract class SelectionTracker<K> {
* @param listener the listener to be used
* @return this
*/
public Builder<K> withOnContextClickListener(
public @NonNull Builder<K> withOnContextClickListener(
@NonNull OnContextClickListener listener) {
checkArgument(listener != null);
@ -616,7 +631,7 @@ public abstract class SelectionTracker<K> {
* @param listener the listener to be used
* @return this
*/
public Builder<K> withOnDragInitiatedListener(
public @NonNull Builder<K> withOnDragInitiatedListener(
@NonNull OnDragInitiatedListener listener) {
checkArgument(listener != null);
@ -627,12 +642,17 @@ public abstract class SelectionTracker<K> {
/**
* 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
* @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;
return this;
}
@ -640,22 +660,19 @@ public abstract class SelectionTracker<K> {
/**
* Replaces default band overlay.
*
* @param bandOverlayId
* @return this
*/
public Builder<K> withBandOverlay(@DrawableRes int bandOverlayId) {
public @NonNull Builder<K> withBandOverlay(@DrawableRes int bandOverlayId) {
mBandOverlayId = bandOverlayId;
return this;
}
/**
* Replaces default band predicate.
* @param bandPredicate
*
* @return this
*/
public Builder<K> withBandPredicate(@NonNull BandPredicate bandPredicate) {
checkArgument(bandPredicate != null);
public @NonNull Builder<K> withBandPredicate(@NonNull BandPredicate bandPredicate) {
mBandPredicate = bandPredicate;
return this;
}
@ -668,8 +685,13 @@ public abstract class SelectionTracker<K> {
*
* @param toolTypes the tool types to be used
* @return this
*
* @deprecated PointerSelection is best bound to {@link MotionEvent#TOOL_TYPE_MOUSE},
* and only that tool type. This method will be removed in a future release.
*/
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;
return this;
}
@ -679,9 +701,9 @@ public abstract class SelectionTracker<K> {
*
* @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);
// 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.
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 =
new ViewAutoScroller(ViewAutoScroller.createScrollHost(mRecyclerView));
@ -698,14 +722,10 @@ public abstract class SelectionTracker<K> {
// GestureRouter is responsible for routing GestureDetector events
// to tool-type specific handlers.
GestureRouter<MotionInputHandler> gestureRouter = new GestureRouter<>();
GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter);
GestureRouter<MotionInputHandler<K>> gestureRouter = new GestureRouter<>();
// TouchEventRouter takes its name from RecyclerView#OnItemTouchListener.
// Despite "Touch" being in the name, it receives events for all types of tools.
// This class is responsible for routing events to tool-type specific handlers,
// and if not handled by a handler, on to a GestureDetector for analysis.
TouchEventRouter eventRouter = new TouchEventRouter(gestureDetector);
// GestureDetector cancels itself in response to ACTION_CANCEL events.
GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter);
// GestureSelectionHelper provides logic that interprets a combination
// of motions and gestures in order to provide gesture driven selection support
@ -713,8 +733,37 @@ public abstract class SelectionTracker<K> {
final GestureSelectionHelper gestureHelper = GestureSelectionHelper.create(
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(
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
// 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
// and gestures to selection framework.
TouchInputHandler<K> touchHandler = new TouchInputHandler<K>(
TouchInputHandler<K> touchHandler = new TouchInputHandler<>(
tracker,
mKeyProvider,
mDetailsLookup,
@ -768,8 +817,8 @@ public abstract class SelectionTracker<K> {
if (mSelectionPredicate.canSelectMultiple()) {
try {
gestureHelper.start();
} catch (IllegalStateException ex) {
eu.faircode.email.Log.w(ex);
} catch (Throwable ex) {
eu.faircode.email.Log.e(ex);
}
}
}
@ -786,7 +835,7 @@ public abstract class SelectionTracker<K> {
for (int toolType : mGestureToolTypes) {
gestureRouter.register(toolType, touchHandler);
eventRouter.register(toolType, gestureHelper);
eventRouter.set(toolType, gestureHelper);
}
// Provides high level glue for binding mouse events and gestures
@ -803,7 +852,7 @@ public abstract class SelectionTracker<K> {
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
// is limited to anything less than the entire corpus.
@ -824,14 +873,14 @@ public abstract class SelectionTracker<K> {
mBandPredicate,
mFocusDelegate,
mMonitor);
resetMgr.addResetHandler(bandHelper);
}
OnItemTouchListener pointerEventHandler = new PointerDragEventInterceptor(
mDetailsLookup, mOnDragInitiatedListener, bandHelper);
for (int toolType : mPointerToolTypes) {
eventRouter.register(toolType, pointerEventHandler);
}
eventRouter.set(MotionEvent.TOOL_TYPE_MOUSE, pointerEventHandler);
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.
*
* @param <K> Selection key type. Built in support is provided for String, Long, and Parcelable
* types. Use the respective factory method to create a StorageStrategy instance
* appropriate to the desired type.
* {@link #createStringStorage()},
* {@link #createParcelableStorage(Class)},
* {@link #createLongStorage()}
* types. Use the respective factory method to create a StorageStrategy instance
* appropriate to the desired type.
* {@link #createStringStorage()},
* {@link #createParcelableStorage(Class)},
* {@link #createLongStorage()}
*/
public abstract class StorageStrategy<K> {
@ -69,7 +69,6 @@ public abstract class StorageStrategy<K> {
* Create a {@link Selection} from supplied {@link Bundle}.
*
* @param state Bundle instance that may contain parceled Selection instance.
* @return
*/
public abstract @Nullable Selection<K> asSelection(@NonNull Bundle state);
@ -77,7 +76,6 @@ public abstract class StorageStrategy<K> {
* Creates a {@link Bundle} from supplied {@link Selection}.
*
* @param selection The selection to asBundle.
* @return
*/
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
* (like {@link android.net.Uri}).
*/
public static <K extends Parcelable> StorageStrategy<K> createParcelableStorage(Class<K> type) {
return new ParcelableStorageStrategy(type);
public static @NonNull <K extends Parcelable> StorageStrategy<K> createParcelableStorage(
@NonNull Class<K> type) {
return new ParcelableStorageStrategy<>(type);
}
/**
* @return StorageStrategy suitable for use with {@link String} keys.
*/
public static StorageStrategy<String> createStringStorage() {
public static @NonNull StorageStrategy<String> createStringStorage() {
return new StringStorageStrategy();
}
/**
* @return StorageStrategy suitable for use with {@link Long} keys.
*/
public static StorageStrategy<Long> createLongStorage() {
public static @NonNull StorageStrategy<Long> createLongStorage() {
return new LongStorageStrategy();
}
@ -191,7 +190,7 @@ public abstract class StorageStrategy<K> {
private static class ParcelableStorageStrategy<K extends Parcelable>
extends StorageStrategy<K> {
ParcelableStorageStrategy(Class<K> type) {
ParcelableStorageStrategy(@NonNull Class<K> type) {
super(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,
* 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.
*/
final class ToolHandlerRegistry<T> {
// Currently there are four known input types. ERASER is the last one, so has the
// highest value. UNKNOWN is zero, so we add one. This allows delegates to be
// registered by type, and avoid the auto-boxing that would be necessary were we
// to store delegates in a Map<Integer, Delegate>.
private static final int NUM_INPUT_TYPES = MotionEvent.TOOL_TYPE_ERASER + 1;
// list with one null entry for each known tooltype (0-4).
// See MotionEvent.TOOL_TYPE_ERASER for details. We're using a list here because
// it is parameterized type friendly, and a natural container given that
// the index values are 0-based ints.
private final List<T> mHandlers = Arrays.asList(null, null, null, null, null);
private final T mDefault;
ToolHandlerRegistry(@NonNull T defaultDelegate) {
checkArgument(defaultDelegate != null);
mDefault = defaultDelegate;
// 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;
import static androidx.core.util.Preconditions.checkArgument;
import static androidx.recyclerview.selection.Shared.DEBUG;
import android.util.Log;
import android.view.MotionEvent;
@ -36,7 +37,6 @@ import androidx.recyclerview.widget.RecyclerView;
final class TouchInputHandler<K> extends MotionInputHandler<K> {
private static final String TAG = "TouchInputDelegate";
private static final boolean DEBUG = false;
private final ItemDetailsLookup<K> mDetailsLookup;
private final SelectionPredicate<K> mSelectionPredicate;
@ -75,6 +75,11 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
@Override
public boolean onSingleTapUp(@NonNull MotionEvent e) {
if (DEBUG) {
checkArgument(MotionEvents.isFingerEvent(e));
checkArgument(MotionEvents.isActionUp(e));
}
if (!mDetailsLookup.overItemWithSelectionKey(e)) {
if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection.");
mSelectionTracker.clearSelection();
@ -113,6 +118,11 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
@Override
public void onLongPress(@NonNull MotionEvent e) {
if (DEBUG) {
checkArgument(MotionEvents.isFingerEvent(e));
checkArgument(MotionEvents.isActionDown(e));
}
if (!mDetailsLookup.overItemWithSelectionKey(e)) {
if (DEBUG) Log.d(TAG, "Ignoring LongPress on non-model-backed item.");
return;
@ -124,8 +134,6 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
return;
}
boolean handled = false;
if (shouldExtendRange(e)) {
extendSelectionRange(item);
mHapticPerformer.run();

View File

@ -97,7 +97,7 @@
* </b>
*
* <p>
* Example usage (with {@code Long} selection keys:
* Example usage (with {@code Long} selection keys):
* <pre>SelectionTracker<Long> tracker = new SelectionTracker.Builder<>(
* "my-selection-id",
* 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
index 76087ea4..121fbd14 100644
index 50cc4ceb7..910935a0a 100644
--- a/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 BandPredicate mBandPredicate;
- private int mBandOverlayId = R.drawable.selection_band_overlay;
+ private int mBandOverlayId = eu.faircode.email.R.drawable.selection_band_overlay;
private int[] mGestureToolTypes = new int[] {
MotionEvent.TOOL_TYPE_FINGER,
@@ -766,7 +766,11 @@ public abstract class SelectionTracker<K> {
// TODO(b/144500333): Remove support for overriding gesture and pointer tooltypes.
private int[] mGestureToolTypes = new int[]{
@@ -815,7 +815,11 @@ public abstract class SelectionTracker<K> {
@Override
public void run() {
if (mSelectionPredicate.canSelectMultiple()) {
- gestureHelper.start();
+ try {
+ gestureHelper.start();
+ } catch (IllegalStateException ex) {
+ ex.printStackTrace();
+ } catch (Throwable ex) {
+ 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
index d82812cc..48db78ec 100644
index be4c3fa0b..c434ced07 100644
--- a/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
@ -38,5 +38,5 @@ index d82812cc..48db78ec 100644
+
+ @Override
public void onLongPress(@NonNull MotionEvent e) {
if (!mDetailsLookup.overItemWithSelectionKey(e)) {
if (DEBUG) Log.d(TAG, "Ignoring LongPress on non-model-backed item.");
if (DEBUG) {
checkArgument(MotionEvents.isFingerEvent(e));