blob: 1276ece7e28b7f2cd321251fa5f70f2ed315bbe2 [file] [log] [blame]
/*
* 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.graphics.PointF;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.util.LinkedList;
import java.util.Queue;
/**
* 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.
*
* @see SingleAxisSwipeDetector
* @see BothAxesSwipeDetector
*/
public abstract class BaseSwipeDetector {
private static final boolean DBG = false;
private static final String TAG = "BaseSwipeDetector";
private static final float ANIMATION_DURATION = 1200;
/** The minimum release velocity in pixels per millisecond that triggers fling.*/
private static final float RELEASE_VELOCITY_PX_MS = 1.0f;
private static final PointF sTempPoint = new PointF();
private final PointF mDownPos = new PointF();
private final PointF mLastPos = new PointF();
protected final boolean mIsRtl;
protected final float mTouchSlop;
protected final float mMaxVelocity;
private final Queue<Runnable> mSetStateQueue = new LinkedList<>();
private int mActivePointerId = INVALID_POINTER_ID;
private VelocityTracker mVelocityTracker;
private PointF mLastDisplacement = new PointF();
private PointF mDisplacement = new PointF();
protected PointF mSubtractDisplacement = new PointF();
@VisibleForTesting ScrollState mState = ScrollState.IDLE;
private boolean mIsSettingState;
protected boolean mIgnoreSlopWhenSettling;
private enum ScrollState {
IDLE,
DRAGGING, // onDragStart, onDrag
SETTLING // onDragEnd
}
protected BaseSwipeDetector(@NonNull ViewConfiguration config, boolean isRtl) {
mTouchSlop = config.getScaledTouchSlop();
mMaxVelocity = config.getScaledMaximumFlingVelocity();
mIsRtl = isRtl;
}
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;
}
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;
}
public boolean isDraggingOrSettling() {
return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
}
public void finishedScrolling() {
setState(ScrollState.IDLE);
}
public boolean isFling(float velocity) {
return Math.abs(velocity) > RELEASE_VELOCITY_PX_MS;
}
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.set(0, 0);
mDisplacement.set(0, 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.set(ev.getX(pointerIndex) - mDownPos.x,
ev.getY(pointerIndex) - mDownPos.y);
if (mIsRtl) {
mDisplacement.x = -mDisplacement.x;
}
// handle state and listener calls.
if (mState != ScrollState.DRAGGING && shouldScrollStart(mDisplacement)) {
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;
}
//------------------- 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 (mIsSettingState) {
mSetStateQueue.add(() -> setState(newState));
return;
}
mIsSettingState = true;
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) {
reportDragStart(false /* recatch */);
} else if (mState == ScrollState.SETTLING) {
reportDragStart(true /* recatch */);
}
}
if (newState == ScrollState.SETTLING) {
reportDragEnd();
}
mState = newState;
mIsSettingState = false;
if (!mSetStateQueue.isEmpty()) {
mSetStateQueue.remove().run();
}
}
private void initializeDragging() {
if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
mSubtractDisplacement.set(0, 0);
} else {
mSubtractDisplacement.x = mDisplacement.x > 0 ? mTouchSlop : -mTouchSlop;
mSubtractDisplacement.y = mDisplacement.y > 0 ? mTouchSlop : -mTouchSlop;
}
}
protected abstract boolean shouldScrollStart(PointF displacement);
private void reportDragStart(boolean recatch) {
reportDragStartInternal(recatch);
if (DBG) {
Log.d(TAG, "onDragStart recatch:" + recatch);
}
}
protected abstract void reportDragStartInternal(boolean recatch);
private void reportDragging(MotionEvent event) {
if (mDisplacement != mLastDisplacement) {
if (DBG) {
Log.d(TAG, String.format("onDrag disp=%s", mDisplacement));
}
mLastDisplacement.set(mDisplacement);
sTempPoint.set(mDisplacement.x - mSubtractDisplacement.x,
mDisplacement.y - mSubtractDisplacement.y);
reportDraggingInternal(sTempPoint, event);
}
}
protected abstract void reportDraggingInternal(PointF displacement, MotionEvent event);
private void reportDragEnd() {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
PointF velocity = new PointF(mVelocityTracker.getXVelocity() / 1000,
mVelocityTracker.getYVelocity() / 1000);
if (mIsRtl) {
velocity.x = -velocity.x;
}
if (DBG) {
Log.d(TAG, String.format("onScrollEnd disp=%.1s, velocity=%.1s",
mDisplacement, velocity));
}
reportDragEndInternal(velocity);
}
protected abstract void reportDragEndInternal(PointF velocity);
}