/* * 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; /** * 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 int mActivePointerId = INVALID_POINTER_ID; private VelocityTracker mVelocityTracker; private PointF mLastDisplacement = new PointF(); private PointF mDisplacement = new PointF(); protected PointF mSubtractDisplacement = new PointF(); private ScrollState mState = ScrollState.IDLE; 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 (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; } 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); }