• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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