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