1 /* 2 * Copyright (C) 2019 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.quickstep.util; 17 18 import android.content.Context; 19 import android.content.res.Resources; 20 import android.view.MotionEvent; 21 import android.view.VelocityTracker; 22 23 import com.android.launcher3.Alarm; 24 import com.android.launcher3.R; 25 import com.android.launcher3.compat.AccessibilityManagerCompat; 26 27 /** 28 * Given positions along x- or y-axis, tracks velocity and acceleration and determines when there is 29 * a pause in motion. 30 */ 31 public class MotionPauseDetector { 32 33 // The percentage of the previous speed that determines whether this is a rapid deceleration. 34 // The bigger this number, the easier it is to trigger the first pause. 35 private static final float RAPID_DECELERATION_FACTOR = 0.6f; 36 37 /** If no motion is added for this amount of time, assume the motion has paused. */ 38 private static final long FORCE_PAUSE_TIMEOUT = 300; 39 40 /** 41 * After {@link #mMakePauseHarderToTrigger}, must move slowly for this long to trigger a pause. 42 */ 43 private static final long HARDER_TRIGGER_TIMEOUT = 400; 44 45 private final float mSpeedVerySlow; 46 private final float mSpeedSlow; 47 private final float mSpeedSomewhatFast; 48 private final float mSpeedFast; 49 private final Alarm mForcePauseTimeout; 50 private final boolean mMakePauseHarderToTrigger; 51 private final Context mContext; 52 private final SystemVelocityProvider mVelocityProvider; 53 54 private Float mPreviousVelocity = null; 55 56 private OnMotionPauseListener mOnMotionPauseListener; 57 private boolean mIsPaused; 58 // Bias more for the first pause to make it feel extra responsive. 59 private boolean mHasEverBeenPaused; 60 /** @see #setDisallowPause(boolean) */ 61 private boolean mDisallowPause; 62 // Time at which speed became < mSpeedSlow (only used if mMakePauseHarderToTrigger == true). 63 private long mSlowStartTime; 64 MotionPauseDetector(Context context)65 public MotionPauseDetector(Context context) { 66 this(context, false); 67 } 68 69 /** 70 * @param makePauseHarderToTrigger Used for gestures that require a more explicit pause. 71 */ MotionPauseDetector(Context context, boolean makePauseHarderToTrigger)72 public MotionPauseDetector(Context context, boolean makePauseHarderToTrigger) { 73 this(context, makePauseHarderToTrigger, MotionEvent.AXIS_Y); 74 } 75 76 /** 77 * @param makePauseHarderToTrigger Used for gestures that require a more explicit pause. 78 */ MotionPauseDetector(Context context, boolean makePauseHarderToTrigger, int axis)79 public MotionPauseDetector(Context context, boolean makePauseHarderToTrigger, int axis) { 80 mContext = context; 81 Resources res = context.getResources(); 82 mSpeedVerySlow = res.getDimension(R.dimen.motion_pause_detector_speed_very_slow); 83 mSpeedSlow = res.getDimension(R.dimen.motion_pause_detector_speed_slow); 84 mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast); 85 mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast); 86 mForcePauseTimeout = new Alarm(); 87 mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */)); 88 mMakePauseHarderToTrigger = makePauseHarderToTrigger; 89 mVelocityProvider = new SystemVelocityProvider(axis); 90 } 91 92 /** 93 * Get callbacks for when motion pauses and resumes. 94 */ setOnMotionPauseListener(OnMotionPauseListener listener)95 public void setOnMotionPauseListener(OnMotionPauseListener listener) { 96 mOnMotionPauseListener = listener; 97 } 98 99 /** 100 * @param disallowPause If true, we will not detect any pauses until this is set to false again. 101 */ setDisallowPause(boolean disallowPause)102 public void setDisallowPause(boolean disallowPause) { 103 mDisallowPause = disallowPause; 104 updatePaused(mIsPaused); 105 } 106 107 /** 108 * Computes velocity and acceleration to determine whether the motion is paused. 109 * @param ev The motion being tracked. 110 */ addPosition(MotionEvent ev)111 public void addPosition(MotionEvent ev) { 112 addPosition(ev, 0); 113 } 114 115 /** 116 * Computes velocity and acceleration to determine whether the motion is paused. 117 * @param ev The motion being tracked. 118 * @param pointerIndex Index for the pointer being tracked in the motion event 119 */ addPosition(MotionEvent ev, int pointerIndex)120 public void addPosition(MotionEvent ev, int pointerIndex) { 121 mForcePauseTimeout.setAlarm(mMakePauseHarderToTrigger 122 ? HARDER_TRIGGER_TIMEOUT 123 : FORCE_PAUSE_TIMEOUT); 124 float newVelocity = mVelocityProvider.addMotionEvent(ev, ev.getPointerId(pointerIndex)); 125 if (mPreviousVelocity != null) { 126 checkMotionPaused(newVelocity, mPreviousVelocity, ev.getEventTime()); 127 } 128 mPreviousVelocity = newVelocity; 129 } 130 checkMotionPaused(float velocity, float prevVelocity, long time)131 private void checkMotionPaused(float velocity, float prevVelocity, long time) { 132 float speed = Math.abs(velocity); 133 float previousSpeed = Math.abs(prevVelocity); 134 boolean isPaused; 135 if (mIsPaused) { 136 // Continue to be paused until moving at a fast speed. 137 isPaused = speed < mSpeedFast || previousSpeed < mSpeedFast; 138 } else { 139 if (velocity < 0 != prevVelocity < 0) { 140 // We're just changing directions, not necessarily stopping. 141 isPaused = false; 142 } else { 143 isPaused = speed < mSpeedVerySlow && previousSpeed < mSpeedVerySlow; 144 if (!isPaused && !mHasEverBeenPaused) { 145 // We want to be more aggressive about detecting the first pause to ensure it 146 // feels as responsive as possible; getting two very slow speeds back to back 147 // takes too long, so also check for a rapid deceleration. 148 boolean isRapidDeceleration = speed < previousSpeed * RAPID_DECELERATION_FACTOR; 149 isPaused = isRapidDeceleration && speed < mSpeedSomewhatFast; 150 } 151 if (mMakePauseHarderToTrigger) { 152 if (speed < mSpeedSlow) { 153 if (mSlowStartTime == 0) { 154 mSlowStartTime = time; 155 } 156 isPaused = time - mSlowStartTime >= HARDER_TRIGGER_TIMEOUT; 157 } else { 158 mSlowStartTime = 0; 159 isPaused = false; 160 } 161 } 162 } 163 } 164 updatePaused(isPaused); 165 } 166 167 private void updatePaused(boolean isPaused) { 168 if (mDisallowPause) { 169 isPaused = false; 170 } 171 if (mIsPaused != isPaused) { 172 mIsPaused = isPaused; 173 boolean isFirstDetectedPause = !mHasEverBeenPaused && mIsPaused; 174 if (mIsPaused) { 175 AccessibilityManagerCompat.sendPauseDetectedEventToTest(mContext); 176 mHasEverBeenPaused = true; 177 } 178 if (mOnMotionPauseListener != null) { 179 if (isFirstDetectedPause) { 180 mOnMotionPauseListener.onMotionPauseDetected(); 181 } 182 // Null check again as onMotionPauseDetected() maybe have called clear(). 183 if (mOnMotionPauseListener != null) { 184 mOnMotionPauseListener.onMotionPauseChanged(mIsPaused); 185 } 186 } 187 } 188 } 189 190 public void clear() { 191 mVelocityProvider.clear(); 192 mPreviousVelocity = null; 193 setOnMotionPauseListener(null); 194 mIsPaused = mHasEverBeenPaused = false; 195 mSlowStartTime = 0; 196 mForcePauseTimeout.cancelAlarm(); 197 } 198 199 public boolean isPaused() { 200 return mIsPaused; 201 } 202 203 public interface OnMotionPauseListener { 204 /** Called only the first time motion pause is detected. */ 205 void onMotionPauseDetected(); 206 /** Called every time motion changes from paused to not paused and vice versa. */ 207 default void onMotionPauseChanged(boolean isPaused) { } 208 } 209 210 private static class SystemVelocityProvider { 211 212 private final VelocityTracker mVelocityTracker; 213 private final int mAxis; 214 215 SystemVelocityProvider(int axis) { 216 mVelocityTracker = VelocityTracker.obtain(); 217 mAxis = axis; 218 } 219 220 /** 221 * Adds a new motion events, and returns the velocity at this point, or null if 222 * the velocity is not available 223 */ 224 public float addMotionEvent(MotionEvent ev, int pointer) { 225 mVelocityTracker.addMovement(ev); 226 mVelocityTracker.computeCurrentVelocity(1); // px / ms 227 return mAxis == MotionEvent.AXIS_X 228 ? mVelocityTracker.getXVelocity(pointer) 229 : mVelocityTracker.getYVelocity(pointer); 230 } 231 232 /** 233 * Clears all stored motion event records 234 */ 235 public void clear() { 236 mVelocityTracker.clear(); 237 } 238 } 239 } 240