/*
|
* Copyright (C) 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 com.android.launcher3.touch;
|
|
import static android.view.MotionEvent.INVALID_POINTER_ID;
|
|
import android.content.Context;
|
import android.graphics.PointF;
|
import android.util.Log;
|
import android.view.MotionEvent;
|
import android.view.VelocityTracker;
|
import android.view.ViewConfiguration;
|
|
import com.android.launcher3.Utilities;
|
import com.android.launcher3.testing.TestProtocol;
|
|
import androidx.annotation.NonNull;
|
import androidx.annotation.VisibleForTesting;
|
|
/**
|
* One dimensional scroll/drag/swipe gesture detector.
|
*
|
* Definition of swipe is different from android system in that this detector handles
|
* 'swipe to dismiss', 'swiping up/down a container' but also keeps scrolling state before
|
* swipe action happens
|
*/
|
public class SwipeDetector {
|
|
private static final boolean DBG = false;
|
private static final String TAG = "SwipeDetector";
|
|
private int mScrollConditions;
|
public static final int DIRECTION_POSITIVE = 1 << 0;
|
public static final int DIRECTION_NEGATIVE = 1 << 1;
|
public static final int DIRECTION_BOTH = DIRECTION_NEGATIVE | DIRECTION_POSITIVE;
|
|
private static final float ANIMATION_DURATION = 1200;
|
|
protected int mActivePointerId = INVALID_POINTER_ID;
|
|
/**
|
* The minimum release velocity in pixels per millisecond that triggers fling..
|
*/
|
public static final float RELEASE_VELOCITY_PX_MS = 1.0f;
|
|
/* Scroll state, this is set to true during dragging and animation. */
|
private ScrollState mState = ScrollState.IDLE;
|
|
enum ScrollState {
|
IDLE,
|
DRAGGING, // onDragStart, onDrag
|
SETTLING // onDragEnd
|
}
|
|
public static abstract class Direction {
|
|
abstract float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint,
|
boolean isRtl);
|
|
/**
|
* Distance in pixels a touch can wander before we think the user is scrolling.
|
*/
|
abstract float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos);
|
|
abstract float getVelocity(VelocityTracker tracker, boolean isRtl);
|
|
abstract boolean isPositive(float displacement);
|
|
abstract boolean isNegative(float displacement);
|
}
|
|
public static final Direction VERTICAL = new Direction() {
|
|
@Override
|
float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint, boolean isRtl) {
|
return ev.getY(pointerIndex) - refPoint.y;
|
}
|
|
@Override
|
float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) {
|
return Math.abs(ev.getX(pointerIndex) - downPos.x);
|
}
|
|
@Override
|
float getVelocity(VelocityTracker tracker, boolean isRtl) {
|
return tracker.getYVelocity();
|
}
|
|
@Override
|
boolean isPositive(float displacement) {
|
// Up
|
return displacement < 0;
|
}
|
|
@Override
|
boolean isNegative(float displacement) {
|
// Down
|
return displacement > 0;
|
}
|
};
|
|
public static final Direction HORIZONTAL = new Direction() {
|
|
@Override
|
float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint, boolean isRtl) {
|
float displacement = ev.getX(pointerIndex) - refPoint.x;
|
if (isRtl) {
|
displacement = -displacement;
|
}
|
return displacement;
|
}
|
|
@Override
|
float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) {
|
return Math.abs(ev.getY(pointerIndex) - downPos.y);
|
}
|
|
@Override
|
float getVelocity(VelocityTracker tracker, boolean isRtl) {
|
float velocity = tracker.getXVelocity();
|
if (isRtl) {
|
velocity = -velocity;
|
}
|
return velocity;
|
}
|
|
@Override
|
boolean isPositive(float displacement) {
|
// Right
|
return displacement > 0;
|
}
|
|
@Override
|
boolean isNegative(float displacement) {
|
// Left
|
return displacement < 0;
|
}
|
};
|
|
//------------------- ScrollState transition diagram -----------------------------------
|
//
|
// IDLE -> (mDisplacement > mTouchSlop) -> DRAGGING
|
// DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING
|
// SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING
|
// SETTLING -> (View settled) -> IDLE
|
|
private void setState(ScrollState newState) {
|
if (TestProtocol.sDebugTracing) {
|
Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "setState -- start: " + newState);
|
}
|
if (DBG) {
|
Log.d(TAG, "setState:" + mState + "->" + newState);
|
}
|
// onDragStart and onDragEnd is reported ONLY on state transition
|
if (newState == ScrollState.DRAGGING) {
|
initializeDragging();
|
if (mState == ScrollState.IDLE) {
|
if (TestProtocol.sDebugTracing) {
|
Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "setState -- 1: " + newState);
|
}
|
reportDragStart(false /* recatch */);
|
} else if (mState == ScrollState.SETTLING) {
|
reportDragStart(true /* recatch */);
|
}
|
}
|
if (newState == ScrollState.SETTLING) {
|
reportDragEnd();
|
}
|
|
mState = newState;
|
if (com.android.launcher3.testing.TestProtocol.sDebugTracing) {
|
android.util.Log.e(TestProtocol.NO_ALLAPPS_EVENT_TAG,
|
"setState: " + newState + " @ " + android.util.Log.getStackTraceString(
|
new Throwable()));
|
}
|
}
|
|
public boolean isDraggingOrSettling() {
|
return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
|
}
|
|
public int getDownX() {
|
return (int) mDownPos.x;
|
}
|
|
public int getDownY() {
|
return (int) mDownPos.y;
|
}
|
/**
|
* There's no touch and there's no animation.
|
*/
|
public boolean isIdleState() {
|
return mState == ScrollState.IDLE;
|
}
|
|
public boolean isSettlingState() {
|
return mState == ScrollState.SETTLING;
|
}
|
|
public boolean isDraggingState() {
|
return mState == ScrollState.DRAGGING;
|
}
|
|
private final PointF mDownPos = new PointF();
|
private final PointF mLastPos = new PointF();
|
private final Direction mDir;
|
private final boolean mIsRtl;
|
|
private final float mTouchSlop;
|
private final float mMaxVelocity;
|
|
/* Client of this gesture detector can register a callback. */
|
private final Listener mListener;
|
|
private VelocityTracker mVelocityTracker;
|
|
private float mLastDisplacement;
|
private float mDisplacement;
|
|
private float mSubtractDisplacement;
|
private boolean mIgnoreSlopWhenSettling;
|
|
public interface Listener {
|
void onDragStart(boolean start);
|
|
boolean onDrag(float displacement);
|
|
default boolean onDrag(float displacement, MotionEvent event) {
|
return onDrag(displacement);
|
}
|
|
void onDragEnd(float velocity, boolean fling);
|
}
|
|
public SwipeDetector(@NonNull Context context, @NonNull Listener l, @NonNull Direction dir) {
|
this(ViewConfiguration.get(context), l, dir, Utilities.isRtl(context.getResources()));
|
}
|
|
@VisibleForTesting
|
protected SwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l,
|
@NonNull Direction dir, boolean isRtl) {
|
mListener = l;
|
mDir = dir;
|
mIsRtl = isRtl;
|
mTouchSlop = config.getScaledTouchSlop();
|
mMaxVelocity = config.getScaledMaximumFlingVelocity();
|
}
|
|
public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
|
mScrollConditions = scrollDirectionFlags;
|
mIgnoreSlopWhenSettling = ignoreSlop;
|
}
|
|
public int getScrollDirections() {
|
return mScrollConditions;
|
}
|
|
private boolean shouldScrollStart(MotionEvent ev, int pointerIndex) {
|
// reject cases where the angle or slop condition is not met.
|
if (Math.max(mDir.getActiveTouchSlop(ev, pointerIndex, mDownPos), mTouchSlop)
|
> Math.abs(mDisplacement)) {
|
return false;
|
}
|
|
// Check if the client is interested in scroll in current direction.
|
if (((mScrollConditions & DIRECTION_NEGATIVE) > 0 && mDir.isNegative(mDisplacement)) ||
|
((mScrollConditions & DIRECTION_POSITIVE) > 0 && mDir.isPositive(mDisplacement))) {
|
return true;
|
}
|
return false;
|
}
|
|
public boolean onTouchEvent(MotionEvent ev) {
|
int actionMasked = ev.getActionMasked();
|
if (actionMasked == MotionEvent.ACTION_DOWN && mVelocityTracker != null) {
|
mVelocityTracker.clear();
|
}
|
if (mVelocityTracker == null) {
|
mVelocityTracker = VelocityTracker.obtain();
|
}
|
mVelocityTracker.addMovement(ev);
|
|
switch (actionMasked) {
|
case MotionEvent.ACTION_DOWN:
|
mActivePointerId = ev.getPointerId(0);
|
mDownPos.set(ev.getX(), ev.getY());
|
mLastPos.set(mDownPos);
|
mLastDisplacement = 0;
|
mDisplacement = 0;
|
|
if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
|
setState(ScrollState.DRAGGING);
|
}
|
break;
|
//case MotionEvent.ACTION_POINTER_DOWN:
|
case MotionEvent.ACTION_POINTER_UP:
|
int ptrIdx = ev.getActionIndex();
|
int ptrId = ev.getPointerId(ptrIdx);
|
if (ptrId == mActivePointerId) {
|
final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
|
mDownPos.set(
|
ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
|
ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
|
mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
|
mActivePointerId = ev.getPointerId(newPointerIdx);
|
}
|
break;
|
case MotionEvent.ACTION_MOVE:
|
int pointerIndex = ev.findPointerIndex(mActivePointerId);
|
if (pointerIndex == INVALID_POINTER_ID) {
|
break;
|
}
|
mDisplacement = mDir.getDisplacement(ev, pointerIndex, mDownPos, mIsRtl);
|
if (TestProtocol.sDebugTracing) {
|
Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "onTouchEvent 1");
|
}
|
|
// handle state and listener calls.
|
if (mState != ScrollState.DRAGGING && shouldScrollStart(ev, pointerIndex)) {
|
if (TestProtocol.sDebugTracing) {
|
Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "onTouchEvent 2");
|
}
|
setState(ScrollState.DRAGGING);
|
}
|
if (mState == ScrollState.DRAGGING) {
|
reportDragging(ev);
|
}
|
mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
|
break;
|
case MotionEvent.ACTION_CANCEL:
|
case MotionEvent.ACTION_UP:
|
// These are synthetic events and there is no need to update internal values.
|
if (mState == ScrollState.DRAGGING) {
|
setState(ScrollState.SETTLING);
|
}
|
mVelocityTracker.recycle();
|
mVelocityTracker = null;
|
break;
|
default:
|
break;
|
}
|
return true;
|
}
|
|
public void finishedScrolling() {
|
setState(ScrollState.IDLE);
|
}
|
|
private boolean reportDragStart(boolean recatch) {
|
mListener.onDragStart(!recatch);
|
if (DBG) {
|
Log.d(TAG, "onDragStart recatch:" + recatch);
|
}
|
return true;
|
}
|
|
private void initializeDragging() {
|
if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
|
mSubtractDisplacement = 0;
|
}
|
if (mDisplacement > 0) {
|
mSubtractDisplacement = mTouchSlop;
|
} else {
|
mSubtractDisplacement = -mTouchSlop;
|
}
|
}
|
|
/**
|
* Returns if the start drag was towards the positive direction or negative.
|
*
|
* @see #setDetectableScrollConditions(int, boolean)
|
* @see #DIRECTION_BOTH
|
*/
|
public boolean wasInitialTouchPositive() {
|
return mDir.isPositive(mSubtractDisplacement);
|
}
|
|
private boolean reportDragging(MotionEvent event) {
|
if (mDisplacement != mLastDisplacement) {
|
if (DBG) {
|
Log.d(TAG, String.format("onDrag disp=%.1f", mDisplacement));
|
}
|
|
mLastDisplacement = mDisplacement;
|
return mListener.onDrag(mDisplacement - mSubtractDisplacement, event);
|
}
|
return true;
|
}
|
|
private void reportDragEnd() {
|
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
|
float velocity = mDir.getVelocity(mVelocityTracker, mIsRtl) / 1000;
|
if (DBG) {
|
Log.d(TAG, String.format("onScrollEnd disp=%.1f, velocity=%.1f",
|
mDisplacement, velocity));
|
}
|
|
mListener.onDragEnd(velocity, Math.abs(velocity) > RELEASE_VELOCITY_PX_MS);
|
}
|
|
public static long calculateDuration(float velocity, float progressNeeded) {
|
// TODO: make these values constants after tuning.
|
float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity));
|
float travelDistance = Math.max(0.2f, progressNeeded);
|
long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance);
|
if (DBG) {
|
Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded));
|
}
|
return duration;
|
}
|
}
|