• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 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 
17 package com.android.systemui.bubbles;
18 
19 import android.content.Context;
20 import android.graphics.PointF;
21 import android.os.Handler;
22 import android.view.MotionEvent;
23 import android.view.VelocityTracker;
24 import android.view.View;
25 import android.view.ViewConfiguration;
26 
27 import com.android.systemui.Dependency;
28 
29 /**
30  * Handles interpreting touches on a {@link BubbleStackView}. This includes expanding, collapsing,
31  * dismissing, and flings.
32  */
33 class BubbleTouchHandler implements View.OnTouchListener {
34     /** Velocity required to dismiss the stack without dragging it into the dismiss target. */
35     private static final float STACK_DISMISS_MIN_VELOCITY = 4000f;
36 
37     /**
38      * Velocity required to dismiss an individual bubble without dragging it into the dismiss
39      * target.
40      *
41      * This is higher than the stack dismiss velocity since unlike the stack, a downward fling could
42      * also be an attempted gesture to return the bubble to the row of expanded bubbles, which would
43      * usually be below the dragged bubble. By increasing the required velocity, it's less likely
44      * that the user is trying to drop it back into the row vs. fling it away.
45      */
46     private static final float INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY = 6000f;
47 
48     private static final String TAG = "BubbleTouchHandler";
49     /**
50      * When the stack is flung towards the bottom of the screen, it'll be dismissed if it's flung
51      * towards the center of the screen (where the dismiss target is). This value is the width of
52      * the target area to be considered 'towards the target'. For example 50% means that the stack
53      * needs to be flung towards the middle 50%, and the 25% on the left and right sides won't
54      * count.
55      */
56     private static final float DISMISS_FLING_TARGET_WIDTH_PERCENT = 0.5f;
57 
58     private final PointF mTouchDown = new PointF();
59     private final PointF mViewPositionOnTouchDown = new PointF();
60     private final BubbleStackView mStack;
61     private final BubbleData mBubbleData;
62 
63     private BubbleController mController = Dependency.get(BubbleController.class);
64 
65     private boolean mMovedEnough;
66     private int mTouchSlopSquared;
67     private VelocityTracker mVelocityTracker;
68 
69     private boolean mInDismissTarget;
70     private Handler mHandler = new Handler();
71 
72     /** View that was initially touched, when we received the first ACTION_DOWN event. */
73     private View mTouchedView;
74 
BubbleTouchHandler(BubbleStackView stackView, BubbleData bubbleData, Context context)75     BubbleTouchHandler(BubbleStackView stackView,
76             BubbleData bubbleData, Context context) {
77         final int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
78         mTouchSlopSquared = touchSlop * touchSlop;
79         mBubbleData = bubbleData;
80         mStack = stackView;
81     }
82 
83     @Override
onTouch(View v, MotionEvent event)84     public boolean onTouch(View v, MotionEvent event) {
85         final int action = event.getActionMasked();
86 
87         // If we aren't currently in the process of touching a view, figure out what we're touching.
88         // It'll be the stack, an individual bubble, or nothing.
89         if (mTouchedView == null) {
90             mTouchedView = mStack.getTargetView(event);
91         }
92 
93         // If this is an ACTION_OUTSIDE event, or the stack reported that we aren't touching
94         // anything, collapse the stack.
95         if (action == MotionEvent.ACTION_OUTSIDE || mTouchedView == null) {
96             mBubbleData.setExpanded(false);
97             resetForNextGesture();
98             return false;
99         }
100 
101         final boolean isStack = mStack.equals(mTouchedView);
102         final boolean isFlyout = mStack.getFlyoutView().equals(mTouchedView);
103         final float rawX = event.getRawX();
104         final float rawY = event.getRawY();
105 
106         // The coordinates of the touch event, in terms of the touched view's position.
107         final float viewX = mViewPositionOnTouchDown.x + rawX - mTouchDown.x;
108         final float viewY = mViewPositionOnTouchDown.y + rawY - mTouchDown.y;
109         switch (action) {
110             case MotionEvent.ACTION_DOWN:
111                 trackMovement(event);
112 
113                 mTouchDown.set(rawX, rawY);
114                 mStack.onGestureStart();
115 
116                 if (isStack) {
117                     mViewPositionOnTouchDown.set(mStack.getStackPosition());
118                     mStack.onDragStart();
119                 } else if (isFlyout) {
120                     mStack.onFlyoutDragStart();
121                 } else {
122                     mViewPositionOnTouchDown.set(
123                             mTouchedView.getTranslationX(), mTouchedView.getTranslationY());
124                     mStack.onBubbleDragStart(mTouchedView);
125                 }
126 
127                 break;
128             case MotionEvent.ACTION_MOVE:
129                 trackMovement(event);
130                 final float deltaX = rawX - mTouchDown.x;
131                 final float deltaY = rawY - mTouchDown.y;
132 
133                 if ((deltaX * deltaX) + (deltaY * deltaY) > mTouchSlopSquared && !mMovedEnough) {
134                     mMovedEnough = true;
135                 }
136 
137                 if (mMovedEnough) {
138                     if (isStack) {
139                         mStack.onDragged(viewX, viewY);
140                     } else if (isFlyout) {
141                         mStack.onFlyoutDragged(deltaX);
142                     } else {
143                         mStack.onBubbleDragged(mTouchedView, viewX, viewY);
144                     }
145                 }
146 
147                 final boolean currentlyInDismissTarget = mStack.isInDismissTarget(event);
148                 if (currentlyInDismissTarget != mInDismissTarget) {
149                     mInDismissTarget = currentlyInDismissTarget;
150 
151                     mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
152                     final float velX = mVelocityTracker.getXVelocity();
153                     final float velY = mVelocityTracker.getYVelocity();
154 
155                     // If the touch event is within the dismiss target, magnet the stack to it.
156                     if (!isFlyout) {
157                         mStack.animateMagnetToDismissTarget(
158                                 mTouchedView, mInDismissTarget, viewX, viewY, velX, velY);
159                     }
160                 }
161                 break;
162 
163             case MotionEvent.ACTION_CANCEL:
164                 resetForNextGesture();
165                 break;
166 
167             case MotionEvent.ACTION_UP:
168                 trackMovement(event);
169                 mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
170                 final float velX = mVelocityTracker.getXVelocity();
171                 final float velY = mVelocityTracker.getYVelocity();
172 
173                 final boolean shouldDismiss =
174                         isStack
175                                 ? mInDismissTarget
176                                     || isFastFlingTowardsDismissTarget(rawX, rawY, velX, velY)
177                                 : mInDismissTarget
178                                         || velY > INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY;
179 
180                 if (isFlyout && mMovedEnough) {
181                     mStack.onFlyoutDragFinished(rawX - mTouchDown.x /* deltaX */, velX);
182                 } else if (shouldDismiss) {
183                     final String individualBubbleKey =
184                             isStack ? null : ((BubbleView) mTouchedView).getKey();
185                     mStack.magnetToStackIfNeededThenAnimateDismissal(mTouchedView, velX, velY,
186                             () -> {
187                                 if (isStack) {
188                                     mController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
189                                 } else {
190                                     mController.removeBubble(
191                                             individualBubbleKey,
192                                             BubbleController.DISMISS_USER_GESTURE);
193                                 }
194                             });
195                 } else if (isFlyout) {
196                     // TODO(b/129768381): Expand if tapped, dismiss if swiped away.
197                     if (!mBubbleData.isExpanded() && !mMovedEnough) {
198                         mBubbleData.setExpanded(true);
199                     }
200                 } else if (mMovedEnough) {
201                     if (isStack) {
202                         mStack.onDragFinish(viewX, viewY, velX, velY);
203                     } else {
204                         mStack.onBubbleDragFinish(mTouchedView, viewX, viewY, velX, velY);
205                     }
206                 } else if (mTouchedView == mStack.getExpandedBubbleView()) {
207                     mBubbleData.setExpanded(false);
208                 } else if (isStack || isFlyout) {
209                     // Toggle expansion
210                     mBubbleData.setExpanded(!mBubbleData.isExpanded());
211                 } else {
212                     final String key = ((BubbleView) mTouchedView).getKey();
213                     mBubbleData.setSelectedBubble(mBubbleData.getBubbleWithKey(key));
214                 }
215 
216                 resetForNextGesture();
217                 break;
218         }
219 
220         return true;
221     }
222 
223     /**
224      * Whether the given touch data represents a powerful fling towards the bottom-center of the
225      * screen (the dismiss target).
226      */
isFastFlingTowardsDismissTarget( float rawX, float rawY, float velX, float velY)227     private boolean isFastFlingTowardsDismissTarget(
228             float rawX, float rawY, float velX, float velY) {
229         // Not a fling downward towards the target if velocity is zero or negative.
230         if (velY <= 0) {
231             return false;
232         }
233 
234         float bottomOfScreenInterceptX = rawX;
235 
236         // Only do math if the X velocity is non-zero, otherwise X won't change.
237         if (velX != 0) {
238             // Rise over run...
239             final float slope = velY / velX;
240             // ...y = mx + b, b = y / mx...
241             final float yIntercept = rawY - slope * rawX;
242             // ...calculate the x value when y = bottom of the screen.
243             bottomOfScreenInterceptX = (mStack.getHeight() - yIntercept) / slope;
244         }
245 
246         final float dismissTargetWidth =
247                 mStack.getWidth() * DISMISS_FLING_TARGET_WIDTH_PERCENT;
248         return velY > STACK_DISMISS_MIN_VELOCITY
249                 && bottomOfScreenInterceptX > dismissTargetWidth / 2f
250                 && bottomOfScreenInterceptX < mStack.getWidth() - dismissTargetWidth / 2f;
251     }
252 
253     /** Clears all touch-related state. */
resetForNextGesture()254     private void resetForNextGesture() {
255         if (mVelocityTracker != null) {
256             mVelocityTracker.recycle();
257             mVelocityTracker = null;
258         }
259 
260         mTouchedView = null;
261         mMovedEnough = false;
262         mInDismissTarget = false;
263 
264         mStack.onGestureFinished();
265     }
266 
trackMovement(MotionEvent event)267     private void trackMovement(MotionEvent event) {
268         if (mVelocityTracker == null) {
269             mVelocityTracker = VelocityTracker.obtain();
270         }
271         mVelocityTracker.addMovement(event);
272     }
273 }
274