• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.launcher3.taskbar.bubbles;
17 
18 import android.annotation.SuppressLint;
19 import android.graphics.PointF;
20 import android.view.MotionEvent;
21 import android.view.VelocityTracker;
22 import android.view.View;
23 import android.view.ViewConfiguration;
24 
25 import androidx.annotation.NonNull;
26 import androidx.annotation.Nullable;
27 
28 import com.android.launcher3.taskbar.TaskbarActivityContext;
29 
30 /**
31  * Controls bubble bar drag to dismiss interaction.
32  * Interacts with {@link BubbleDismissController}, used by {@link BubbleBarViewController}.
33  * Supported interactions:
34  * - Drag a single bubble view into dismiss target to remove it.
35  * - Drag the bubble stack into dismiss target to remove all.
36  * Restores initial position of dragged view if released outside of the dismiss target.
37  */
38 public class BubbleDragController {
39     private final TaskbarActivityContext mActivity;
40     private BubbleBarViewController mBubbleBarViewController;
41     private BubbleDismissController mBubbleDismissController;
42 
BubbleDragController(TaskbarActivityContext activity)43     public BubbleDragController(TaskbarActivityContext activity) {
44         mActivity = activity;
45     }
46 
47     /**
48      * Initializes dependencies when bubble controllers are created.
49      * Should be careful to only access things that were created in constructors for now, as some
50      * controllers may still be waiting for init().
51      */
init(@onNull BubbleControllers bubbleControllers)52     public void init(@NonNull BubbleControllers bubbleControllers) {
53         mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
54         mBubbleDismissController = bubbleControllers.bubbleDismissController;
55     }
56 
57     /**
58      * Setup the bubble view for dragging and attach touch listener to it
59      */
60     @SuppressLint("ClickableViewAccessibility")
setupBubbleView(@onNull BubbleView bubbleView)61     public void setupBubbleView(@NonNull BubbleView bubbleView) {
62         if (!(bubbleView.getBubble() instanceof BubbleBarBubble)) {
63             // Don't setup dragging for overflow bubble view
64             return;
65         }
66 
67         bubbleView.setOnTouchListener(new BubbleTouchListener() {
68             @Override
69             void onDragStart() {
70                 mBubbleBarViewController.onDragStart(bubbleView);
71             }
72 
73             @Override
74             void onDragEnd() {
75                 mBubbleBarViewController.onDragEnd();
76             }
77 
78             @Override
79             protected void onDragRelease() {
80                 mBubbleBarViewController.onDragRelease(bubbleView);
81             }
82         });
83     }
84 
85     /**
86      * Setup the bubble bar view for dragging and attach touch listener to it
87      */
88     @SuppressLint("ClickableViewAccessibility")
setupBubbleBarView(@onNull BubbleBarView bubbleBarView)89     public void setupBubbleBarView(@NonNull BubbleBarView bubbleBarView) {
90         PointF initialRelativePivot = new PointF();
91         bubbleBarView.setOnTouchListener(new BubbleTouchListener() {
92             @Override
93             protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) {
94                 if (bubbleBarView.isExpanded()) return false;
95                 return super.onTouchDown(view, event);
96             }
97 
98             @Override
99             void onDragStart() {
100                 initialRelativePivot.set(bubbleBarView.getRelativePivotX(),
101                         bubbleBarView.getRelativePivotY());
102                 // By default the bubble bar view pivot is in bottom right corner, while dragging
103                 // it should be centered in order to align it with the dismiss target view
104                 bubbleBarView.setRelativePivot(/* x = */ 0.5f, /* y = */ 0.5f);
105             }
106 
107             @Override
108             void onDragEnd() {
109                 // Restoring the initial pivot for the bubble bar view
110                 bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y);
111             }
112         });
113     }
114 
115     /**
116      * Bubble touch listener for handling a single bubble view or bubble bar view while dragging.
117      * The dragging starts after "shorter" long click (the long click duration might change):
118      * - When the touch gesture moves out of the {@code ACTION_DOWN} location the dragging
119      * interaction is cancelled.
120      * - When {@code ACTION_UP} happens before long click is registered and there was no significant
121      * movement the view will perform click.
122      * - When the listener registers long click it starts dragging interaction, all the subsequent
123      * {@code ACTION_MOVE} events will drag the view, and the interaction finishes when
124      * {@code ACTION_UP} or {@code ACTION_CANCEL} are received.
125      * Lifecycle methods can be overridden do add extra setup/clean up steps.
126      */
127     private abstract class BubbleTouchListener implements View.OnTouchListener {
128         /**
129          * The internal state of the touch listener
130          */
131         private enum State {
132             // Idle and ready for the touch events.
133             // Changes to:
134             // - TOUCHED, when the {@code ACTION_DOWN} is handled
135             IDLE,
136 
137             // Touch down was handled and the lister is recognising the gestures.
138             // Changes to:
139             // - IDLE, when performs the click
140             // - DRAGGING, when registers the long click and starts dragging interaction
141             // - CANCELLED, when the touch events move out of the initial location before the long
142             // click is recognised
143 
144             TOUCHED,
145 
146             // The long click was registered and the view is being dragged.
147             // Changes to:
148             // - IDLE, when the gesture ends with the {@code ACTION_UP} or {@code ACTION_CANCEL}
149             DRAGGING,
150 
151             // The dragging was cancelled.
152             // Changes to:
153             // - IDLE, when the current gesture completes
154             CANCELLED
155         }
156 
157         private final PointF mTouchDownLocation = new PointF();
158         private final PointF mViewInitialPosition = new PointF();
159         private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
160         private final long mPressToDragTimeout = ViewConfiguration.getLongPressTimeout() / 2;
161         private State mState = State.IDLE;
162         private int mTouchSlop = -1;
163         private BubbleDragAnimator mAnimator;
164         @Nullable
165         private Runnable mLongClickRunnable;
166 
167         /**
168          * Called when the dragging interaction has started
169          */
onDragStart()170         abstract void onDragStart();
171 
172         /**
173          * Called when the dragging interaction has ended and all the animations have completed
174          */
onDragEnd()175         abstract void onDragEnd();
176 
177         /**
178          * Called when the dragged bubble is released outside of the dismiss target area and will
179          * move back to its initial position
180          */
onDragRelease()181         protected void onDragRelease() {
182         }
183 
184         /**
185          * Called when the dragged bubble is released inside of the dismiss target area and will get
186          * dismissed with animation
187          */
onDragDismiss()188         protected void onDragDismiss() {
189         }
190 
191         @Override
192         @SuppressLint("ClickableViewAccessibility")
onTouch(@onNull View view, @NonNull MotionEvent event)193         public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) {
194             updateVelocity(event);
195             switch (event.getActionMasked()) {
196                 case MotionEvent.ACTION_DOWN:
197                     return onTouchDown(view, event);
198                 case MotionEvent.ACTION_MOVE:
199                     onTouchMove(view, event);
200                     break;
201                 case MotionEvent.ACTION_UP:
202                     onTouchUp(view, event);
203                     break;
204                 case MotionEvent.ACTION_CANCEL:
205                     onTouchCancel(view, event);
206                     break;
207             }
208             return true;
209         }
210 
211         /**
212          * The touch down starts the interaction and schedules the long click handler.
213          *
214          * @param view  the view that received the event
215          * @param event the motion event
216          * @return true if the gesture should be intercepted and handled, false otherwise. Note if
217          * the false is returned subsequent events in the gesture won't get reported.
218          */
onTouchDown(@onNull View view, @NonNull MotionEvent event)219         protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) {
220             mState = State.TOUCHED;
221             mTouchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop();
222             mTouchDownLocation.set(event.getRawX(), event.getRawY());
223             mViewInitialPosition.set(view.getTranslationX(), view.getTranslationY());
224             setupLongClickHandler(view);
225             return true;
226         }
227 
228         /**
229          * The move event drags the view or cancels the interaction if hasn't long clicked yet.
230          *
231          * @param view  the view that received the event
232          * @param event the motion event
233          */
onTouchMove(@onNull View view, @NonNull MotionEvent event)234         protected void onTouchMove(@NonNull View view, @NonNull MotionEvent event) {
235             final float dx = event.getRawX() - mTouchDownLocation.x;
236             final float dy = event.getRawY() - mTouchDownLocation.y;
237             switch (mState) {
238                 case TOUCHED:
239                     final boolean movedOut = Math.hypot(dx, dy) > mTouchSlop;
240                     if (movedOut) {
241                         // Moved out of the initial location before the long click was registered
242                         mState = State.CANCELLED;
243                         cleanUpLongClickHandler(view);
244                     }
245                     break;
246                 case DRAGGING:
247                     drag(view, event, dx, dy);
248                     break;
249             }
250         }
251 
252         /**
253          * On touch up performs click or finishes the dragging depending on the state.
254          *
255          * @param view  the view that received the event
256          * @param event the motion event
257          */
onTouchUp(@onNull View view, @NonNull MotionEvent event)258         protected void onTouchUp(@NonNull View view, @NonNull MotionEvent event) {
259             switch (mState) {
260                 case TOUCHED:
261                     view.performClick();
262                     cleanUp(view);
263                     break;
264                 case DRAGGING:
265                     stopDragging(view, event);
266                     break;
267                 default:
268                     cleanUp(view);
269                     break;
270             }
271         }
272 
273         /**
274          * The gesture is cancelled and the interaction should clean up and complete.
275          *
276          * @param view  the view that received the event
277          * @param event the motion event
278          */
onTouchCancel(@onNull View view, @NonNull MotionEvent event)279         protected void onTouchCancel(@NonNull View view, @NonNull MotionEvent event) {
280             if (mState == State.DRAGGING) {
281                 stopDragging(view, event);
282             } else {
283                 cleanUp(view);
284             }
285         }
286 
startDragging(@onNull View view)287         private void startDragging(@NonNull View view) {
288             onDragStart();
289             mActivity.setTaskbarWindowFullscreen(true);
290             mAnimator = new BubbleDragAnimator(view);
291             mAnimator.animateFocused();
292             mBubbleDismissController.setupDismissView(view, mAnimator);
293             mBubbleDismissController.showDismissView();
294         }
295 
drag(@onNull View view, @NonNull MotionEvent event, float dx, float dy)296         private void drag(@NonNull View view, @NonNull MotionEvent event, float dx, float dy) {
297             if (mBubbleDismissController.handleTouchEvent(event)) return;
298             view.setTranslationX(mViewInitialPosition.x + dx);
299             view.setTranslationY(mViewInitialPosition.y + dy);
300         }
301 
stopDragging(@onNull View view, @NonNull MotionEvent event)302         private void stopDragging(@NonNull View view, @NonNull MotionEvent event) {
303             Runnable onComplete = () -> {
304                 mActivity.setTaskbarWindowFullscreen(false);
305                 cleanUp(view);
306                 onDragEnd();
307             };
308 
309             if (mBubbleDismissController.handleTouchEvent(event)) {
310                 onDragDismiss();
311                 mAnimator.animateDismiss(mViewInitialPosition, onComplete);
312             } else {
313                 onDragRelease();
314                 mAnimator.animateToInitialState(mViewInitialPosition, getCurrentVelocity(),
315                         onComplete);
316             }
317             mBubbleDismissController.hideDismissView();
318         }
319 
setupLongClickHandler(@onNull View view)320         private void setupLongClickHandler(@NonNull View view) {
321             cleanUpLongClickHandler(view);
322             mLongClickRunnable = () -> {
323                 // Register long click and start dragging interaction
324                 mState = State.DRAGGING;
325                 startDragging(view);
326             };
327             view.getHandler().postDelayed(mLongClickRunnable, mPressToDragTimeout);
328         }
329 
cleanUpLongClickHandler(@onNull View view)330         private void cleanUpLongClickHandler(@NonNull View view) {
331             if (mLongClickRunnable == null || view.getHandler() == null) return;
332             view.getHandler().removeCallbacks(mLongClickRunnable);
333             mLongClickRunnable = null;
334         }
335 
cleanUp(@onNull View view)336         private void cleanUp(@NonNull View view) {
337             cleanUpLongClickHandler(view);
338             mVelocityTracker.clear();
339             mState = State.IDLE;
340         }
341 
updateVelocity(MotionEvent event)342         private void updateVelocity(MotionEvent event) {
343             final float deltaX = event.getRawX() - event.getX();
344             final float deltaY = event.getRawY() - event.getY();
345             event.offsetLocation(deltaX, deltaY);
346             mVelocityTracker.addMovement(event);
347             event.offsetLocation(-deltaX, -deltaY);
348         }
349 
getCurrentVelocity()350         private PointF getCurrentVelocity() {
351             mVelocityTracker.computeCurrentVelocity(/* units = */ 1000);
352             return new PointF(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
353         }
354     }
355 }
356