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