/* * 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 mLatestVelocityX; private float mLatestVelocityY; private float mStartThresholdX; private int mSwipeEdge; private boolean mShouldUpdateStartLocation = false; private TouchTrackerState mState = TouchTrackerState.INITIAL; /** * Updates the tracker with a new motion event. */ public void update(float touchX, float touchY, float velocityX, float velocityY) { /** * 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; mLatestVelocityX = velocityX; mLatestVelocityY = velocityY; } /** 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; } /** 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; } /** Resets the tracker. */ public void reset() { mInitTouchX = 0; mInitTouchY = 0; mStartThresholdX = 0; mTriggerBack = false; mState = TouchTrackerState.INITIAL; mSwipeEdge = BackEvent.EDGE_LEFT; mShouldUpdateStartLocation = false; } /** Creates a start {@link BackMotionEvent}. */ public BackMotionEvent createStartEvent(RemoteAnimationTarget target) { return new BackMotionEvent( /* touchX = */ mInitTouchX, /* touchY = */ mInitTouchY, /* progress = */ 0, /* velocityX = */ 0, /* velocityY = */ 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, /* progress = */ progress, /* velocityX = */ mLatestVelocityX, /* velocityY = */ mLatestVelocityY, /* 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 } }