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 java.lang.Math.abs;
19 
20 import android.animation.Animator;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.AnimatorSet;
23 import android.annotation.Nullable;
24 import android.view.InsetsController;
25 import android.view.MotionEvent;
26 import android.view.View;
27 
28 import com.android.launcher3.R;
29 import com.android.launcher3.anim.AnimatedFloat;
30 import com.android.launcher3.taskbar.StashedHandleViewController;
31 import com.android.launcher3.taskbar.TaskbarActivityContext;
32 import com.android.launcher3.taskbar.TaskbarControllers;
33 import com.android.launcher3.taskbar.TaskbarInsetsController;
34 import com.android.launcher3.taskbar.TaskbarStashController;
35 import com.android.launcher3.util.MultiPropertyFactory;
36 import com.android.wm.shell.common.bubbles.BubbleBarLocation;
37 import com.android.wm.shell.shared.animation.PhysicsAnimator;
38 
39 /**
40  * Coordinates between controllers such as BubbleBarView and BubbleHandleViewController to
41  * create a cohesive animation between stashed/unstashed states.
42  */
43 public class BubbleStashController {
44 
45     private static final String TAG = "BubbleStashController";
46 
47     /**
48      * How long to stash/unstash.
49      */
50     public static final long BAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE;
51 
52     /**
53      * The scale bubble bar animates to when being stashed.
54      */
55     private static final float STASHED_BAR_SCALE = 0.5f;
56 
57     protected final TaskbarActivityContext mActivity;
58 
59     // Initialized in init.
60     private TaskbarControllers mControllers;
61     private TaskbarInsetsController mTaskbarInsetsController;
62     private BubbleBarViewController mBarViewController;
63     private BubbleStashedHandleViewController mHandleViewController;
64     private TaskbarStashController mTaskbarStashController;
65 
66     private MultiPropertyFactory.MultiProperty mIconAlphaForStash;
67     private AnimatedFloat mIconScaleForStash;
68     private AnimatedFloat mIconTranslationYForStash;
69     private MultiPropertyFactory.MultiProperty mBubbleStashedHandleAlpha;
70 
71     private boolean mRequestedStashState;
72     private boolean mRequestedExpandedState;
73 
74     private boolean mIsStashed;
75     private int mStashedHeight;
76     private int mUnstashedHeight;
77     private boolean mBubblesShowingOnHome;
78     private boolean mBubblesShowingOnOverview;
79     private boolean mIsSysuiLocked;
80 
81     private final float mHandleCenterFromScreenBottom;
82 
83     @Nullable
84     private AnimatorSet mAnimator;
85 
BubbleStashController(TaskbarActivityContext activity)86     public BubbleStashController(TaskbarActivityContext activity) {
87         mActivity = activity;
88         // the handle is centered within the stashed taskbar area
89         mHandleCenterFromScreenBottom =
90                 mActivity.getResources().getDimensionPixelSize(R.dimen.bubblebar_stashed_size) / 2f;
91     }
92 
init(TaskbarControllers controllers, BubbleControllers bubbleControllers)93     public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
94         mControllers = controllers;
95         mTaskbarInsetsController = controllers.taskbarInsetsController;
96         mBarViewController = bubbleControllers.bubbleBarViewController;
97         mHandleViewController = bubbleControllers.bubbleStashedHandleViewController;
98         mTaskbarStashController = controllers.taskbarStashController;
99 
100         mIconAlphaForStash = mBarViewController.getBubbleBarAlpha().get(0);
101         mIconScaleForStash = mBarViewController.getBubbleBarScale();
102         mIconTranslationYForStash = mBarViewController.getBubbleBarTranslationY();
103 
104         mBubbleStashedHandleAlpha = mHandleViewController.getStashedHandleAlpha().get(
105                 StashedHandleViewController.ALPHA_INDEX_STASHED);
106 
107         mStashedHeight = mHandleViewController.getStashedHeight();
108         mUnstashedHeight = mHandleViewController.getUnstashedHeight();
109     }
110 
111     /**
112      * Returns the touchable height of the bubble bar based on it's stashed state.
113      */
getTouchableHeight()114     public int getTouchableHeight() {
115         return mIsStashed ? mStashedHeight : mUnstashedHeight;
116     }
117 
118     /**
119      * Returns whether the bubble bar is currently stashed.
120      */
isStashed()121     public boolean isStashed() {
122         return mIsStashed;
123     }
124 
125     /**
126      * Animates the handle (or bubble bar depending on state) to be visible after the device is
127      * unlocked.
128      *
129      * <p>Normally either the bubble bar or the handle is visible,
130      * and {@link #showBubbleBar(boolean)} and {@link #stashBubbleBar()} are used to transition
131      * between these two states. But the transition from the state where both the bar and handle
132      * are invisible is slightly different.
133      */
animateAfterUnlock()134     private void animateAfterUnlock() {
135         AnimatorSet animatorSet = new AnimatorSet();
136         if (mBubblesShowingOnHome || mBubblesShowingOnOverview) {
137             mIsStashed = false;
138             animatorSet.playTogether(mIconScaleForStash.animateToValue(1),
139                     mIconTranslationYForStash.animateToValue(getBubbleBarTranslationY()),
140                     mIconAlphaForStash.animateToValue(1));
141         } else {
142             mIsStashed = true;
143             animatorSet.playTogether(mBubbleStashedHandleAlpha.animateToValue(1));
144         }
145 
146         animatorSet.addListener(new AnimatorListenerAdapter() {
147             @Override
148             public void onAnimationEnd(Animator animation) {
149                 onIsStashedChanged();
150             }
151         });
152         animatorSet.setDuration(BAR_STASH_DURATION).start();
153     }
154 
155     /**
156      * Called when launcher enters or exits the home page. Bubbles are unstashed on home.
157      */
setBubblesShowingOnHome(boolean onHome)158     public void setBubblesShowingOnHome(boolean onHome) {
159         if (mBubblesShowingOnHome != onHome) {
160             mBubblesShowingOnHome = onHome;
161 
162             if (!mBarViewController.hasBubbles()) {
163                 // if there are no bubbles, there's nothing to show, so just return.
164                 return;
165             }
166 
167             if (mBubblesShowingOnHome) {
168                 showBubbleBar(/* expanded= */ false);
169                 // When transitioning from app to home the stash animator may already have been
170                 // created, so we need to animate the bubble bar here to align with hotseat.
171                 if (!mIsStashed) {
172                     mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForHotseat())
173                             .start();
174                 }
175                 // If the bubble bar is already unstashed, the taskbar touchable region won't be
176                 // updated correctly, so force an update here.
177                 mControllers.runAfterInit(() ->
178                         mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged());
179             } else if (!mBarViewController.isExpanded()) {
180                 stashBubbleBar();
181             }
182         }
183     }
184 
185     /** Whether bubbles are showing on the launcher home page. */
isBubblesShowingOnHome()186     public boolean isBubblesShowingOnHome() {
187         return mBubblesShowingOnHome;
188     }
189 
190     // TODO: when tapping on an app in overview, this is a bit delayed compared to taskbar stashing
191     /** Called when launcher enters or exits overview. Bubbles are unstashed on overview. */
setBubblesShowingOnOverview(boolean onOverview)192     public void setBubblesShowingOnOverview(boolean onOverview) {
193         if (mBubblesShowingOnOverview != onOverview) {
194             mBubblesShowingOnOverview = onOverview;
195             if (!mBubblesShowingOnOverview && !mBarViewController.isExpanded()) {
196                 stashBubbleBar();
197             } else {
198                 // When transitioning to overview the stash animator may already have been
199                 // created, so we need to animate the bubble bar here to align with taskbar.
200                 mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForTaskbar())
201                         .start();
202             }
203         }
204     }
205 
206     /** Whether bubbles are showing on Overview. */
isBubblesShowingOnOverview()207     public boolean isBubblesShowingOnOverview() {
208         return mBubblesShowingOnOverview;
209     }
210 
211     /** Called when sysui locked state changes, when locked, bubble bar is stashed. */
onSysuiLockedStateChange(boolean isSysuiLocked)212     public void onSysuiLockedStateChange(boolean isSysuiLocked) {
213         if (isSysuiLocked != mIsSysuiLocked) {
214             mIsSysuiLocked = isSysuiLocked;
215             if (!mIsSysuiLocked && mBarViewController.hasBubbles()) {
216                 animateAfterUnlock();
217             }
218         }
219     }
220 
221     /**
222      * Stashes the bubble bar if allowed based on other state (e.g. on home and overview the
223      * bar does not stash).
224      */
stashBubbleBar()225     public void stashBubbleBar() {
226         mRequestedStashState = true;
227         mRequestedExpandedState = false;
228         updateStashedAndExpandedState();
229     }
230 
231     /**
232      * Shows the bubble bar, and expands bubbles depending on {@param expandBubbles}.
233      */
showBubbleBar(boolean expandBubbles)234     public void showBubbleBar(boolean expandBubbles) {
235         mRequestedStashState = false;
236         mRequestedExpandedState = expandBubbles;
237         updateStashedAndExpandedState();
238     }
239 
updateStashedAndExpandedState()240     private void updateStashedAndExpandedState() {
241         if (mBarViewController.isHiddenForNoBubbles()) {
242             // If there are no bubbles the bar and handle are invisible, nothing to do here.
243             return;
244         }
245         boolean isStashed = mRequestedStashState
246                 && !mBubblesShowingOnHome
247                 && !mBubblesShowingOnOverview;
248         if (mIsStashed != isStashed) {
249             // notify the view controller that the stash state is about to change so that it can
250             // cancel an ongoing animation if there is one.
251             // note that this has to be called before updating mIsStashed with the new value,
252             // otherwise interrupting an ongoing animation may update it again with the wrong state
253             mBarViewController.onStashStateChanging();
254             mIsStashed = isStashed;
255             if (mAnimator != null) {
256                 mAnimator.cancel();
257             }
258             mAnimator = createStashAnimator(mIsStashed, BAR_STASH_DURATION);
259             mAnimator.start();
260             onIsStashedChanged();
261         }
262         if (mBarViewController.isExpanded() != mRequestedExpandedState) {
263             mBarViewController.setExpanded(mRequestedExpandedState);
264         }
265     }
266 
267     /**
268      * Create a stash animation.
269      *
270      * @param isStashed whether it's a stash animation or an unstash animation
271      * @param duration duration of the animation
272      * @return the animation
273      */
createStashAnimator(boolean isStashed, long duration)274     private AnimatorSet createStashAnimator(boolean isStashed, long duration) {
275         AnimatorSet animatorSet = new AnimatorSet();
276 
277         AnimatorSet fullLengthAnimatorSet = new AnimatorSet();
278         // Not exactly half and may overlap. See [first|second]HalfDurationScale below.
279         AnimatorSet firstHalfAnimatorSet = new AnimatorSet();
280         AnimatorSet secondHalfAnimatorSet = new AnimatorSet();
281 
282         final float firstHalfDurationScale;
283         final float secondHalfDurationScale;
284 
285         if (isStashed) {
286             firstHalfDurationScale = 0.75f;
287             secondHalfDurationScale = 0.5f;
288 
289             fullLengthAnimatorSet.play(
290                     mIconTranslationYForStash.animateToValue(getStashTranslation()));
291 
292             firstHalfAnimatorSet.playTogether(
293                     mIconAlphaForStash.animateToValue(0),
294                     mIconScaleForStash.animateToValue(STASHED_BAR_SCALE));
295             secondHalfAnimatorSet.playTogether(
296                     mBubbleStashedHandleAlpha.animateToValue(1));
297         } else  {
298             firstHalfDurationScale = 0.5f;
299             secondHalfDurationScale = 0.75f;
300 
301             final float translationY = getBubbleBarTranslationY();
302 
303             fullLengthAnimatorSet.playTogether(
304                     mIconScaleForStash.animateToValue(1),
305                     mIconTranslationYForStash.animateToValue(translationY));
306 
307             firstHalfAnimatorSet.playTogether(
308                     mBubbleStashedHandleAlpha.animateToValue(0)
309             );
310             secondHalfAnimatorSet.playTogether(
311                     mIconAlphaForStash.animateToValue(1)
312             );
313         }
314 
315         fullLengthAnimatorSet.play(mHandleViewController.createRevealAnimToIsStashed(isStashed));
316 
317         fullLengthAnimatorSet.setDuration(duration);
318         firstHalfAnimatorSet.setDuration((long) (duration * firstHalfDurationScale));
319         secondHalfAnimatorSet.setDuration((long) (duration * secondHalfDurationScale));
320         secondHalfAnimatorSet.setStartDelay((long) (duration * (1 - secondHalfDurationScale)));
321 
322         animatorSet.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet,
323                 secondHalfAnimatorSet);
324         animatorSet.addListener(new AnimatorListenerAdapter() {
325             @Override
326             public void onAnimationEnd(Animator animation) {
327                 mAnimator = null;
328                 mControllers.runAfterInit(() -> {
329                     if (isStashed) {
330                         mBarViewController.setExpanded(false);
331                     }
332                     mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
333                 });
334             }
335         });
336         return animatorSet;
337     }
338 
getStashTranslation()339     private float getStashTranslation() {
340         return (mUnstashedHeight - mStashedHeight) / 2f;
341     }
342 
onIsStashedChanged()343     private void onIsStashedChanged() {
344         mControllers.runAfterInit(() -> {
345             mHandleViewController.onIsStashedChanged();
346             mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
347         });
348     }
349 
getBubbleBarTranslationYForTaskbar()350     public float getBubbleBarTranslationYForTaskbar() {
351         return -mActivity.getDeviceProfile().taskbarBottomMargin;
352     }
353 
getBubbleBarTranslationYForHotseat()354     private float getBubbleBarTranslationYForHotseat() {
355         final float hotseatBottomSpace = mActivity.getDeviceProfile().hotseatBarBottomSpacePx;
356         final float hotseatCellHeight = mActivity.getDeviceProfile().hotseatCellHeightPx;
357         return -hotseatBottomSpace - hotseatCellHeight + mUnstashedHeight - abs(
358                 hotseatCellHeight - mUnstashedHeight) / 2;
359     }
360 
getBubbleBarTranslationY()361     public float getBubbleBarTranslationY() {
362         // If we're on home, adjust the translation so the bubble bar aligns with hotseat.
363         // Otherwise we're either showing in an app or in overview. In either case adjust it so
364         // the bubble bar aligns with the taskbar.
365         return mBubblesShowingOnHome ? getBubbleBarTranslationYForHotseat()
366                 : getBubbleBarTranslationYForTaskbar();
367     }
368 
369     /**
370      * The difference on the Y axis between the center of the handle and the center of the bubble
371      * bar.
372      */
getDiffBetweenHandleAndBarCenters()373     public float getDiffBetweenHandleAndBarCenters() {
374         // the difference between the centers of the handle and the bubble bar is the difference
375         // between their distance from the bottom of the screen.
376 
377         float barCenter = mBarViewController.getBubbleBarCollapsedHeight() / 2f;
378         return mHandleCenterFromScreenBottom - barCenter;
379     }
380 
381     /** The distance the handle moves as part of the new bubble animation. */
getStashedHandleTranslationForNewBubbleAnimation()382     public float getStashedHandleTranslationForNewBubbleAnimation() {
383         // the should move up to the top of the stashed taskbar area. it is centered within it so
384         // it should move the same distance as it is away from the bottom.
385         return -mHandleCenterFromScreenBottom;
386     }
387 
388     /** Checks whether the motion event is over the stash handle. */
isEventOverStashHandle(MotionEvent ev)389     public boolean isEventOverStashHandle(MotionEvent ev) {
390         return mHandleViewController.isEventOverHandle(ev);
391     }
392 
393     /** Set a bubble bar location */
setBubbleBarLocation(BubbleBarLocation bubbleBarLocation)394     public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
395         mHandleViewController.setBubbleBarLocation(bubbleBarLocation);
396     }
397 
398     /** Returns the [PhysicsAnimator] for the stashed handle view. */
getStashedHandlePhysicsAnimator()399     public PhysicsAnimator<View> getStashedHandlePhysicsAnimator() {
400         return mHandleViewController.getPhysicsAnimator();
401     }
402 
403     /** Notifies taskbar that it should update its touchable region. */
updateTaskbarTouchRegion()404     public void updateTaskbarTouchRegion() {
405         mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
406     }
407 
408     /** Shows the bubble bar immediately without animation. */
showBubbleBarImmediate()409     public void showBubbleBarImmediate() {
410         mHandleViewController.setTranslationYForSwipe(0);
411         mIconTranslationYForStash.updateValue(getBubbleBarTranslationY());
412         mIconAlphaForStash.setValue(1);
413         mIconScaleForStash.updateValue(1);
414         mIsStashed = false;
415         onIsStashedChanged();
416     }
417 
418     /** Stashes the bubble bar immediately without animation. */
stashBubbleBarImmediate()419     public void stashBubbleBarImmediate() {
420         mHandleViewController.setTranslationYForSwipe(0);
421         mBubbleStashedHandleAlpha.setValue(1);
422         mIconAlphaForStash.setValue(0);
423         mIconTranslationYForStash.updateValue(getStashTranslation());
424         mIconScaleForStash.updateValue(STASHED_BAR_SCALE);
425         mIsStashed = true;
426         onIsStashedChanged();
427     }
428 
429     /**
430      * Updates the values of the internal animators after the new bubble animation was interrupted
431      *
432      * @param isStashed whether the current state should be stashed
433      * @param bubbleBarTranslationY the current bubble bar translation. this is only used if the
434      *                              bubble bar is showing to ensure that the stash animator runs
435      *                              smoothly.
436      */
onNewBubbleAnimationInterrupted(boolean isStashed, float bubbleBarTranslationY)437     public void onNewBubbleAnimationInterrupted(boolean isStashed, float bubbleBarTranslationY) {
438         if (isStashed) {
439             mBubbleStashedHandleAlpha.setValue(1);
440             mIconAlphaForStash.setValue(0);
441             mIconScaleForStash.updateValue(STASHED_BAR_SCALE);
442             mIconTranslationYForStash.updateValue(getStashTranslation());
443         } else {
444             mBubbleStashedHandleAlpha.setValue(0);
445             mHandleViewController.setTranslationYForSwipe(0);
446             mIconAlphaForStash.setValue(1);
447             mIconScaleForStash.updateValue(1);
448             mIconTranslationYForStash.updateValue(bubbleBarTranslationY);
449         }
450         mIsStashed = isStashed;
451         onIsStashedChanged();
452     }
453 
454     /** Set the translation Y for the stashed handle. */
setHandleTranslationY(float ty)455     public void setHandleTranslationY(float ty) {
456         mHandleViewController.setTranslationYForSwipe(ty);
457     }
458 }
459