summaryrefslogtreecommitdiff
path: root/core/java/android/window/BackTouchTracker.java
blob: d8a1d38464e1dc3b54d827a31420223c06ae9a97 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.window;

import android.annotation.FloatRange;
import android.os.SystemProperties;
import android.util.MathUtils;
import android.view.MotionEvent;
import android.view.RemoteAnimationTarget;

import java.io.PrintWriter;

/**
 * Helper class to record the touch location for gesture and generate back events.
 * @hide
 */
public class BackTouchTracker {
    private static final String PREDICTIVE_BACK_LINEAR_DISTANCE_PROP =
            "persist.wm.debug.predictive_back_linear_distance";
    private static final int LINEAR_DISTANCE = SystemProperties
            .getInt(PREDICTIVE_BACK_LINEAR_DISTANCE_PROP, -1);
    private float mLinearDistance = LINEAR_DISTANCE;
    private float mMaxDistance;
    private float mNonLinearFactor;
    /**
     * Location of the latest touch event
     */
    private float mLatestTouchX;
    private float mLatestTouchY;
    private boolean mTriggerBack;

    /**
     * Location of the initial touch event of the back gesture.
     */
    private float mInitTouchX;
    private float mInitTouchY;
    private float mStartThresholdX;
    private int mSwipeEdge;
    private boolean mShouldUpdateStartLocation = false;
    private TouchTrackerState mState = TouchTrackerState.INITIAL;
    private boolean mIsInterceptedMotionEvent;

    /**
     * Updates the tracker with a new motion event.
     */
    public void update(float touchX, float touchY) {
        /**
         * If back was previously cancelled but the user has started swiping in the forward
         * direction again, restart back.
         */
        if ((touchX < mStartThresholdX && mSwipeEdge == BackEvent.EDGE_LEFT)
                || (touchX > mStartThresholdX && mSwipeEdge == BackEvent.EDGE_RIGHT)) {
            mStartThresholdX = touchX;
            if ((mSwipeEdge == BackEvent.EDGE_LEFT && mStartThresholdX < mInitTouchX)
                    || (mSwipeEdge == BackEvent.EDGE_RIGHT && mStartThresholdX > mInitTouchX)) {
                mInitTouchX = mStartThresholdX;
            }
        }
        mLatestTouchX = touchX;
        mLatestTouchY = touchY;
    }

    /** Sets whether the back gesture is past the trigger threshold. */
    public void setTriggerBack(boolean triggerBack) {
        if (mTriggerBack != triggerBack && !triggerBack) {
            mStartThresholdX = mLatestTouchX;
        }
        mTriggerBack = triggerBack;
    }

    /** Gets whether the back gesture is past the trigger threshold. */
    public boolean getTriggerBack() {
        return mTriggerBack;
    }


    /** Returns if the start location should be updated. */
    public boolean shouldUpdateStartLocation() {
        return mShouldUpdateStartLocation;
    }

    /** Sets if the start location should be updated. */
    public void setShouldUpdateStartLocation(boolean shouldUpdate) {
        mShouldUpdateStartLocation = shouldUpdate;
    }

    /** Sets the state of the touch tracker. */
    public void setState(TouchTrackerState state) {
        mState = state;
    }

    /** Returns if the tracker is in initial state. */
    public boolean isInInitialState() {
        return mState == TouchTrackerState.INITIAL;
    }

    /** Returns if a back gesture is active. */
    public boolean isActive() {
        return mState == TouchTrackerState.ACTIVE;
    }

    /** Returns if a back gesture has been finished. */
    public boolean isFinished() {
        return mState == TouchTrackerState.FINISHED;
    }

    /**
     * Returns whether current app should not receive motion event.
     */
    public boolean isInterceptedMotionEvent() {
        return mIsInterceptedMotionEvent;
    }

    /**
     * Marks the app will not receive motion event from current gesture.
     */
    public void setMotionEventIntercepted() {
        mIsInterceptedMotionEvent = true;
    }

    /** Sets the start location of the back gesture. */
    public void setGestureStartLocation(float touchX, float touchY, int swipeEdge) {
        mInitTouchX = touchX;
        mInitTouchY = touchY;
        mLatestTouchX = touchX;
        mLatestTouchY = touchY;
        mSwipeEdge = swipeEdge;
        mStartThresholdX = mInitTouchX;
    }

    /** Update the start location used to compute the progress to the latest touch location. */
    public void updateStartLocation() {
        mInitTouchX = mLatestTouchX;
        mInitTouchY = mLatestTouchY;
        mStartThresholdX = mInitTouchX;
        mShouldUpdateStartLocation = false;
    }

    /**
     * Updates the swipe edge. This is useful when it's not clear yet which swipe edge the gesture
     * is performed on from the start of the gesture (for example trackpad back gestures).
     *
     * @param swipeEdge the updated swipeEdge value
     */
    public void updateSwipeEdge(@BackEvent.SwipeEdge int swipeEdge) {
        mSwipeEdge = swipeEdge;
    }

    /** Resets the tracker. */
    public void reset() {
        mInitTouchX = 0;
        mInitTouchY = 0;
        mStartThresholdX = 0;
        mTriggerBack = false;
        mState = TouchTrackerState.INITIAL;
        mSwipeEdge = BackEvent.EDGE_LEFT;
        mShouldUpdateStartLocation = false;
        mIsInterceptedMotionEvent = false;
    }

    /** Creates a start {@link BackMotionEvent}. */
    public BackMotionEvent createStartEvent(RemoteAnimationTarget target) {
        return new BackMotionEvent(
                /* touchX = */ mInitTouchX,
                /* touchY = */ mInitTouchY,
                /* frameTimeMillis = */ 0,
                /* progress = */ 0,
                /* triggerBack = */ mTriggerBack,
                /* swipeEdge = */ mSwipeEdge,
                /* departingAnimationTarget = */ target);
    }

    /** Creates a progress {@link BackMotionEvent}. */
    public BackMotionEvent createProgressEvent() {
        float progress = getProgress(mLatestTouchX);
        return createProgressEvent(progress);
    }

    /**
     * Progress value computed from the touch position.
     *
     * @param touchX the X touch position of the {@link MotionEvent}.
     * @return progress value
     */
    @FloatRange(from = 0.0, to = 1.0)
    public float getProgress(float touchX) {
        // If back is committed, progress is the distance between the last and first touch
        // point, divided by the max drag distance. Otherwise, it's the distance between
        // the last touch point and the starting threshold, divided by max drag distance.
        // The starting threshold is initially the first touch location, and updated to
        // the location everytime back is restarted after being cancelled.
        float startX = mTriggerBack ? mInitTouchX : mStartThresholdX;
        float distance;
        if (mSwipeEdge == BackEvent.EDGE_LEFT) {
            distance = touchX - startX;
        } else {
            distance = startX - touchX;
        }
        float deltaX = Math.max(0f, distance);
        float linearDistance = mLinearDistance;
        float maxDistance = getMaxDistance();
        maxDistance = maxDistance == 0 ? 1 : maxDistance;
        float progress;
        if (linearDistance < maxDistance) {
            // Up to linearDistance it behaves linearly, then slowly reaches 1f.

            // maxDistance is composed of linearDistance + nonLinearDistance
            float nonLinearDistance = maxDistance - linearDistance;
            float initialTarget = linearDistance + nonLinearDistance * mNonLinearFactor;

            boolean isLinear = deltaX <= linearDistance;
            if (isLinear) {
                progress = deltaX / initialTarget;
            } else {
                float nonLinearDeltaX = deltaX - linearDistance;
                float nonLinearProgress = nonLinearDeltaX / nonLinearDistance;
                float currentTarget = MathUtils.lerp(
                        /* start = */ initialTarget,
                        /* stop = */ maxDistance,
                        /* amount = */ nonLinearProgress);
                progress = deltaX / currentTarget;
            }
        } else {
            // Always linear behavior.
            progress = deltaX / maxDistance;
        }
        return MathUtils.constrain(progress, 0, 1);
    }

    /**
     * Maximum distance in pixels.
     * Progress is considered to be completed (1f) when this limit is exceeded.
     */
    public float getMaxDistance() {
        return mMaxDistance;
    }

    public float getLinearDistance() {
        return mLinearDistance;
    }

    public float getNonLinearFactor() {
        return mNonLinearFactor;
    }

    /** Creates a progress {@link BackMotionEvent} for the given progress. */
    public BackMotionEvent createProgressEvent(float progress) {
        return new BackMotionEvent(
                /* touchX = */ mLatestTouchX,
                /* touchY = */ mLatestTouchY,
                /* frameTimeMillis = */ 0,
                /* progress = */ progress,
                /* triggerBack = */ mTriggerBack,
                /* swipeEdge = */ mSwipeEdge,
                /* departingAnimationTarget = */ null);
    }

    /** Sets the thresholds for computing progress. */
    public void setProgressThresholds(float linearDistance, float maxDistance,
            float nonLinearFactor) {
        if (LINEAR_DISTANCE >= 0) {
            mLinearDistance = LINEAR_DISTANCE;
        } else {
            mLinearDistance = linearDistance;
        }
        mMaxDistance = maxDistance;
        mNonLinearFactor = nonLinearFactor;
    }

    /** Dumps debugging info. */
    public void dump(PrintWriter pw, String prefix) {
        pw.println(prefix + "BackTouchTracker state:");
        pw.println(prefix + "  mState=" + mState);
        pw.println(prefix + "  mTriggerBack=" + mTriggerBack);
    }

    public enum TouchTrackerState {
        INITIAL, ACTIVE, FINISHED
    }

}