1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.launcher3.touch; 17 18 import static android.view.MotionEvent.INVALID_POINTER_ID; 19 20 import android.graphics.PointF; 21 import android.util.Log; 22 import android.view.MotionEvent; 23 import android.view.VelocityTracker; 24 import android.view.ViewConfiguration; 25 26 import androidx.annotation.NonNull; 27 import androidx.annotation.VisibleForTesting; 28 29 import com.android.launcher3.testing.TestProtocol; 30 31 import java.util.LinkedList; 32 import java.util.Queue; 33 34 /** 35 * Scroll/drag/swipe gesture detector. 36 * 37 * Definition of swipe is different from android system in that this detector handles 38 * 'swipe to dismiss', 'swiping up/down a container' but also keeps scrolling state before 39 * swipe action happens. 40 * 41 * @see SingleAxisSwipeDetector 42 * @see BothAxesSwipeDetector 43 */ 44 public abstract class BaseSwipeDetector { 45 46 private static final boolean DBG = false; 47 private static final String TAG = "BaseSwipeDetector"; 48 private static final float ANIMATION_DURATION = 1200; 49 /** The minimum release velocity in pixels per millisecond that triggers fling.*/ 50 private static final float RELEASE_VELOCITY_PX_MS = 1.0f; 51 private static final PointF sTempPoint = new PointF(); 52 53 private final PointF mDownPos = new PointF(); 54 private final PointF mLastPos = new PointF(); 55 protected final boolean mIsRtl; 56 protected final float mTouchSlop; 57 protected final float mMaxVelocity; 58 private final Queue<Runnable> mSetStateQueue = new LinkedList<>(); 59 60 private int mActivePointerId = INVALID_POINTER_ID; 61 private VelocityTracker mVelocityTracker; 62 private PointF mLastDisplacement = new PointF(); 63 private PointF mDisplacement = new PointF(); 64 protected PointF mSubtractDisplacement = new PointF(); 65 @VisibleForTesting ScrollState mState = ScrollState.IDLE; 66 private boolean mIsSettingState; 67 68 protected boolean mIgnoreSlopWhenSettling; 69 70 private enum ScrollState { 71 IDLE, 72 DRAGGING, // onDragStart, onDrag 73 SETTLING // onDragEnd 74 } 75 BaseSwipeDetector(@onNull ViewConfiguration config, boolean isRtl)76 protected BaseSwipeDetector(@NonNull ViewConfiguration config, boolean isRtl) { 77 mTouchSlop = config.getScaledTouchSlop(); 78 mMaxVelocity = config.getScaledMaximumFlingVelocity(); 79 mIsRtl = isRtl; 80 } 81 calculateDuration(float velocity, float progressNeeded)82 public static long calculateDuration(float velocity, float progressNeeded) { 83 // TODO: make these values constants after tuning. 84 float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity)); 85 float travelDistance = Math.max(0.2f, progressNeeded); 86 long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance); 87 if (DBG) { 88 Log.d(TAG, String.format( 89 "calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded)); 90 } 91 return duration; 92 } 93 getDownX()94 public int getDownX() { 95 return (int) mDownPos.x; 96 } 97 getDownY()98 public int getDownY() { 99 return (int) mDownPos.y; 100 } 101 /** 102 * There's no touch and there's no animation. 103 */ isIdleState()104 public boolean isIdleState() { 105 return mState == ScrollState.IDLE; 106 } 107 isSettlingState()108 public boolean isSettlingState() { 109 return mState == ScrollState.SETTLING; 110 } 111 isDraggingState()112 public boolean isDraggingState() { 113 return mState == ScrollState.DRAGGING; 114 } 115 isDraggingOrSettling()116 public boolean isDraggingOrSettling() { 117 return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING; 118 } 119 finishedScrolling()120 public void finishedScrolling() { 121 setState(ScrollState.IDLE); 122 } 123 isFling(float velocity)124 public boolean isFling(float velocity) { 125 return Math.abs(velocity) > RELEASE_VELOCITY_PX_MS; 126 } 127 onTouchEvent(MotionEvent ev)128 public boolean onTouchEvent(MotionEvent ev) { 129 int actionMasked = ev.getActionMasked(); 130 if (actionMasked == MotionEvent.ACTION_DOWN && mVelocityTracker != null) { 131 mVelocityTracker.clear(); 132 } 133 if (mVelocityTracker == null) { 134 mVelocityTracker = VelocityTracker.obtain(); 135 } 136 mVelocityTracker.addMovement(ev); 137 138 switch (actionMasked) { 139 case MotionEvent.ACTION_DOWN: 140 mActivePointerId = ev.getPointerId(0); 141 mDownPos.set(ev.getX(), ev.getY()); 142 mLastPos.set(mDownPos); 143 mLastDisplacement.set(0, 0); 144 mDisplacement.set(0, 0); 145 146 if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) { 147 setState(ScrollState.DRAGGING); 148 } 149 break; 150 //case MotionEvent.ACTION_POINTER_DOWN: 151 case MotionEvent.ACTION_POINTER_UP: 152 int ptrIdx = ev.getActionIndex(); 153 int ptrId = ev.getPointerId(ptrIdx); 154 if (ptrId == mActivePointerId) { 155 final int newPointerIdx = ptrIdx == 0 ? 1 : 0; 156 mDownPos.set( 157 ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x), 158 ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y)); 159 mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx)); 160 mActivePointerId = ev.getPointerId(newPointerIdx); 161 } 162 break; 163 case MotionEvent.ACTION_MOVE: 164 int pointerIndex = ev.findPointerIndex(mActivePointerId); 165 if (pointerIndex == INVALID_POINTER_ID) { 166 break; 167 } 168 mDisplacement.set(ev.getX(pointerIndex) - mDownPos.x, 169 ev.getY(pointerIndex) - mDownPos.y); 170 if (mIsRtl) { 171 mDisplacement.x = -mDisplacement.x; 172 } 173 174 // handle state and listener calls. 175 if (mState != ScrollState.DRAGGING && shouldScrollStart(mDisplacement)) { 176 setState(ScrollState.DRAGGING); 177 } 178 if (TestProtocol.sDebugTracing) { 179 Log.d(TestProtocol.PAUSE_NOT_DETECTED, "before report dragging"); 180 } 181 if (mState == ScrollState.DRAGGING) { 182 reportDragging(ev); 183 } 184 mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); 185 break; 186 case MotionEvent.ACTION_CANCEL: 187 case MotionEvent.ACTION_UP: 188 // These are synthetic events and there is no need to update internal values. 189 if (mState == ScrollState.DRAGGING) { 190 setState(ScrollState.SETTLING); 191 } 192 mVelocityTracker.recycle(); 193 mVelocityTracker = null; 194 break; 195 default: 196 break; 197 } 198 return true; 199 } 200 201 //------------------- ScrollState transition diagram ----------------------------------- 202 // 203 // IDLE -> (mDisplacement > mTouchSlop) -> DRAGGING 204 // DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING 205 // SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING 206 // SETTLING -> (View settled) -> IDLE 207 setState(ScrollState newState)208 private void setState(ScrollState newState) { 209 if (mIsSettingState) { 210 mSetStateQueue.add(() -> setState(newState)); 211 return; 212 } 213 mIsSettingState = true; 214 215 if (DBG) { 216 Log.d(TAG, "setState:" + mState + "->" + newState); 217 } 218 // onDragStart and onDragEnd is reported ONLY on state transition 219 if (newState == ScrollState.DRAGGING) { 220 initializeDragging(); 221 if (mState == ScrollState.IDLE) { 222 reportDragStart(false /* recatch */); 223 } else if (mState == ScrollState.SETTLING) { 224 reportDragStart(true /* recatch */); 225 } 226 } 227 if (newState == ScrollState.SETTLING) { 228 reportDragEnd(); 229 } 230 231 mState = newState; 232 mIsSettingState = false; 233 if (!mSetStateQueue.isEmpty()) { 234 mSetStateQueue.remove().run(); 235 } 236 } 237 initializeDragging()238 private void initializeDragging() { 239 if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) { 240 mSubtractDisplacement.set(0, 0); 241 } else { 242 mSubtractDisplacement.x = mDisplacement.x > 0 ? mTouchSlop : -mTouchSlop; 243 mSubtractDisplacement.y = mDisplacement.y > 0 ? mTouchSlop : -mTouchSlop; 244 } 245 } 246 shouldScrollStart(PointF displacement)247 protected abstract boolean shouldScrollStart(PointF displacement); 248 reportDragStart(boolean recatch)249 private void reportDragStart(boolean recatch) { 250 reportDragStartInternal(recatch); 251 if (DBG) { 252 Log.d(TAG, "onDragStart recatch:" + recatch); 253 } 254 } 255 reportDragStartInternal(boolean recatch)256 protected abstract void reportDragStartInternal(boolean recatch); 257 reportDragging(MotionEvent event)258 private void reportDragging(MotionEvent event) { 259 if (mDisplacement != mLastDisplacement) { 260 if (DBG) { 261 Log.d(TAG, String.format("onDrag disp=%s", mDisplacement)); 262 } 263 264 mLastDisplacement.set(mDisplacement); 265 sTempPoint.set(mDisplacement.x - mSubtractDisplacement.x, 266 mDisplacement.y - mSubtractDisplacement.y); 267 reportDraggingInternal(sTempPoint, event); 268 } 269 } 270 reportDraggingInternal(PointF displacement, MotionEvent event)271 protected abstract void reportDraggingInternal(PointF displacement, MotionEvent event); 272 reportDragEnd()273 private void reportDragEnd() { 274 mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); 275 PointF velocity = new PointF(mVelocityTracker.getXVelocity() / 1000, 276 mVelocityTracker.getYVelocity() / 1000); 277 if (mIsRtl) { 278 velocity.x = -velocity.x; 279 } 280 if (DBG) { 281 Log.d(TAG, String.format("onScrollEnd disp=%.1s, velocity=%.1s", 282 mDisplacement, velocity)); 283 } 284 285 reportDragEndInternal(velocity); 286 } 287 reportDragEndInternal(PointF velocity)288 protected abstract void reportDragEndInternal(PointF velocity); 289 } 290