• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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