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