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 static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
19 
20 import android.util.Log;
21 import android.view.MotionEvent;
22 import android.view.View;
23 import android.widget.FrameLayout;
24 
25 import androidx.annotation.NonNull;
26 import androidx.annotation.Nullable;
27 import androidx.dynamicanimation.animation.DynamicAnimation;
28 
29 import com.android.launcher3.R;
30 import com.android.launcher3.taskbar.TaskbarActivityContext;
31 import com.android.launcher3.taskbar.TaskbarDragLayer;
32 import com.android.wm.shell.common.bubbles.DismissView;
33 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
34 
35 /**
36  * Controls dismiss view presentation for the bubble bar dismiss functionality.
37  * Provides the dragged view snapping to the target dismiss area and animates it.
38  * When the dragged bubble/bubble stack is released inside of the target area, it gets dismissed.
39  *
40  * @see BubbleDragController
41  */
42 public class BubbleDismissController {
43     private static final String TAG = "BubbleDismissController";
44     private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f;
45     private final TaskbarActivityContext mActivity;
46     private final TaskbarDragLayer mDragLayer;
47     @Nullable
48     private BubbleBarViewController mBubbleBarViewController;
49 
50     // Dismiss view that's attached to drag layer. It consists of the scrim view and the circular
51     // dismiss view used as a dismiss target.
52     @Nullable
53     private DismissView mDismissView;
54 
55     // The currently magnetized object, which is being dragged and will be attracted to the magnetic
56     // dismiss target. This is either the stack itself, or an individual bubble.
57     @Nullable
58     private MagnetizedObject<View> mMagnetizedObject;
59 
60     // The MagneticTarget instance for our circular dismiss view. This is added to the
61     // MagnetizedObject instances for the stack and any dragged-out bubbles.
62     @Nullable
63     private MagnetizedObject.MagneticTarget mMagneticTarget;
64 
65     // The bubble drag animator that synchronizes bubble drag and dismiss view animations
66     // A new instance is provided when the dismiss view is setup
67     @Nullable
68     private BubbleDragAnimator mAnimator;
69 
70     @Nullable
71     private Listener mListener;
72 
BubbleDismissController(TaskbarActivityContext activity, TaskbarDragLayer dragLayer)73     public BubbleDismissController(TaskbarActivityContext activity, TaskbarDragLayer dragLayer) {
74         mActivity = activity;
75         mDragLayer = dragLayer;
76     }
77 
78     /**
79      * Initializes dependencies when bubble controllers are created.
80      * Should be careful to only access things that were created in constructors for now, as some
81      * controllers may still be waiting for init().
82      */
init(@onNull BubbleControllers bubbleControllers)83     public void init(@NonNull BubbleControllers bubbleControllers) {
84         mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
85     }
86 
87     /**
88      * Set listener to be notified of dismiss events
89      */
setListener(@ullable Listener listener)90     public void setListener(@Nullable Listener listener) {
91         mListener = listener;
92     }
93 
94     /**
95      * Setup the dismiss view and magnetized object that will be attracted to magnetic target.
96      * Should be called before handling events or showing/hiding dismiss view.
97      *
98      * @param magnetizedView the view to be pulled into target dismiss area
99      * @param animator       the bubble animator to be used for the magnetized view, it syncs bubble
100      *                       dragging and dismiss animations with the dismiss view provided.
101      */
setupDismissView(@onNull View magnetizedView, @NonNull BubbleDragAnimator animator)102     public void setupDismissView(@NonNull View magnetizedView,
103             @NonNull BubbleDragAnimator animator) {
104         setupDismissView();
105         setupMagnetizedObject(magnetizedView);
106         if (mDismissView != null) {
107             animator.setDismissView(mDismissView);
108             mAnimator = animator;
109         }
110     }
111 
112     /**
113      * Handle the touch event and pass it to the magnetized object.
114      * It should be called after {@code setupDismissView}
115      */
handleTouchEvent(@onNull MotionEvent event)116     public boolean handleTouchEvent(@NonNull MotionEvent event) {
117         return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
118     }
119 
120     /**
121      * Show dismiss view with animation
122      * It should be called after {@code setupDismissView}
123      */
showDismissView()124     public void showDismissView() {
125         if (mDismissView == null) return;
126         mDismissView.show();
127     }
128 
129     /**
130      * Hide dismiss view with animation
131      * It should be called after {@code setupDismissView}
132      */
hideDismissView()133     public void hideDismissView() {
134         if (mDismissView == null) return;
135         mDismissView.hide();
136     }
137 
138     /**
139      * Dismiss magnetized object when it's released in the dismiss target area
140      */
dismissMagnetizedObject()141     private void dismissMagnetizedObject() {
142         if (mMagnetizedObject == null || mBubbleBarViewController == null) return;
143         if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) {
144             BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject();
145             if (bubbleView.getBubble() != null) {
146                 mBubbleBarViewController.onDismissBubbleWhileDragging(bubbleView.getBubble());
147             }
148         } else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) {
149             mBubbleBarViewController.onDismissAllBubblesWhileDragging();
150         }
151     }
152 
setupDismissView()153     private void setupDismissView() {
154         if (mDismissView != null) return;
155         mDismissView = new DismissView(mActivity.getApplicationContext());
156         BubbleDismissViewUtils.setup(mDismissView);
157         mDragLayer.addView(mDismissView, /* index = */ 0,
158                 new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
159         mDismissView.setElevation(mDismissView.getResources().getDimensionPixelSize(
160                 R.dimen.bubblebar_elevation));
161         setupMagneticTarget(mDismissView.getCircle());
162     }
163 
setupMagneticTarget(@onNull View view)164     private void setupMagneticTarget(@NonNull View view) {
165         int magneticFieldRadius = mActivity.getResources().getDimensionPixelSize(
166                 R.dimen.bubblebar_dismiss_target_size);
167         mMagneticTarget = new MagnetizedObject.MagneticTarget(view, magneticFieldRadius);
168     }
169 
setupMagnetizedObject(@onNull View magnetizedView)170     private void setupMagnetizedObject(@NonNull View magnetizedView) {
171         mMagnetizedObject = new MagnetizedObject<>(mActivity.getApplicationContext(),
172                 magnetizedView, BubbleDragController.DRAG_TRANSLATION_X,
173                 DynamicAnimation.TRANSLATION_Y) {
174             @Override
175             public float getWidth(@NonNull View underlyingObject) {
176                 return underlyingObject.getWidth() * underlyingObject.getScaleX();
177             }
178 
179             @Override
180             public float getHeight(@NonNull View underlyingObject) {
181                 return underlyingObject.getHeight() * underlyingObject.getScaleY();
182             }
183 
184             @Override
185             public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) {
186                 underlyingObject.getLocationOnScreen(loc);
187             }
188         };
189 
190         mMagnetizedObject.setHapticsEnabled(true);
191         mMagnetizedObject.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
192         if (mMagneticTarget != null) {
193             mMagnetizedObject.addTarget(mMagneticTarget);
194         } else {
195             Log.e(TAG,"Requires MagneticTarget to add target to MagnetizedObject!");
196         }
197         mMagnetizedObject.setMagnetListener(new MagnetizedObject.MagnetListener() {
198             @Override
199             public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target,
200                     @NonNull MagnetizedObject<?> draggedObject) {
201                 if (mAnimator == null) return;
202                 mAnimator.animateDismissCaptured();
203                 if (mListener != null) {
204                     mListener.onStuckToDismissChanged(true /* stuck */);
205                 }
206             }
207 
208             @Override
209             public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
210                     @NonNull MagnetizedObject<?> draggedObject,
211                     float velX, float velY, boolean wasFlungOut) {
212                 if (mAnimator == null) return;
213                 mAnimator.animateDismissReleased();
214                 if (mListener != null) {
215                     mListener.onStuckToDismissChanged(false /* stuck */);
216                 }
217             }
218 
219             @Override
220             public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target,
221                     @NonNull MagnetizedObject<?> draggedObject) {
222                 dismissMagnetizedObject();
223             }
224         });
225     }
226 
227     /** Interface to receive updates about the dismiss state */
228     public interface Listener {
229         /** Called when view is stuck or unstuck from dismiss target */
onStuckToDismissChanged(boolean stuck)230         void onStuckToDismissChanged(boolean stuck);
231     }
232 }
233