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