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