blob: 4240e06a8800ec1ed1a1a38d27fd7f387ad7cecb [file] [log] [blame]
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001/*
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
17package com.android.systemui.bubbles;
18
19import android.content.Context;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080020import android.graphics.PointF;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080021import android.view.MotionEvent;
22import android.view.VelocityTracker;
23import android.view.View;
24import android.view.ViewConfiguration;
25
26import com.android.systemui.Dependency;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080027
28/**
29 * Handles interpreting touches on a {@link BubbleStackView}. This includes expanding, collapsing,
30 * dismissing, and flings.
31 */
Mark Renouf89b1a4a2018-12-04 14:59:45 -050032class BubbleTouchHandler implements View.OnTouchListener {
Joshua Tsuji4accf5982019-04-22 17:36:11 -040033 /** Velocity required to dismiss the stack without dragging it into the dismiss target. */
34 private static final float STACK_DISMISS_MIN_VELOCITY = 4000f;
35
36 /**
37 * Velocity required to dismiss an individual bubble without dragging it into the dismiss
38 * target.
39 *
40 * This is higher than the stack dismiss velocity since unlike the stack, a downward fling could
41 * also be an attempted gesture to return the bubble to the row of expanded bubbles, which would
42 * usually be below the dragged bubble. By increasing the required velocity, it's less likely
43 * that the user is trying to drop it back into the row vs. fling it away.
44 */
45 private static final float INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY = 6000f;
Joshua Tsuji442b6272019-02-08 13:23:43 -050046
Joshua Tsuji4accf5982019-04-22 17:36:11 -040047 /**
48 * When the stack is flung towards the bottom of the screen, it'll be dismissed if it's flung
49 * towards the center of the screen (where the dismiss target is). This value is the width of
50 * the target area to be considered 'towards the target'. For example 50% means that the stack
51 * needs to be flung towards the middle 50%, and the 25% on the left and right sides won't
52 * count.
53 */
54 private static final float DISMISS_FLING_TARGET_WIDTH_PERCENT = 0.5f;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040055
Joshua Tsuji442b6272019-02-08 13:23:43 -050056 private final PointF mTouchDown = new PointF();
57 private final PointF mViewPositionOnTouchDown = new PointF();
58 private final BubbleStackView mStack;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040059 private final BubbleData mBubbleData;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080060
61 private BubbleController mController = Dependency.get(BubbleController.class);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080062
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080063 private boolean mMovedEnough;
64 private int mTouchSlopSquared;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080065 private VelocityTracker mVelocityTracker;
66
Joshua Tsuji442b6272019-02-08 13:23:43 -050067 /** View that was initially touched, when we received the first ACTION_DOWN event. */
68 private View mTouchedView;
Mady Mellore9371bc2019-07-10 18:50:59 -070069 /** Whether the current touched view is in the dismiss target. */
70 private boolean mInDismissTarget;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080071
Mark Renouf9ba6cea2019-04-17 11:53:50 -040072 BubbleTouchHandler(BubbleStackView stackView,
73 BubbleData bubbleData, Context context) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080074 final int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
75 mTouchSlopSquared = touchSlop * touchSlop;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040076 mBubbleData = bubbleData;
Joshua Tsuji442b6272019-02-08 13:23:43 -050077 mStack = stackView;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080078 }
79
80 @Override
81 public boolean onTouch(View v, MotionEvent event) {
Joshua Tsuji442b6272019-02-08 13:23:43 -050082 final int action = event.getActionMasked();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080083
Joshua Tsuji442b6272019-02-08 13:23:43 -050084 // If we aren't currently in the process of touching a view, figure out what we're touching.
85 // It'll be the stack, an individual bubble, or nothing.
86 if (mTouchedView == null) {
87 mTouchedView = mStack.getTargetView(event);
88 }
89
90 // If this is an ACTION_OUTSIDE event, or the stack reported that we aren't touching
91 // anything, collapse the stack.
92 if (action == MotionEvent.ACTION_OUTSIDE || mTouchedView == null) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -040093 mBubbleData.setExpanded(false);
Mark Renoufa867b7e2019-02-28 15:14:47 -050094 resetForNextGesture();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080095 return false;
96 }
97
Mady Mellor47b11e32019-07-11 19:06:21 -070098 if (!(mTouchedView instanceof BubbleView)
99 && !(mTouchedView instanceof BubbleStackView)
100 && !(mTouchedView instanceof BubbleFlyoutView)) {
101 // Not touching anything touchable, but we shouldn't collapse (e.g. touching edge
102 // of expanded view).
103 resetForNextGesture();
104 return false;
105 }
106
Joshua Tsuji442b6272019-02-08 13:23:43 -0500107 final boolean isStack = mStack.equals(mTouchedView);
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400108 final boolean isFlyout = mStack.getFlyoutView().equals(mTouchedView);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500109 final float rawX = event.getRawX();
110 final float rawY = event.getRawY();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800111
Joshua Tsuji442b6272019-02-08 13:23:43 -0500112 // The coordinates of the touch event, in terms of the touched view's position.
113 final float viewX = mViewPositionOnTouchDown.x + rawX - mTouchDown.x;
114 final float viewY = mViewPositionOnTouchDown.y + rawY - mTouchDown.y;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800115 switch (action) {
116 case MotionEvent.ACTION_DOWN:
117 trackMovement(event);
118
Joshua Tsuji442b6272019-02-08 13:23:43 -0500119 mTouchDown.set(rawX, rawY);
Joshua Tsuji6549e702019-05-02 13:13:16 -0400120 mStack.onGestureStart();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800121
Joshua Tsuji442b6272019-02-08 13:23:43 -0500122 if (isStack) {
123 mViewPositionOnTouchDown.set(mStack.getStackPosition());
124 mStack.onDragStart();
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400125 } else if (isFlyout) {
Joshua Tsuji6549e702019-05-02 13:13:16 -0400126 mStack.onFlyoutDragStart();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800127 } else {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500128 mViewPositionOnTouchDown.set(
129 mTouchedView.getTranslationX(), mTouchedView.getTranslationY());
130 mStack.onBubbleDragStart(mTouchedView);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800131 }
132
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800133 break;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800134 case MotionEvent.ACTION_MOVE:
135 trackMovement(event);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500136 final float deltaX = rawX - mTouchDown.x;
137 final float deltaY = rawY - mTouchDown.y;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800138
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800139 if ((deltaX * deltaX) + (deltaY * deltaY) > mTouchSlopSquared && !mMovedEnough) {
140 mMovedEnough = true;
141 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800142
143 if (mMovedEnough) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500144 if (isStack) {
145 mStack.onDragged(viewX, viewY);
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400146 } else if (isFlyout) {
Joshua Tsuji6549e702019-05-02 13:13:16 -0400147 mStack.onFlyoutDragged(deltaX);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800148 } else {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500149 mStack.onBubbleDragged(mTouchedView, viewX, viewY);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800150 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800151 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500152
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400153 final boolean currentlyInDismissTarget = mStack.isInDismissTarget(event);
154 if (currentlyInDismissTarget != mInDismissTarget) {
155 mInDismissTarget = currentlyInDismissTarget;
156
157 mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
158 final float velX = mVelocityTracker.getXVelocity();
159 final float velY = mVelocityTracker.getYVelocity();
160
161 // If the touch event is within the dismiss target, magnet the stack to it.
Joshua Tsuji6549e702019-05-02 13:13:16 -0400162 if (!isFlyout) {
163 mStack.animateMagnetToDismissTarget(
164 mTouchedView, mInDismissTarget, viewX, viewY, velX, velY);
165 }
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400166 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800167 break;
168
169 case MotionEvent.ACTION_CANCEL:
Mark Renoufa867b7e2019-02-28 15:14:47 -0500170 resetForNextGesture();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800171 break;
172
173 case MotionEvent.ACTION_UP:
174 trackMovement(event);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400175 mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
176 final float velX = mVelocityTracker.getXVelocity();
177 final float velY = mVelocityTracker.getYVelocity();
178
179 final boolean shouldDismiss =
180 isStack
181 ? mInDismissTarget
182 || isFastFlingTowardsDismissTarget(rawX, rawY, velX, velY)
183 : mInDismissTarget
184 || velY > INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY;
185
Joshua Tsuji6549e702019-05-02 13:13:16 -0400186 if (isFlyout && mMovedEnough) {
187 mStack.onFlyoutDragFinished(rawX - mTouchDown.x /* deltaX */, velX);
188 } else if (shouldDismiss) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400189 final String individualBubbleKey =
190 isStack ? null : ((BubbleView) mTouchedView).getKey();
191 mStack.magnetToStackIfNeededThenAnimateDismissal(mTouchedView, velX, velY,
192 () -> {
193 if (isStack) {
194 mController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
195 } else {
196 mController.removeBubble(
197 individualBubbleKey,
198 BubbleController.DISMISS_USER_GESTURE);
199 }
200 });
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400201 } else if (isFlyout) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400202 if (!mBubbleData.isExpanded() && !mMovedEnough) {
Joshua Tsuji14e68552019-06-06 17:17:08 -0400203 mStack.onFlyoutTapped();
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400204 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800205 } else if (mMovedEnough) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500206 if (isStack) {
207 mStack.onDragFinish(viewX, viewY, velX, velY);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800208 } else {
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400209 mStack.onBubbleDragFinish(mTouchedView, viewX, viewY, velX, velY);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800210 }
Mark Renoufa867b7e2019-02-28 15:14:47 -0500211 } else if (mTouchedView == mStack.getExpandedBubbleView()) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400212 mBubbleData.setExpanded(false);
Joshua Tsuji6549e702019-05-02 13:13:16 -0400213 } else if (isStack || isFlyout) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400214 // Toggle expansion
215 mBubbleData.setExpanded(!mBubbleData.isExpanded());
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800216 } else {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400217 final String key = ((BubbleView) mTouchedView).getKey();
218 mBubbleData.setSelectedBubble(mBubbleData.getBubbleWithKey(key));
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800219 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500220
Mark Renoufa867b7e2019-02-28 15:14:47 -0500221 resetForNextGesture();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800222 break;
223 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500224
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800225 return true;
226 }
227
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400228 /**
229 * Whether the given touch data represents a powerful fling towards the bottom-center of the
230 * screen (the dismiss target).
231 */
232 private boolean isFastFlingTowardsDismissTarget(
233 float rawX, float rawY, float velX, float velY) {
234 // Not a fling downward towards the target if velocity is zero or negative.
235 if (velY <= 0) {
236 return false;
237 }
238
239 float bottomOfScreenInterceptX = rawX;
240
241 // Only do math if the X velocity is non-zero, otherwise X won't change.
242 if (velX != 0) {
243 // Rise over run...
244 final float slope = velY / velX;
245 // ...y = mx + b, b = y / mx...
246 final float yIntercept = rawY - slope * rawX;
247 // ...calculate the x value when y = bottom of the screen.
248 bottomOfScreenInterceptX = (mStack.getHeight() - yIntercept) / slope;
249 }
250
251 final float dismissTargetWidth =
252 mStack.getWidth() * DISMISS_FLING_TARGET_WIDTH_PERCENT;
253 return velY > STACK_DISMISS_MIN_VELOCITY
254 && bottomOfScreenInterceptX > dismissTargetWidth / 2f
255 && bottomOfScreenInterceptX < mStack.getWidth() - dismissTargetWidth / 2f;
256 }
257
Mark Renoufa867b7e2019-02-28 15:14:47 -0500258 /** Clears all touch-related state. */
259 private void resetForNextGesture() {
Mark Renoufa867b7e2019-02-28 15:14:47 -0500260 if (mVelocityTracker != null) {
261 mVelocityTracker.recycle();
262 mVelocityTracker = null;
263 }
Joshua Tsuji6549e702019-05-02 13:13:16 -0400264
Mark Renoufa867b7e2019-02-28 15:14:47 -0500265 mTouchedView = null;
266 mMovedEnough = false;
267 mInDismissTarget = false;
Joshua Tsuji6549e702019-05-02 13:13:16 -0400268
269 mStack.onGestureFinished();
Mark Renoufa867b7e2019-02-28 15:14:47 -0500270 }
271
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800272 private void trackMovement(MotionEvent event) {
273 if (mVelocityTracker == null) {
274 mVelocityTracker = VelocityTracker.obtain();
275 }
276 mVelocityTracker.addMovement(event);
277 }
278}