mirror of https://github.com/M66B/FairEmail.git
363 lines
12 KiB
Java
363 lines
12 KiB
Java
/*
|
|
* Copyright 2017 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package androidx.recyclerview.selection;
|
|
|
|
import static androidx.core.util.Preconditions.checkArgument;
|
|
import static androidx.core.util.Preconditions.checkState;
|
|
import static androidx.recyclerview.selection.Shared.VERBOSE;
|
|
|
|
import android.graphics.Point;
|
|
import android.graphics.Rect;
|
|
import android.util.Log;
|
|
import android.view.MotionEvent;
|
|
|
|
import androidx.annotation.DrawableRes;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
|
|
import androidx.recyclerview.widget.RecyclerView;
|
|
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
|
|
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
|
|
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* Provides mouse driven band-selection support when used in conjunction with a {@link RecyclerView}
|
|
* instance. This class is responsible for rendering a band overlay and manipulating selection
|
|
* status of the items it intersects with.
|
|
*
|
|
* <p>
|
|
* Given the recycling nature of RecyclerView items that have scrolled off-screen would not
|
|
* be selectable with a band that itself was partially rendered off-screen. To address this,
|
|
* BandSelectionController builds a model of the list/grid information presented by RecyclerView as
|
|
* 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.
|
|
*
|
|
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
|
*/
|
|
class BandSelectionHelper<K> implements OnItemTouchListener, Resettable {
|
|
|
|
static final String TAG = "BandSelectionHelper";
|
|
static final boolean DEBUG = false;
|
|
|
|
private final BandHost<K> mHost;
|
|
private final ItemKeyProvider<K> mKeyProvider;
|
|
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
|
final SelectionTracker<K> mSelectionTracker;
|
|
private final BandPredicate mBandPredicate;
|
|
private final FocusDelegate<K> mFocusDelegate;
|
|
private final OperationMonitor mLock;
|
|
private final AutoScroller mScroller;
|
|
private final GridModel.SelectionObserver<K> mGridObserver;
|
|
|
|
private @Nullable Point mCurrentPosition;
|
|
private @Nullable Point mOrigin;
|
|
private @Nullable GridModel<K> mModel;
|
|
|
|
/**
|
|
* See {@link BandSelectionHelper#create}.
|
|
*/
|
|
BandSelectionHelper(
|
|
@NonNull BandHost<K> host,
|
|
@NonNull AutoScroller scroller,
|
|
@NonNull ItemKeyProvider<K> keyProvider,
|
|
@NonNull SelectionTracker<K> selectionTracker,
|
|
@NonNull BandPredicate bandPredicate,
|
|
@NonNull FocusDelegate<K> focusDelegate,
|
|
@NonNull OperationMonitor lock) {
|
|
|
|
checkArgument(host != null);
|
|
checkArgument(scroller != null);
|
|
checkArgument(keyProvider != null);
|
|
checkArgument(selectionTracker != null);
|
|
checkArgument(bandPredicate != null);
|
|
checkArgument(focusDelegate != null);
|
|
checkArgument(lock != null);
|
|
|
|
mHost = host;
|
|
mKeyProvider = keyProvider;
|
|
mSelectionTracker = selectionTracker;
|
|
mBandPredicate = bandPredicate;
|
|
mFocusDelegate = focusDelegate;
|
|
mLock = lock;
|
|
|
|
mHost.addOnScrollListener(
|
|
new OnScrollListener() {
|
|
@Override
|
|
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
|
BandSelectionHelper.this.onScrolled(recyclerView, dx, dy);
|
|
}
|
|
});
|
|
|
|
mScroller = scroller;
|
|
|
|
mGridObserver = new GridModel.SelectionObserver<K>() {
|
|
@Override
|
|
public void onSelectionChanged(Set<K> updatedSelection) {
|
|
mSelectionTracker.setProvisionalSelection(updatedSelection);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a new instance.
|
|
*
|
|
* @return new BandSelectionHelper instance.
|
|
*/
|
|
static <K> BandSelectionHelper<K> create(
|
|
@NonNull RecyclerView recyclerView,
|
|
@NonNull AutoScroller scroller,
|
|
@DrawableRes int bandOverlayId,
|
|
@NonNull ItemKeyProvider<K> keyProvider,
|
|
@NonNull SelectionTracker<K> selectionTracker,
|
|
@NonNull SelectionPredicate<K> selectionPredicate,
|
|
@NonNull BandPredicate bandPredicate,
|
|
@NonNull FocusDelegate<K> focusDelegate,
|
|
@NonNull OperationMonitor lock) {
|
|
|
|
return new BandSelectionHelper<>(
|
|
new DefaultBandHost<>(recyclerView, bandOverlayId, keyProvider, selectionPredicate),
|
|
scroller,
|
|
keyProvider,
|
|
selectionTracker,
|
|
bandPredicate,
|
|
focusDelegate,
|
|
lock);
|
|
}
|
|
|
|
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.
|
|
*/
|
|
@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();
|
|
mModel.onDestroy();
|
|
}
|
|
|
|
mModel = null;
|
|
mOrigin = null;
|
|
|
|
mScroller.reset();
|
|
// mLock is reset by reset manager.
|
|
}
|
|
|
|
@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.
|
|
return MotionEvents.isPrimaryMouseButtonPressed(e)
|
|
&& MotionEvents.isActionMove(e)
|
|
&& mBandPredicate.canInitiate(e)
|
|
&& !isActive();
|
|
}
|
|
|
|
private boolean shouldStop(@NonNull MotionEvent e) {
|
|
return isActive() && MotionEvents.isActionUp(e);
|
|
}
|
|
|
|
@Override
|
|
public boolean onInterceptTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
|
|
if (shouldStart(e)) {
|
|
startBandSelect(e);
|
|
} else if (shouldStop(e)) {
|
|
endBandSelect();
|
|
}
|
|
|
|
return isActive();
|
|
}
|
|
|
|
/**
|
|
* Processes a MotionEvent by starting, ending, or resizing the band select overlay.
|
|
*/
|
|
@Override
|
|
public void onTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
|
|
if (shouldStop(e)) {
|
|
endBandSelect();
|
|
return;
|
|
}
|
|
|
|
// We shouldn't get any events in this method when band select is not active,
|
|
// but it turns some guests show up late to the party.
|
|
// Probably happening when a re-layout is happening to the ReyclerView (ie. Pull-To-Refresh)
|
|
if (!isActive()) {
|
|
return;
|
|
}
|
|
|
|
if (DEBUG) {
|
|
checkArgument(MotionEvents.isActionMove(e));
|
|
checkState(mModel != null);
|
|
}
|
|
|
|
mCurrentPosition = MotionEvents.getOrigin(e);
|
|
|
|
mModel.resizeSelection(mCurrentPosition);
|
|
|
|
resizeBand();
|
|
mScroller.scroll(mCurrentPosition);
|
|
}
|
|
|
|
@Override
|
|
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
|
|
}
|
|
|
|
/**
|
|
* Starts band select by adding the drawable to the RecyclerView's overlay.
|
|
*/
|
|
private void startBandSelect(@NonNull MotionEvent e) {
|
|
if (DEBUG) {
|
|
checkState(!isActive());
|
|
}
|
|
|
|
if (!MotionEvents.isCtrlKeyPressed(e)) {
|
|
mSelectionTracker.clearSelection();
|
|
}
|
|
|
|
Point origin = MotionEvents.getOrigin(e);
|
|
if (DEBUG) Log.d(TAG, "Starting band select @ " + origin);
|
|
|
|
mModel = mHost.createGridModel();
|
|
mModel.addOnSelectionChangedListener(mGridObserver);
|
|
|
|
mLock.start();
|
|
mFocusDelegate.clearFocus();
|
|
mOrigin = origin;
|
|
// NOTE: Pay heed that resizeBand modifies the y coordinates
|
|
// in onScrolled. Not sure if model expects this. If not
|
|
// it should be defending against this.
|
|
mModel.startCapturing(mOrigin);
|
|
}
|
|
|
|
/**
|
|
* Resizes the band select rectangle by using the origin and the current pointer position as
|
|
* two opposite corners of the selection.
|
|
*/
|
|
private void resizeBand() {
|
|
Rect bounds = new Rect(Math.min(mOrigin.x, mCurrentPosition.x),
|
|
Math.min(mOrigin.y, mCurrentPosition.y),
|
|
Math.max(mOrigin.x, mCurrentPosition.x),
|
|
Math.max(mOrigin.y, mCurrentPosition.y));
|
|
|
|
if (VERBOSE) Log.v(TAG, "Resizing band! " + bounds);
|
|
mHost.showBand(bounds);
|
|
}
|
|
|
|
/**
|
|
* Ends band select by removing the overlay.
|
|
*/
|
|
private void endBandSelect() {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Ending band select.");
|
|
checkState(mModel != null);
|
|
}
|
|
|
|
// TODO: Currently when a band select operation ends outside
|
|
// of an item (e.g. in the empty area between items),
|
|
// getPositionNearestOrigin may return an unselected item.
|
|
// Since the point of this code is to establish the
|
|
// anchor point for subsequent range operations (SHIFT+CLICK)
|
|
// we really want to do a better job figuring out the last
|
|
// item selected (and nearest to the cursor).
|
|
int firstSelected = mModel.getPositionNearestOrigin();
|
|
if (firstSelected != GridModel.NOT_SET
|
|
&& mSelectionTracker.isSelected(mKeyProvider.getKey(firstSelected))) {
|
|
// Establish the band selection point as range anchor. This
|
|
// allows touch and keyboard based selection activities
|
|
// to be based on the band selection anchor point.
|
|
mSelectionTracker.anchorRange(firstSelected);
|
|
}
|
|
|
|
mSelectionTracker.mergeProvisionalSelection();
|
|
mLock.stop();
|
|
|
|
mHost.hideBand();
|
|
if (mModel != null) {
|
|
mModel.stopCapturing();
|
|
mModel.onDestroy();
|
|
}
|
|
|
|
mModel = null;
|
|
mOrigin = null;
|
|
|
|
mScroller.reset();
|
|
}
|
|
|
|
/**
|
|
* @see OnScrollListener
|
|
*/
|
|
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
|
void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
|
if (!isActive()) {
|
|
return;
|
|
}
|
|
|
|
// Adjust the y-coordinate of the origin the opposite number of pixels so that the
|
|
// origin remains in the same place relative to the view's items.
|
|
mOrigin.y -= dy;
|
|
resizeBand();
|
|
}
|
|
|
|
/**
|
|
* Provides functionality for BandController. Exists primarily to tests that are
|
|
* fully isolated from RecyclerView.
|
|
*
|
|
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
|
|
*/
|
|
abstract static class BandHost<K> {
|
|
|
|
/**
|
|
* Returns a new GridModel instance.
|
|
*/
|
|
abstract GridModel<K> createGridModel();
|
|
|
|
/**
|
|
* Show the band covering the bounds.
|
|
*
|
|
* @param bounds The boundaries of the band to show.
|
|
*/
|
|
abstract void showBand(@NonNull Rect bounds);
|
|
|
|
/**
|
|
* Hide the band.
|
|
*/
|
|
abstract void hideBand();
|
|
|
|
/**
|
|
* Add a listener to be notified on scroll events.
|
|
*/
|
|
abstract void addOnScrollListener(@NonNull OnScrollListener listener);
|
|
}
|
|
}
|