1 /* 2 * Copyright (C) 2020 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 package com.android.quickstep.interaction; 17 18 import static com.android.launcher3.Utilities.squaredHypot; 19 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_COMPLETED; 20 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_BAD_ANGLE; 21 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT; 22 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_GESTURE_COMPLETED; 23 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_NOT_STARTED_TOO_FAR_FROM_EDGE; 24 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_OR_OVERVIEW_CANCELLED; 25 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION; 26 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_GESTURE_COMPLETED; 27 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE; 28 29 import android.animation.ValueAnimator; 30 import android.content.Context; 31 import android.content.res.Resources; 32 import android.graphics.Point; 33 import android.graphics.PointF; 34 import android.graphics.RectF; 35 import android.os.SystemClock; 36 import android.view.Display; 37 import android.view.GestureDetector; 38 import android.view.MotionEvent; 39 import android.view.Surface; 40 import android.view.View; 41 import android.view.View.OnTouchListener; 42 import android.view.ViewConfiguration; 43 44 import androidx.annotation.Nullable; 45 46 import com.android.launcher3.R; 47 import com.android.launcher3.ResourceUtils; 48 import com.android.launcher3.anim.Interpolators; 49 import com.android.launcher3.util.VibratorWrapper; 50 import com.android.quickstep.SysUINavigationMode.Mode; 51 import com.android.quickstep.util.NavBarPosition; 52 import com.android.quickstep.util.TriggerSwipeUpTouchTracker; 53 import com.android.systemui.shared.system.QuickStepContract; 54 55 /** Utility class to handle Home and Assistant gestures. */ 56 public class NavBarGestureHandler implements OnTouchListener, 57 TriggerSwipeUpTouchTracker.OnSwipeUpListener { 58 59 private static final String LOG_TAG = "NavBarGestureHandler"; 60 private static final long RETRACT_GESTURE_ANIMATION_DURATION_MS = 300; 61 62 private final Context mContext; 63 private final Point mDisplaySize = new Point(); 64 private final TriggerSwipeUpTouchTracker mSwipeUpTouchTracker; 65 private final int mBottomGestureHeight; 66 private final GestureDetector mAssistantGestureDetector; 67 private final int mAssistantAngleThreshold; 68 private final RectF mAssistantLeftRegion = new RectF(); 69 private final RectF mAssistantRightRegion = new RectF(); 70 private final float mAssistantDragDistThreshold; 71 private final float mAssistantFlingDistThreshold; 72 private final long mAssistantTimeThreshold; 73 private final float mAssistantSquaredSlop; 74 private final PointF mAssistantStartDragPos = new PointF(); 75 private final PointF mDownPos = new PointF(); 76 private final PointF mLastPos = new PointF(); 77 private boolean mTouchCameFromAssistantCorner; 78 private boolean mTouchCameFromNavBar; 79 private boolean mPassedAssistantSlop; 80 private boolean mAssistantGestureActive; 81 private boolean mLaunchedAssistant; 82 private long mAssistantDragStartTime; 83 private float mAssistantDistance; 84 private float mAssistantTimeFraction; 85 private float mAssistantLastProgress; 86 @Nullable 87 private NavBarGestureAttemptCallback mGestureCallback; 88 NavBarGestureHandler(Context context)89 NavBarGestureHandler(Context context) { 90 mContext = context; 91 final Display display = mContext.getDisplay(); 92 final int displayRotation; 93 if (display == null) { 94 displayRotation = Surface.ROTATION_0; 95 } else { 96 displayRotation = display.getRotation(); 97 display.getRealSize(mDisplaySize); 98 } 99 mSwipeUpTouchTracker = 100 new TriggerSwipeUpTouchTracker(context, true /*disableHorizontalSwipe*/, 101 new NavBarPosition(Mode.NO_BUTTON, displayRotation), 102 null /*onInterceptTouch*/, this); 103 104 final Resources resources = context.getResources(); 105 mBottomGestureHeight = 106 ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, resources); 107 mAssistantDragDistThreshold = 108 resources.getDimension(R.dimen.gestures_assistant_drag_threshold); 109 mAssistantFlingDistThreshold = 110 resources.getDimension(R.dimen.gestures_assistant_fling_threshold); 111 mAssistantTimeThreshold = 112 resources.getInteger(R.integer.assistant_gesture_min_time_threshold); 113 mAssistantAngleThreshold = 114 resources.getInteger(R.integer.assistant_gesture_corner_deg_threshold); 115 116 mAssistantGestureDetector = new GestureDetector(context, new AssistantGestureListener()); 117 int assistantWidth = resources.getDimensionPixelSize(R.dimen.gestures_assistant_width); 118 final float assistantHeight = Math.max(mBottomGestureHeight, 119 QuickStepContract.getWindowCornerRadius(resources)); 120 mAssistantLeftRegion.bottom = mAssistantRightRegion.bottom = mDisplaySize.y; 121 mAssistantLeftRegion.top = mAssistantRightRegion.top = mDisplaySize.y - assistantHeight; 122 mAssistantLeftRegion.left = 0; 123 mAssistantLeftRegion.right = assistantWidth; 124 mAssistantRightRegion.right = mDisplaySize.x; 125 mAssistantRightRegion.left = mDisplaySize.x - assistantWidth; 126 float slop = ViewConfiguration.get(context).getScaledTouchSlop(); 127 mAssistantSquaredSlop = slop * slop; 128 } 129 registerNavBarGestureAttemptCallback(NavBarGestureAttemptCallback callback)130 void registerNavBarGestureAttemptCallback(NavBarGestureAttemptCallback callback) { 131 mGestureCallback = callback; 132 } 133 unregisterNavBarGestureAttemptCallback()134 void unregisterNavBarGestureAttemptCallback() { 135 mGestureCallback = null; 136 } 137 138 @Override onSwipeUp(boolean wasFling, PointF finalVelocity)139 public void onSwipeUp(boolean wasFling, PointF finalVelocity) { 140 if (mGestureCallback == null || mAssistantGestureActive) { 141 return; 142 } 143 finalVelocity.set(finalVelocity.x / 1000, finalVelocity.y / 1000); 144 if (mTouchCameFromNavBar) { 145 mGestureCallback.onNavBarGestureAttempted(wasFling 146 ? HOME_GESTURE_COMPLETED : OVERVIEW_GESTURE_COMPLETED, finalVelocity); 147 } else { 148 mGestureCallback.onNavBarGestureAttempted(wasFling 149 ? HOME_NOT_STARTED_TOO_FAR_FROM_EDGE : OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE, 150 finalVelocity); 151 } 152 } 153 154 @Override onSwipeUpCancelled()155 public void onSwipeUpCancelled() { 156 if (mGestureCallback != null && !mAssistantGestureActive) { 157 mGestureCallback.onNavBarGestureAttempted(HOME_OR_OVERVIEW_CANCELLED, new PointF()); 158 } 159 } 160 161 @Override onTouch(View view, MotionEvent event)162 public boolean onTouch(View view, MotionEvent event) { 163 int action = event.getAction(); 164 boolean intercepted = mSwipeUpTouchTracker.interceptedTouch(); 165 switch (action) { 166 case MotionEvent.ACTION_DOWN: 167 mDownPos.set(event.getX(), event.getY()); 168 mLastPos.set(mDownPos); 169 mTouchCameFromAssistantCorner = 170 mAssistantLeftRegion.contains(event.getX(), event.getY()) 171 || mAssistantRightRegion.contains(event.getX(), event.getY()); 172 mAssistantGestureActive = mTouchCameFromAssistantCorner; 173 mTouchCameFromNavBar = !mTouchCameFromAssistantCorner 174 && mDownPos.y >= mDisplaySize.y - mBottomGestureHeight; 175 if (!mTouchCameFromNavBar && mGestureCallback != null) { 176 mGestureCallback.setNavBarGestureProgress(null); 177 } 178 mLaunchedAssistant = false; 179 mSwipeUpTouchTracker.init(); 180 break; 181 case MotionEvent.ACTION_MOVE: 182 if (!mAssistantGestureActive) { 183 break; 184 } 185 mLastPos.set(event.getX(), event.getY()); 186 187 if (!mPassedAssistantSlop) { 188 // Normal gesture, ensure we pass the slop before we start tracking the gesture 189 if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) 190 > mAssistantSquaredSlop) { 191 192 mPassedAssistantSlop = true; 193 mAssistantStartDragPos.set(mLastPos.x, mLastPos.y); 194 mAssistantDragStartTime = SystemClock.uptimeMillis(); 195 196 mAssistantGestureActive = isValidAssistantGestureAngle( 197 mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y); 198 if (!mAssistantGestureActive && mGestureCallback != null) { 199 mGestureCallback.onNavBarGestureAttempted( 200 ASSISTANT_NOT_STARTED_BAD_ANGLE, new PointF()); 201 } 202 } 203 } else { 204 // Movement 205 mAssistantDistance = (float) Math.hypot(mLastPos.x - mAssistantStartDragPos.x, 206 mLastPos.y - mAssistantStartDragPos.y); 207 if (mAssistantDistance >= 0) { 208 final long diff = SystemClock.uptimeMillis() - mAssistantDragStartTime; 209 mAssistantTimeFraction = Math.min(diff * 1f / mAssistantTimeThreshold, 1); 210 updateAssistantProgress(); 211 } 212 } 213 break; 214 case MotionEvent.ACTION_UP: 215 case MotionEvent.ACTION_CANCEL: 216 if (mGestureCallback != null && !intercepted && mTouchCameFromNavBar) { 217 mGestureCallback.onNavBarGestureAttempted( 218 HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION, new PointF()); 219 intercepted = true; 220 break; 221 } 222 if (mAssistantGestureActive && !mLaunchedAssistant && mGestureCallback != null) { 223 mGestureCallback.onNavBarGestureAttempted( 224 ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT, new PointF()); 225 ValueAnimator animator = ValueAnimator.ofFloat(mAssistantLastProgress, 0) 226 .setDuration(RETRACT_GESTURE_ANIMATION_DURATION_MS); 227 animator.addUpdateListener(valueAnimator -> { 228 float progress = (float) valueAnimator.getAnimatedValue(); 229 mGestureCallback.setAssistantProgress(progress); 230 }); 231 animator.setInterpolator(Interpolators.DEACCEL_2); 232 animator.start(); 233 } 234 mPassedAssistantSlop = false; 235 break; 236 } 237 if (mTouchCameFromNavBar && mGestureCallback != null) { 238 mGestureCallback.setNavBarGestureProgress(event.getY() - mDownPos.y); 239 } 240 mSwipeUpTouchTracker.onMotionEvent(event); 241 mAssistantGestureDetector.onTouchEvent(event); 242 return intercepted; 243 } 244 245 /** 246 * Determine if angle is larger than threshold for assistant detection 247 */ isValidAssistantGestureAngle(float deltaX, float deltaY)248 private boolean isValidAssistantGestureAngle(float deltaX, float deltaY) { 249 float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX)); 250 251 // normalize so that angle is measured clockwise from horizontal in the bottom right corner 252 // and counterclockwise from horizontal in the bottom left corner 253 angle = angle > 90 ? 180 - angle : angle; 254 return (angle > mAssistantAngleThreshold && angle < 90); 255 } 256 updateAssistantProgress()257 private void updateAssistantProgress() { 258 if (!mLaunchedAssistant) { 259 mAssistantLastProgress = 260 Math.min(mAssistantDistance * 1f / mAssistantDragDistThreshold, 1) 261 * mAssistantTimeFraction; 262 if (mAssistantDistance >= mAssistantDragDistThreshold && mAssistantTimeFraction >= 1) { 263 startAssistant(new PointF()); 264 } else if (mGestureCallback != null) { 265 mGestureCallback.setAssistantProgress(mAssistantLastProgress); 266 } 267 } 268 } 269 startAssistant(PointF velocity)270 private void startAssistant(PointF velocity) { 271 if (mGestureCallback != null) { 272 mGestureCallback.onNavBarGestureAttempted(ASSISTANT_COMPLETED, velocity); 273 } 274 VibratorWrapper.INSTANCE.get(mContext).vibrate(VibratorWrapper.EFFECT_CLICK); 275 mLaunchedAssistant = true; 276 } 277 278 enum NavBarGestureResult { 279 UNKNOWN, 280 HOME_GESTURE_COMPLETED, 281 OVERVIEW_GESTURE_COMPLETED, 282 HOME_NOT_STARTED_TOO_FAR_FROM_EDGE, 283 OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE, 284 HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION, // Side swipe on nav bar. 285 HOME_OR_OVERVIEW_CANCELLED, 286 ASSISTANT_COMPLETED, 287 ASSISTANT_NOT_STARTED_BAD_ANGLE, 288 ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT, 289 } 290 291 /** Callback to let the UI react to attempted nav bar gestures. */ 292 interface NavBarGestureAttemptCallback { 293 /** Called whenever any touch is completed. */ onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity)294 void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity); 295 296 /** Indicates how far a touch originating in the nav bar has moved from the nav bar. */ setNavBarGestureProgress(@ullable Float displacement)297 default void setNavBarGestureProgress(@Nullable Float displacement) {} 298 299 /** Indicates the progress of an Assistant gesture. */ setAssistantProgress(float progress)300 default void setAssistantProgress(float progress) {} 301 } 302 303 private class AssistantGestureListener extends GestureDetector.SimpleOnGestureListener { 304 @Override onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)305 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 306 if (!mLaunchedAssistant && mTouchCameFromAssistantCorner) { 307 PointF velocity = new PointF(velocityX, velocityY); 308 if (!isValidAssistantGestureAngle(velocityX, -velocityY)) { 309 if (mGestureCallback != null) { 310 mGestureCallback.onNavBarGestureAttempted(ASSISTANT_NOT_STARTED_BAD_ANGLE, 311 velocity); 312 } 313 } else if (mAssistantDistance >= mAssistantFlingDistThreshold) { 314 mAssistantLastProgress = 1; 315 startAssistant(velocity); 316 } 317 } 318 return true; 319 } 320 } 321 } 322