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