blob: 127a5116f0fb866b038dede1f562259ec6233263 [file]
/*
* 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 android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static android.support.v4.util.Preconditions.checkArgument;
import android.content.Context;
import android.support.annotation.DrawableRes;
import android.support.annotation.RestrictTo;
import android.support.v7.widget.RecyclerView;
import android.view.GestureDetector;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
/**
* Builder class for assembling selection support. Example usage:
*
* <p><pre>SelectionHelperBuilder selSupport = new SelectionHelperBuilder(
mRecView, new DemoStableIdProvider(mAdapter), detailsLookup);
// By default multi-select is supported.
SelectionHelper selHelper = selSupport
.build();
// This configuration support single selection for any element.
SelectionHelper selHelper = selSupport
.withSelectionPredicate(SelectionHelper.SelectionPredicate.SINGLE_ANYTHING)
.build();
// Lazily bind SelectionHelper. Allows us to defer initialization of the
// SelectionHelper dependency until after the adapter is created.
mAdapter.bindSelectionHelper(selHelper);
* </pre></p>
*
* @see SelectionStorage for important deatils on retaining selection across Activity
* lifecycle events.
*
* @param <K> Selection key type. Usually String or Long.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public final class SelectionHelperBuilder<K> {
private final RecyclerView mRecView;
private final RecyclerView.Adapter<?> mAdapter;
private final Context mContext;
// Content lock provides a mechanism to block content reload while selection
// activities are active. If using a loader to load content, route
// the call through the content lock using ContentLock#runWhenUnlocked.
// This is especially useful when listening on content change notification.
private final ContentLock mLock = new ContentLock();
private SelectionPredicate<K> mSelectionPredicate = SelectionPredicates.selectAnything();
private ItemKeyProvider<K> mKeyProvider;
private ItemDetailsLookup<K> mDetailsLookup;
private ActivationCallbacks<K> mActivationCallbacks = ActivationCallbacks.dummy();
private FocusCallbacks<K> mFocusCallbacks = FocusCallbacks.dummy();
private TouchCallbacks mTouchCallbacks = TouchCallbacks.DUMMY;
private MouseCallbacks mMouseCallbacks = MouseCallbacks.DUMMY;
private BandPredicate mBandPredicate;
private int mBandOverlayId = R.drawable.selection_band_overlay;
private int[] mGestureToolTypes = new int[] {
MotionEvent.TOOL_TYPE_FINGER,
MotionEvent.TOOL_TYPE_UNKNOWN
};
private int[] mBandToolTypes = new int[] {
MotionEvent.TOOL_TYPE_MOUSE
};
public SelectionHelperBuilder(
RecyclerView recView,
ItemKeyProvider<K> keyProvider,
ItemDetailsLookup<K> detailsLookup) {
checkArgument(recView != null);
mRecView = recView;
mContext = recView.getContext();
mAdapter = recView.getAdapter();
checkArgument(mAdapter != null);
checkArgument(keyProvider != null);
checkArgument(detailsLookup != null);
mDetailsLookup = detailsLookup;
mKeyProvider = keyProvider;
mBandPredicate = BandPredicate.notDraggable(mRecView, detailsLookup);
}
/**
* Install seleciton predicate.
* @param predicate
* @return
*/
public SelectionHelperBuilder<K> withSelectionPredicate(SelectionPredicate<K> predicate) {
checkArgument(predicate != null);
mSelectionPredicate = predicate;
return this;
}
/**
* Add activation callbacks to respond to taps/enter/double-click on items.
*
* @param callbacks
* @return
*/
public SelectionHelperBuilder<K> withActivationCallbacks(ActivationCallbacks<K> callbacks) {
checkArgument(callbacks != null);
mActivationCallbacks = callbacks;
return this;
}
/**
* Add focus callbacks to interfact with selection related focus changes.
* @param callbacks
* @return
*/
public SelectionHelperBuilder<K> withFocusCallbacks(FocusCallbacks<K> callbacks) {
checkArgument(callbacks != null);
mFocusCallbacks = callbacks;
return this;
}
/**
* Configures mouse callbacks, replacing defaults.
*
* @param callbacks
* @return
*/
public SelectionHelperBuilder<K> withMouseCallbacks(MouseCallbacks callbacks) {
checkArgument(callbacks != null);
mMouseCallbacks = callbacks;
return this;
}
/**
* Replaces default touch callbacks.
*
* @param callbacks
* @return
*/
public SelectionHelperBuilder<K> withTouchCallbacks(TouchCallbacks callbacks) {
checkArgument(callbacks != null);
mTouchCallbacks = callbacks;
return this;
}
/**
* Replaces default gesture tooltypes.
* @param toolTypes
* @return
*/
public SelectionHelperBuilder<K> withTouchTooltypes(int... toolTypes) {
mGestureToolTypes = toolTypes;
return this;
}
/**
* Replaces default band overlay.
*
* @param bandOverlayId
* @return
*/
public SelectionHelperBuilder<K> withBandOverlay(@DrawableRes int bandOverlayId) {
mBandOverlayId = bandOverlayId;
return this;
}
/**
* Replaces default band predicate.
* @param bandPredicate
* @return
*/
public SelectionHelperBuilder<K> withBandPredicate(BandPredicate bandPredicate) {
checkArgument(bandPredicate != null);
mBandPredicate = bandPredicate;
return this;
}
/**
* Replaces default band tools types.
* @param toolTypes
* @return
*/
public SelectionHelperBuilder<K> withBandTooltypes(int... toolTypes) {
mBandToolTypes = toolTypes;
return this;
}
/**
* Prepares selection support and returns the corresponding SelectionHelper.
*
* @return
*/
public SelectionHelper<K> build() {
SelectionHelper<K> selectionHelper =
new DefaultSelectionHelper<>(mKeyProvider, mSelectionPredicate);
// Event glue between RecyclerView and SelectionHelper keeps the classes separate
// so that a SelectionHelper can be shared across RecyclerView instances that
// represent the same data in different ways.
EventBridge.install(mAdapter, selectionHelper, mKeyProvider);
AutoScroller scroller = new ViewAutoScroller(ViewAutoScroller.createScrollHost(mRecView));
// Setup basic input handling, with the touch handler as the default consumer
// of events. If mouse handling is configured as well, the mouse input
// related handlers will intercept mouse input events.
// GestureRouter is responsible for routing GestureDetector events
// to tool-type specific handlers.
GestureRouter<MotionInputHandler> gestureRouter = new GestureRouter<>();
GestureDetector gestureDetector = new GestureDetector(mContext, 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);
// GestureSelectionHelper provides logic that interprets a combination
// of motions and gestures in order to provide gesture driven selection support
// when used in conjunction with RecyclerView.
final GestureSelectionHelper gestureHelper =
GestureSelectionHelper.create(selectionHelper, mRecView, scroller, mLock);
// Finally hook the framework up to listening to recycle view events.
mRecView.addOnItemTouchListener(eventRouter);
// 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.
// Helpers contain predefined logic converting events into selection related events.
// Callbacks provide authors the ability to reponspond to other types of
// events (like "active" a tapped item). This is broken up into two main
// suites, one for "touch" and one for "mouse", though both can and should (usually)
// be configued to handle other types of input (to satisfy user expectation).);
// Provides high level glue for binding touch events
// and gestures to selection framework.
TouchInputHandler<K> touchHandler = new TouchInputHandler<K>(
selectionHelper,
mKeyProvider,
mDetailsLookup,
mSelectionPredicate,
new Runnable() {
@Override
public void run() {
if (mSelectionPredicate.canSelectMultiple()) {
gestureHelper.start();
}
}
},
mTouchCallbacks,
mActivationCallbacks,
mFocusCallbacks,
new Runnable() {
@Override
public void run() {
mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
});
for (int toolType : mGestureToolTypes) {
gestureRouter.register(toolType, touchHandler);
eventRouter.register(toolType, gestureHelper);
}
// Provides high level glue for binding mouse events and gestures
// to selection framework.
MouseInputHandler<K> mouseHandler = new MouseInputHandler<>(
selectionHelper,
mKeyProvider,
mDetailsLookup,
mMouseCallbacks,
mActivationCallbacks,
mFocusCallbacks);
for (int toolType : mBandToolTypes) {
gestureRouter.register(toolType, mouseHandler);
}
// Band selection not supported in single select mode, or when key access
// is limited to anything less than the entire corpus.
// TODO: Since we cach grid info from laid out items, we could cache key too.
// Then we couldn't have to limit to CORPUS access.
if (mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED)
&& mSelectionPredicate.canSelectMultiple()) {
// BandSelectionHelper provides support for band selection on-top of a RecyclerView
// instance. Given the recycling nature of RecyclerView BandSelectionController
// necessarily models and caches list/grid information as the user's pointer
// interacts with the item in the RecyclerView. Selectable items that intersect
// with the band, both on and off screen, are selected.
BandSelectionHelper bandHelper = BandSelectionHelper.create(
mRecView,
scroller,
mBandOverlayId,
mKeyProvider,
selectionHelper,
mSelectionPredicate,
mBandPredicate,
mFocusCallbacks,
mLock);
for (int toolType : mBandToolTypes) {
eventRouter.register(toolType, bandHelper);
}
}
return selectionHelper;
}
}