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