blob: cdbbc0fc4a04df4d63b08a074cf7e516bd1787b2 [file] [log] [blame]
/*
* Copyright (C) 2016 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 com.android.documentsui.dirlist;
import android.graphics.Point;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import com.android.documentsui.Events.InputEvent;
import com.android.documentsui.dirlist.ViewAutoScroller.ScrollActionDelegate;
import com.android.documentsui.dirlist.ViewAutoScroller.ScrollDistanceDelegate;
import java.util.function.IntSupplier;
import javax.annotation.Nullable;
/*
* Helper class used to intercept events that could cause a gesture multi-select, and keeps
* the interception going if necessary.
*/
final class GestureSelector {
private final MultiSelectManager mSelectionMgr;
private final Runnable mDragScroller;
private final int mAutoScrollEdgeHeight;
private final IntSupplier mHeight;
private final ViewFinder mViewFinder;
private int mLastStartedItemPos = -1;
private boolean mStarted = false;
private Point mLastInterceptedPoint;
GestureSelector(
int autoScrollEdgeHeight,
MultiSelectManager selectionMgr,
IntSupplier heightSupplier,
ViewFinder viewFinder,
ScrollActionDelegate actionDelegate) {
mAutoScrollEdgeHeight = autoScrollEdgeHeight;
mSelectionMgr = selectionMgr;
mHeight = heightSupplier;
mViewFinder = viewFinder;
ScrollDistanceDelegate distanceDelegate = new ScrollDistanceDelegate() {
@Override
public Point getCurrentPosition() {
return mLastInterceptedPoint;
}
@Override
public int getViewHeight() {
return mHeight.getAsInt();
}
@Override
public boolean isActive() {
return mStarted && mSelectionMgr.hasSelection();
}
};
mDragScroller = new ViewAutoScroller(
mAutoScrollEdgeHeight, distanceDelegate, actionDelegate);
}
static GestureSelector create(
int autoScrollEdgeHeight,
MultiSelectManager selectionMgr,
RecyclerView scrollView) {
ScrollActionDelegate actionDelegate = new ScrollActionDelegate() {
@Override
public void scrollBy(int dy) {
scrollView.scrollBy(0, dy);
}
@Override
public void runAtNextFrame(Runnable r) {
scrollView.postOnAnimation(r);
}
@Override
public void removeCallback(Runnable r) {
scrollView.removeCallbacks(r);
}
};
GestureSelector helper =
new GestureSelector(
autoScrollEdgeHeight, selectionMgr, scrollView::getHeight,
scrollView::findChildViewUnder, actionDelegate);
return helper;
}
// Explicitly kick off a gesture multi-select.
boolean start(InputEvent event) {
if (mStarted) {
return false;
}
mStarted = true;
return true;
}
public boolean onInterceptTouchEvent(InputEvent e) {
if (e.isMouseEvent()) {
return false;
}
boolean handled = false;
if (e.isActionDown()) {
handled = handleInterceptedDownEvent(e);
}
if (e.isActionUp()) {
handled = handleUpEvent(e);
}
if (e.isActionMove()) {
handled = handleInterceptedMoveEvent(e);
}
return handled;
}
public void onTouchEvent(RecyclerView rv, InputEvent e) {
if (!mStarted) {
return;
}
if (e.isActionUp()) {
handleUpEvent(e);
}
if (e.isActionMove()) {
handleOnTouchMoveEvent(rv, e);
}
}
// Called when an ACTION_DOWN event is intercepted.
// If down event happens on a file/doc, we mark that item's position as last started.
private boolean handleInterceptedDownEvent(InputEvent e) {
View itemView = mViewFinder.findView(e.getX(), e.getY());
if (itemView != null) {
mLastStartedItemPos = e.getItemPosition();
}
return false;
}
// Called when an ACTION_MOVE event is intercepted.
private boolean handleInterceptedMoveEvent(InputEvent e) {
mLastInterceptedPoint = e.getOrigin();
if (mStarted) {
mSelectionMgr.startRangeSelection(mLastStartedItemPos);
return true;
}
return false;
}
// Called when ACTION_UP event is intercepted.
// Essentially, since this means all gesture movement is over, reset everything.
private boolean handleUpEvent(InputEvent e) {
mLastStartedItemPos = -1;
mStarted = false;
mSelectionMgr.getSelection().applyProvisionalSelection();
return false;
}
// Call when an intercepted ACTION_MOVE event is passed down.
// At this point, we are sure user wants to gesture multi-select.
private void handleOnTouchMoveEvent(RecyclerView rv, InputEvent e) {
mLastInterceptedPoint = e.getOrigin();
// If user has moved his pointer to the bottom-right empty pane (ie. to the right of the
// last item of the recycler view), we would want to set that as the currentItemPos
View lastItem = rv.getLayoutManager()
.getChildAt(rv.getLayoutManager().getChildCount() - 1);
int direction = rv.getContext().getResources().getConfiguration().getLayoutDirection();
final boolean pastLastItem = isPastLastItem(lastItem.getTop(),
lastItem.getLeft(),
lastItem.getRight(),
e,
direction);
// Since views get attached & detached from RecyclerView,
// {@link LayoutManager#getChildCount} can return a different number from the actual
// number
// of items in the adapter. Using the adapter is the for sure way to get the actual last
// item position.
final float inboundY = getInboundY(rv.getHeight(), e.getY());
final int lastGlidedItemPos = (pastLastItem) ? rv.getAdapter().getItemCount() - 1
: rv.getChildAdapterPosition(rv.findChildViewUnder(e.getX(), inboundY));
if (lastGlidedItemPos != RecyclerView.NO_POSITION) {
doGestureMultiSelect(lastGlidedItemPos);
}
if (insideDragZone(rv)) {
mDragScroller.run();
}
}
// It's possible for events to go over the top/bottom of the RecyclerView.
// We want to get a Y-coordinate within the RecyclerView so we can find the childView underneath
// correctly.
private static float getInboundY(float max, float y) {
if (y < 0f) {
return 0f;
} else if (y > max) {
return max;
}
return y;
}
/*
* Check to see an InputEvent if past a particular item, i.e. to the right or to the bottom
* of the item.
* For RTL, it would to be to the left or to the bottom of the item.
*/
@VisibleForTesting
static boolean isPastLastItem(int top, int left, int right, InputEvent e, int direction) {
if (direction == View.LAYOUT_DIRECTION_LTR) {
return e.getX() > right && e.getY() > top;
} else {
return e.getX() < left && e.getY() > top;
}
}
/* Given the end position, select everything in-between.
* @param endPos The adapter position of the end item.
*/
private void doGestureMultiSelect(int endPos) {
mSelectionMgr.snapProvisionalRangeSelection(endPos);
}
private boolean insideDragZone(View scrollView) {
if (mLastInterceptedPoint == null) {
return false;
}
boolean shouldScrollUp = mLastInterceptedPoint.y < mAutoScrollEdgeHeight
&& scrollView.canScrollVertically(-1);
boolean shouldScrollDown = mLastInterceptedPoint.y > scrollView.getHeight() -
mAutoScrollEdgeHeight && scrollView.canScrollVertically(1);
return shouldScrollUp || shouldScrollDown;
}
@FunctionalInterface
interface ViewFinder {
@Nullable View findView(float x, float y);
}
}