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