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