1 /*
2  * Copyright (C) 2019 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.util;
17 
18 import static android.view.MotionEvent.ACTION_CANCEL;
19 import static android.view.MotionEvent.ACTION_DOWN;
20 import static android.view.MotionEvent.ACTION_MOVE;
21 import static android.view.MotionEvent.ACTION_UP;
22 
23 import static com.android.launcher3.Utilities.squaredHypot;
24 import static com.android.launcher3.util.VelocityUtils.PX_PER_MS;
25 
26 import android.content.Context;
27 import android.graphics.PointF;
28 import android.view.MotionEvent;
29 import android.view.VelocityTracker;
30 
31 import androidx.annotation.NonNull;
32 
33 import com.android.launcher3.R;
34 import com.android.launcher3.Utilities;
35 
36 /**
37  * Tracks motion events to determine whether a gesture on the nav bar is a swipe up.
38  */
39 public class TriggerSwipeUpTouchTracker {
40 
41     private final PointF mDownPos = new PointF();
42     private final float mSquaredTouchSlop;
43     private final float mMinFlingVelocity;
44     private final boolean mDisableHorizontalSwipe;
45     private final NavBarPosition mNavBarPosition;
46 
47     @NonNull
48     private final OnSwipeUpListener mOnSwipeUp;
49 
50     private boolean mInterceptedTouch;
51     private VelocityTracker mVelocityTracker;
52 
TriggerSwipeUpTouchTracker(Context context, boolean disableHorizontalSwipe, NavBarPosition navBarPosition, @NonNull OnSwipeUpListener onSwipeUp)53     public TriggerSwipeUpTouchTracker(Context context, boolean disableHorizontalSwipe,
54             NavBarPosition navBarPosition, @NonNull OnSwipeUpListener onSwipeUp) {
55         mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
56         mMinFlingVelocity = context.getResources().getDimension(
57                 R.dimen.quickstep_fling_threshold_speed);
58         mNavBarPosition = navBarPosition;
59         mDisableHorizontalSwipe = disableHorizontalSwipe;
60         mOnSwipeUp = onSwipeUp;
61 
62         init();
63     }
64 
65     /**
66      * Reset some initial values to prepare for the next gesture.
67      */
init()68     public void init() {
69         mInterceptedTouch = false;
70         mVelocityTracker = VelocityTracker.obtain();
71     }
72 
73     /**
74      * @return Whether we have passed the touch slop and are still tracking the gesture.
75      */
interceptedTouch()76     public boolean interceptedTouch() {
77         return mInterceptedTouch;
78     }
79 
80     /**
81      * Track motion events to determine whether an atomic swipe up has occurred.
82      */
onMotionEvent(MotionEvent ev)83     public void onMotionEvent(MotionEvent ev) {
84         if (mVelocityTracker == null) {
85             return;
86         }
87 
88         mVelocityTracker.addMovement(ev);
89         switch (ev.getActionMasked()) {
90             case ACTION_DOWN: {
91                 mDownPos.set(ev.getX(), ev.getY());
92                 break;
93             }
94             case ACTION_MOVE: {
95                 if (!mInterceptedTouch) {
96                     float displacementX = ev.getX() - mDownPos.x;
97                     float displacementY = ev.getY() - mDownPos.y;
98                     if (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop) {
99                         if (mDisableHorizontalSwipe
100                                 && Math.abs(displacementX) > Math.abs(displacementY)) {
101                             // Horizontal gesture is not allowed in this region
102                             mOnSwipeUp.onSwipeUpCancelled();
103                             endTouchTracking();
104                             break;
105                         }
106 
107                         mInterceptedTouch = true;
108                         mOnSwipeUp.onSwipeUpTouchIntercepted();
109                     }
110                 }
111                 break;
112             }
113 
114             case ACTION_CANCEL:
115                 mOnSwipeUp.onSwipeUpCancelled();
116                 endTouchTracking();
117                 break;
118 
119             case ACTION_UP: {
120                 onGestureEnd(ev);
121                 endTouchTracking();
122                 break;
123             }
124         }
125     }
126 
127     /** Finishes the tracking. All events after this call are ignored */
endTouchTracking()128     public void endTouchTracking() {
129         if (mVelocityTracker != null) {
130             mVelocityTracker.recycle();
131             mVelocityTracker = null;
132         }
133     }
134 
onGestureEnd(MotionEvent ev)135     private void onGestureEnd(MotionEvent ev) {
136         mVelocityTracker.computeCurrentVelocity(PX_PER_MS);
137         float velocityX = mVelocityTracker.getXVelocity();
138         float velocityY = mVelocityTracker.getYVelocity();
139         float velocity = mNavBarPosition.isRightEdge()
140                 ? -velocityX
141                 : mNavBarPosition.isLeftEdge()
142                         ? velocityX
143                         : -velocityY;
144 
145         final boolean wasFling = Math.abs(velocity) >= mMinFlingVelocity;
146         final boolean isSwipeUp;
147         if (wasFling) {
148             isSwipeUp = velocity > 0;
149         } else {
150             float displacementX = mDisableHorizontalSwipe ? 0 : (ev.getX() - mDownPos.x);
151             float displacementY = ev.getY() - mDownPos.y;
152             isSwipeUp = squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop;
153         }
154 
155         if (isSwipeUp) {
156             mOnSwipeUp.onSwipeUp(wasFling, new PointF(velocityX, velocityY));
157         } else {
158             mOnSwipeUp.onSwipeUpCancelled();
159         }
160     }
161 
162     /**
163      * Callback when the gesture ends and was determined to be a swipe from the nav bar.
164      */
165     public interface OnSwipeUpListener {
166         /**
167          * Called on touch up if a swipe up was detected.
168          * @param wasFling Whether the swipe was a fling, or just passed touch slop at low velocity.
169          * @param finalVelocity The final velocity of the swipe.
170          */
onSwipeUp(boolean wasFling, PointF finalVelocity)171         void onSwipeUp(boolean wasFling, PointF finalVelocity);
172 
173         /** Called on touch up if a swipe up was not detected. */
onSwipeUpCancelled()174         default void onSwipeUpCancelled() { }
175 
176         /** Called when the touch for swipe up is intercepted. */
onSwipeUpTouchIntercepted()177         default void onSwipeUpTouchIntercepted() { }
178     }
179 }
180