• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 
17 package android.window;
18 
19 import android.annotation.FloatRange;
20 import android.os.SystemProperties;
21 import android.util.MathUtils;
22 import android.view.MotionEvent;
23 import android.view.RemoteAnimationTarget;
24 
25 import java.io.PrintWriter;
26 
27 /**
28  * Helper class to record the touch location for gesture and generate back events.
29  * @hide
30  */
31 public class BackTouchTracker {
32     private static final String PREDICTIVE_BACK_LINEAR_DISTANCE_PROP =
33             "persist.wm.debug.predictive_back_linear_distance";
34     private static final int LINEAR_DISTANCE = SystemProperties
35             .getInt(PREDICTIVE_BACK_LINEAR_DISTANCE_PROP, -1);
36     private float mLinearDistance = LINEAR_DISTANCE;
37     private float mMaxDistance;
38     private float mNonLinearFactor;
39     /**
40      * Location of the latest touch event
41      */
42     private float mLatestTouchX;
43     private float mLatestTouchY;
44     private boolean mTriggerBack;
45 
46     /**
47      * Location of the initial touch event of the back gesture.
48      */
49     private float mInitTouchX;
50     private float mInitTouchY;
51     private float mStartThresholdX;
52     private int mSwipeEdge;
53     private boolean mShouldUpdateStartLocation = false;
54     private TouchTrackerState mState = TouchTrackerState.INITIAL;
55 
56     /**
57      * Updates the tracker with a new motion event.
58      */
update(float touchX, float touchY)59     public void update(float touchX, float touchY) {
60         /**
61          * If back was previously cancelled but the user has started swiping in the forward
62          * direction again, restart back.
63          */
64         if ((touchX < mStartThresholdX && mSwipeEdge == BackEvent.EDGE_LEFT)
65                 || (touchX > mStartThresholdX && mSwipeEdge == BackEvent.EDGE_RIGHT)) {
66             mStartThresholdX = touchX;
67             if ((mSwipeEdge == BackEvent.EDGE_LEFT && mStartThresholdX < mInitTouchX)
68                     || (mSwipeEdge == BackEvent.EDGE_RIGHT && mStartThresholdX > mInitTouchX)) {
69                 mInitTouchX = mStartThresholdX;
70             }
71         }
72         mLatestTouchX = touchX;
73         mLatestTouchY = touchY;
74     }
75 
76     /** Sets whether the back gesture is past the trigger threshold. */
setTriggerBack(boolean triggerBack)77     public void setTriggerBack(boolean triggerBack) {
78         if (mTriggerBack != triggerBack && !triggerBack) {
79             mStartThresholdX = mLatestTouchX;
80         }
81         mTriggerBack = triggerBack;
82     }
83 
84     /** Gets whether the back gesture is past the trigger threshold. */
getTriggerBack()85     public boolean getTriggerBack() {
86         return mTriggerBack;
87     }
88 
89 
90     /** Returns if the start location should be updated. */
shouldUpdateStartLocation()91     public boolean shouldUpdateStartLocation() {
92         return mShouldUpdateStartLocation;
93     }
94 
95     /** Sets if the start location should be updated. */
setShouldUpdateStartLocation(boolean shouldUpdate)96     public void setShouldUpdateStartLocation(boolean shouldUpdate) {
97         mShouldUpdateStartLocation = shouldUpdate;
98     }
99 
100     /** Sets the state of the touch tracker. */
setState(TouchTrackerState state)101     public void setState(TouchTrackerState state) {
102         mState = state;
103     }
104 
105     /** Returns if the tracker is in initial state. */
isInInitialState()106     public boolean isInInitialState() {
107         return mState == TouchTrackerState.INITIAL;
108     }
109 
110     /** Returns if a back gesture is active. */
isActive()111     public boolean isActive() {
112         return mState == TouchTrackerState.ACTIVE;
113     }
114 
115     /** Returns if a back gesture has been finished. */
isFinished()116     public boolean isFinished() {
117         return mState == TouchTrackerState.FINISHED;
118     }
119 
120     /** Sets the start location of the back gesture. */
setGestureStartLocation(float touchX, float touchY, int swipeEdge)121     public void setGestureStartLocation(float touchX, float touchY, int swipeEdge) {
122         mInitTouchX = touchX;
123         mInitTouchY = touchY;
124         mLatestTouchX = touchX;
125         mLatestTouchY = touchY;
126         mSwipeEdge = swipeEdge;
127         mStartThresholdX = mInitTouchX;
128     }
129 
130     /** Update the start location used to compute the progress to the latest touch location. */
updateStartLocation()131     public void updateStartLocation() {
132         mInitTouchX = mLatestTouchX;
133         mInitTouchY = mLatestTouchY;
134         mStartThresholdX = mInitTouchX;
135         mShouldUpdateStartLocation = false;
136     }
137 
138     /** Resets the tracker. */
reset()139     public void reset() {
140         mInitTouchX = 0;
141         mInitTouchY = 0;
142         mStartThresholdX = 0;
143         mTriggerBack = false;
144         mState = TouchTrackerState.INITIAL;
145         mSwipeEdge = BackEvent.EDGE_LEFT;
146         mShouldUpdateStartLocation = false;
147     }
148 
149     /** Creates a start {@link BackMotionEvent}. */
createStartEvent(RemoteAnimationTarget target)150     public BackMotionEvent createStartEvent(RemoteAnimationTarget target) {
151         return new BackMotionEvent(
152                 /* touchX = */ mInitTouchX,
153                 /* touchY = */ mInitTouchY,
154                 /* frameTimeMillis = */ 0,
155                 /* progress = */ 0,
156                 /* triggerBack = */ mTriggerBack,
157                 /* swipeEdge = */ mSwipeEdge,
158                 /* departingAnimationTarget = */ target);
159     }
160 
161     /** Creates a progress {@link BackMotionEvent}. */
createProgressEvent()162     public BackMotionEvent createProgressEvent() {
163         float progress = getProgress(mLatestTouchX);
164         return createProgressEvent(progress);
165     }
166 
167     /**
168      * Progress value computed from the touch position.
169      *
170      * @param touchX the X touch position of the {@link MotionEvent}.
171      * @return progress value
172      */
173     @FloatRange(from = 0.0, to = 1.0)
getProgress(float touchX)174     public float getProgress(float touchX) {
175         // If back is committed, progress is the distance between the last and first touch
176         // point, divided by the max drag distance. Otherwise, it's the distance between
177         // the last touch point and the starting threshold, divided by max drag distance.
178         // The starting threshold is initially the first touch location, and updated to
179         // the location everytime back is restarted after being cancelled.
180         float startX = mTriggerBack ? mInitTouchX : mStartThresholdX;
181         float distance;
182         if (mSwipeEdge == BackEvent.EDGE_LEFT) {
183             distance = touchX - startX;
184         } else {
185             distance = startX - touchX;
186         }
187         float deltaX = Math.max(0f, distance);
188         float linearDistance = mLinearDistance;
189         float maxDistance = getMaxDistance();
190         maxDistance = maxDistance == 0 ? 1 : maxDistance;
191         float progress;
192         if (linearDistance < maxDistance) {
193             // Up to linearDistance it behaves linearly, then slowly reaches 1f.
194 
195             // maxDistance is composed of linearDistance + nonLinearDistance
196             float nonLinearDistance = maxDistance - linearDistance;
197             float initialTarget = linearDistance + nonLinearDistance * mNonLinearFactor;
198 
199             boolean isLinear = deltaX <= linearDistance;
200             if (isLinear) {
201                 progress = deltaX / initialTarget;
202             } else {
203                 float nonLinearDeltaX = deltaX - linearDistance;
204                 float nonLinearProgress = nonLinearDeltaX / nonLinearDistance;
205                 float currentTarget = MathUtils.lerp(
206                         /* start = */ initialTarget,
207                         /* stop = */ maxDistance,
208                         /* amount = */ nonLinearProgress);
209                 progress = deltaX / currentTarget;
210             }
211         } else {
212             // Always linear behavior.
213             progress = deltaX / maxDistance;
214         }
215         return MathUtils.constrain(progress, 0, 1);
216     }
217 
218     /**
219      * Maximum distance in pixels.
220      * Progress is considered to be completed (1f) when this limit is exceeded.
221      */
getMaxDistance()222     public float getMaxDistance() {
223         return mMaxDistance;
224     }
225 
getLinearDistance()226     public float getLinearDistance() {
227         return mLinearDistance;
228     }
229 
getNonLinearFactor()230     public float getNonLinearFactor() {
231         return mNonLinearFactor;
232     }
233 
234     /** Creates a progress {@link BackMotionEvent} for the given progress. */
createProgressEvent(float progress)235     public BackMotionEvent createProgressEvent(float progress) {
236         return new BackMotionEvent(
237                 /* touchX = */ mLatestTouchX,
238                 /* touchY = */ mLatestTouchY,
239                 /* frameTimeMillis = */ 0,
240                 /* progress = */ progress,
241                 /* triggerBack = */ mTriggerBack,
242                 /* swipeEdge = */ mSwipeEdge,
243                 /* departingAnimationTarget = */ null);
244     }
245 
246     /** Sets the thresholds for computing progress. */
setProgressThresholds(float linearDistance, float maxDistance, float nonLinearFactor)247     public void setProgressThresholds(float linearDistance, float maxDistance,
248             float nonLinearFactor) {
249         if (LINEAR_DISTANCE >= 0) {
250             mLinearDistance = LINEAR_DISTANCE;
251         } else {
252             mLinearDistance = linearDistance;
253         }
254         mMaxDistance = maxDistance;
255         mNonLinearFactor = nonLinearFactor;
256     }
257 
258     /** Dumps debugging info. */
dump(PrintWriter pw, String prefix)259     public void dump(PrintWriter pw, String prefix) {
260         pw.println(prefix + "BackTouchTracker state:");
261         pw.println(prefix + "  mState=" + mState);
262         pw.println(prefix + "  mTriggerBack=" + mTriggerBack);
263     }
264 
265     public enum TouchTrackerState {
266         INITIAL, ACTIVE, FINISHED
267     }
268 
269 }
270