• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.launcher3.taskbar.bubbles.stashing
18 
19 import android.animation.Animator
20 import android.animation.AnimatorSet
21 import android.animation.ValueAnimator
22 import android.content.Context
23 import android.graphics.Rect
24 import android.view.MotionEvent
25 import android.view.View
26 import androidx.annotation.VisibleForTesting
27 import androidx.core.animation.doOnEnd
28 import androidx.core.animation.doOnStart
29 import androidx.dynamicanimation.animation.SpringForce
30 import com.android.app.animation.Interpolators.EMPHASIZED
31 import com.android.app.animation.Interpolators.LINEAR
32 import com.android.launcher3.R
33 import com.android.launcher3.anim.AnimatedFloat
34 import com.android.launcher3.anim.SpringAnimationBuilder
35 import com.android.launcher3.taskbar.BarsLocationAnimatorHelper.FADE_IN_ANIM_ALPHA_DURATION_MS
36 import com.android.launcher3.taskbar.BarsLocationAnimatorHelper.FADE_OUT_ANIM_ALPHA_DELAY_MS
37 import com.android.launcher3.taskbar.BarsLocationAnimatorHelper.FADE_OUT_ANIM_ALPHA_DURATION_MS
38 import com.android.launcher3.taskbar.BarsLocationAnimatorHelper.FADE_OUT_ANIM_POSITION_DURATION_MS
39 import com.android.launcher3.taskbar.BarsLocationAnimatorHelper.inShiftX
40 import com.android.launcher3.taskbar.BarsLocationAnimatorHelper.outShift
41 import com.android.launcher3.taskbar.TaskbarInsetsController
42 import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_ALPHA_START_DELAY
43 import com.android.launcher3.taskbar.TaskbarStashController.TRANSIENT_TASKBAR_STASH_ALPHA_DURATION
44 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
45 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
46 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState
47 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_STASH_DURATION
48 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_TRANSLATION_DURATION
49 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.ControllersAfterInitAction
50 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.TaskbarHotseatDimensionsProvider
51 import com.android.launcher3.util.MultiPropertyFactory
52 import com.android.wm.shell.shared.animation.PhysicsAnimator
53 import com.android.wm.shell.shared.bubbles.BubbleBarLocation
54 import com.android.wm.shell.shared.bubbles.ContextUtils.isRtl
55 import kotlin.math.max
56 
57 class TransientBubbleStashController(
58     private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider,
59     private val context: Context,
60 ) : BubbleStashController {
61 
62     private lateinit var bubbleBarViewController: BubbleBarViewController
63     private lateinit var taskbarInsetsController: TaskbarInsetsController
64     private lateinit var controllersAfterInitAction: ControllersAfterInitAction
65 
66     // stash view properties
67     private var bubbleStashedHandleViewController: BubbleStashedHandleViewController? = null
68     private var stashHandleViewAlpha: MultiPropertyFactory<View>.MultiProperty? = null
69     private var translationYDuringStash = AnimatedFloat { transY ->
70         bubbleStashedHandleViewController?.setTranslationYForStash(transY)
71         bubbleBarViewController.setTranslationYForStash(transY)
72     }
73     private val stashHandleStashVelocity =
74         context.resources.getDimension(R.dimen.bubblebar_stashed_handle_spring_velocity_dp_per_s)
75     private var stashedHeight: Int = 0
76 
77     // bubble bar properties
78     private lateinit var bubbleBarAlpha: MultiPropertyFactory<View>.MultiProperty
79     private lateinit var bubbleBarBubbleAlpha: AnimatedFloat
80     private lateinit var bubbleBarBackgroundAlpha: AnimatedFloat
81     private lateinit var bubbleBarTranslationYAnimator: AnimatedFloat
82     private lateinit var bubbleBarBubbleTranslationY: AnimatedFloat
83     private lateinit var bubbleBarBackgroundScaleX: AnimatedFloat
84     private lateinit var bubbleBarBackgroundScaleY: AnimatedFloat
85     private val handleCenterFromScreenBottom =
86         context.resources.getDimensionPixelSize(R.dimen.bubblebar_stashed_size) / 2f
87 
88     private var animator: AnimatorSet? = null
89     override var bubbleBarVerticalCenterForHome: Int = 0
90 
91     override var isStashed: Boolean = false
92         @VisibleForTesting set
93 
94     override var launcherState: BubbleLauncherState = BubbleLauncherState.IN_APP
95         set(state) {
96             if (field == state) return
97             field = state
98             val hasBubbles = bubbleBarViewController.hasBubbles()
99             bubbleBarViewController.onBubbleBarConfigurationChanged(hasBubbles)
100             if (!hasBubbles) {
101                 // if there are no bubbles, there's no need to update the bubble bar, just keep the
102                 // isStashed state up to date so that we can process state changes when bubbles are
103                 // created.
104                 isStashed = launcherState == BubbleLauncherState.IN_APP
105                 return
106             }
107             if (field == BubbleLauncherState.HOME) {
108                 // When to home we need to animate the bubble bar
109                 // here to align with hotseat center.
110                 animateBubbleBarYToHotseat()
111             } else if (field == BubbleLauncherState.OVERVIEW) {
112                 // When transitioning to overview we need to animate the bubble bar to align with
113                 // the taskbar bottom.
114                 animateBubbleBarYToTaskbar()
115             }
116             // Only stash if we're in an app, otherwise we're in home or overview where we should
117             // be un-stashed
118             updateStashedAndExpandedState(field == BubbleLauncherState.IN_APP, expand = false)
119         }
120 
121     override var isSysuiLocked: Boolean = false
122         set(isLocked) {
123             if (field == isLocked) return
124             field = isLocked
125             if (!isLocked && bubbleBarViewController.hasBubbles()) {
126                 animateAfterUnlock()
127             }
128         }
129 
130     override val isTransientTaskBar: Boolean = true
131 
132     override val bubbleBarTranslationYForHotseat: Float
133         get() {
134             val bubbleBarHeight = bubbleBarViewController.bubbleBarCollapsedHeight
135             return -bubbleBarVerticalCenterForHome + bubbleBarHeight / 2
136         }
137 
138     override val bubbleBarTranslationYForTaskbar: Float =
139         -taskbarHotseatDimensionsProvider.getTaskbarBottomSpace().toFloat()
140 
141     /** Not supported in transient mode */
142     override var inAppDisplayOverrideProgress: Float = 0f
143 
144     /** Check if we have handle view controller */
145     override val hasHandleView: Boolean
146         get() = bubbleStashedHandleViewController != null
147 
148     override fun init(
149         taskbarInsetsController: TaskbarInsetsController,
150         bubbleBarViewController: BubbleBarViewController,
151         bubbleStashedHandleViewController: BubbleStashedHandleViewController?,
152         controllersAfterInitAction: ControllersAfterInitAction,
153     ) {
154         this.taskbarInsetsController = taskbarInsetsController
155         this.bubbleBarViewController = bubbleBarViewController
156         this.bubbleStashedHandleViewController = bubbleStashedHandleViewController
157         this.controllersAfterInitAction = controllersAfterInitAction
158         bubbleBarTranslationYAnimator = bubbleBarViewController.bubbleBarTranslationY
159         bubbleBarBubbleTranslationY = bubbleBarViewController.bubbleOffsetY
160         // bubble bar has only alpha property, getting it at index 0
161         bubbleBarAlpha = bubbleBarViewController.bubbleBarAlpha.get(/* index= */ 0)
162         bubbleBarBubbleAlpha = bubbleBarViewController.bubbleBarBubbleAlpha
163         bubbleBarBackgroundAlpha = bubbleBarViewController.bubbleBarBackgroundAlpha
164         bubbleBarBackgroundScaleX = bubbleBarViewController.bubbleBarBackgroundScaleX
165         bubbleBarBackgroundScaleY = bubbleBarViewController.bubbleBarBackgroundScaleY
166         stashedHeight = bubbleStashedHandleViewController?.stashedHeight ?: 0
167         stashHandleViewAlpha = bubbleStashedHandleViewController?.stashedHandleAlpha?.get(0)
168     }
169 
170     private fun animateAfterUnlock() {
171         val animatorSet = AnimatorSet()
172         if (isBubblesShowingOnHome || isBubblesShowingOnOverview) {
173             isStashed = false
174             animatorSet.playTogether(
175                 bubbleBarBackgroundScaleX.animateToValue(1f),
176                 bubbleBarBackgroundScaleY.animateToValue(1f),
177                 bubbleBarTranslationYAnimator.animateToValue(bubbleBarTranslationY),
178                 bubbleBarAlpha.animateToValue(1f),
179                 bubbleBarBubbleAlpha.animateToValue(1f),
180                 bubbleBarBackgroundAlpha.animateToValue(1f),
181             )
182         } else {
183             isStashed = true
184             stashHandleViewAlpha?.let { animatorSet.playTogether(it.animateToValue(1f)) }
185         }
186         animatorSet
187             .updateBarVisibility(isStashed)
188             .updateTouchRegionOnAnimationEnd()
189             .setDuration(BAR_STASH_DURATION)
190             .start()
191     }
192 
193     override fun showBubbleBarImmediate() {
194         showBubbleBarImmediate(bubbleBarTranslationY)
195     }
196 
197     override fun showBubbleBarImmediate(bubbleBarTranslationY: Float) {
198         showBubbleBarImmediateVisually(bubbleBarTranslationY)
199         onIsStashedChanged()
200     }
201 
202     private fun showBubbleBarImmediateVisually(bubbleBarTranslationY: Float) {
203         bubbleStashedHandleViewController?.setTranslationYForSwipe(0f)
204         stashHandleViewAlpha?.value = 0f
205         this.bubbleBarTranslationYAnimator.updateValue(bubbleBarTranslationY)
206         bubbleBarAlpha.setValue(1f)
207         bubbleBarBubbleAlpha.updateValue(1f)
208         bubbleBarBackgroundAlpha.updateValue(1f)
209         bubbleBarBackgroundScaleX.updateValue(1f)
210         bubbleBarBackgroundScaleY.updateValue(1f)
211         isStashed = false
212         bubbleBarViewController.setHiddenForStashed(false)
213     }
214 
215     override fun stashBubbleBarImmediate() {
216         stashBubbleBarImmediateVisually()
217         onIsStashedChanged()
218     }
219 
220     private fun stashBubbleBarImmediateVisually() {
221         bubbleStashedHandleViewController?.setTranslationYForSwipe(0f)
222         stashHandleViewAlpha?.value = 1f
223         this.bubbleBarTranslationYAnimator.updateValue(getStashTranslation())
224         bubbleBarAlpha.setValue(0f)
225         // Reset bubble and background alpha to 1 and only keep the bubble bar alpha at 0
226         bubbleBarBubbleAlpha.updateValue(1f)
227         bubbleBarBackgroundAlpha.updateValue(1f)
228         bubbleBarBackgroundScaleX.updateValue(getStashScaleX())
229         bubbleBarBackgroundScaleY.updateValue(getStashScaleY())
230         isStashed = true
231         bubbleBarViewController.setHiddenForStashed(true)
232     }
233 
234     override fun getTouchableHeight(): Int =
235         when {
236             isStashed -> stashedHeight
237             isBubbleBarVisible() -> bubbleBarViewController.bubbleBarCollapsedHeight.toInt()
238             else -> 0
239         }
240 
241     override fun isBubbleBarVisible(): Boolean = bubbleBarViewController.hasBubbles() && !isStashed
242 
243     override fun onNewBubbleAnimationInterrupted(isStashed: Boolean, bubbleBarTranslationY: Float) {
244         if (isStashed) {
245             stashBubbleBarImmediate()
246         } else {
247             showBubbleBarImmediate(bubbleBarTranslationY)
248         }
249     }
250 
251     /** Check if [ev] belongs to the stash handle or the bubble bar views. */
252     override fun isEventOverBubbleBarViews(ev: MotionEvent): Boolean {
253         val isOverHandle = bubbleStashedHandleViewController?.isEventOverHandle(ev) ?: false
254         return isOverHandle || bubbleBarViewController.isEventOverAnyItem(ev)
255     }
256 
257     /** Set the bubble bar stash handle location . */
258     override fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) {
259         bubbleStashedHandleViewController?.setBubbleBarLocation(bubbleBarLocation)
260     }
261 
262     override fun stashBubbleBar() {
263         updateStashedAndExpandedState(stash = true, expand = false)
264     }
265 
266     override fun stashBubbleBarToLocation(
267         fromLocation: BubbleBarLocation,
268         toLocation: BubbleBarLocation,
269     ) {
270         if (fromLocation.isSameSideWith(toLocation)) {
271             updateStashedAndExpandedState(
272                 stash = true,
273                 expand = false,
274                 updateTouchRegionOnEnd = false,
275             )
276             return
277         }
278         cancelAnimation()
279         animator =
280             AnimatorSet().apply {
281                 playSequentially(
282                     bubbleBarViewController.animateBubbleBarLocationOut(toLocation),
283                     createHandleInAnimator(location = toLocation),
284                 )
285                 start()
286             }
287     }
288 
289     override fun showBubbleBar(expandBubbles: Boolean, bubbleBarGesture: Boolean) {
290         updateStashedAndExpandedState(
291             stash = false,
292             expand = expandBubbles,
293             bubbleBarGesture = bubbleBarGesture,
294         )
295     }
296 
297     override fun showBubbleBarAtLocation(
298         fromLocation: BubbleBarLocation,
299         toLocation: BubbleBarLocation,
300     ) {
301         if (fromLocation.isSameSideWith(toLocation)) {
302             updateStashedAndExpandedState(
303                 stash = false,
304                 expand = false,
305                 updateTouchRegionOnEnd = false,
306             )
307             return
308         }
309         cancelAnimation()
310         val bubbleBarInAnimation =
311             bubbleBarViewController.animateBubbleBarLocationIn(fromLocation, toLocation).apply {
312                 doOnStart { showBubbleBarImmediateVisually(bubbleBarTranslationY) }
313             }
314         animator =
315             AnimatorSet().apply {
316                 playSequentially(
317                     createHandleOutAnimator(location = toLocation),
318                     bubbleBarInAnimation,
319                 )
320                 start()
321             }
322     }
323 
324     override fun getDiffBetweenHandleAndBarCenters(): Float {
325         // the difference between the centers of the handle and the bubble bar is the difference
326         // between their distance from the bottom of the screen.
327         val barCenter: Float = bubbleBarViewController.bubbleBarCollapsedHeight / 2f
328         return handleCenterFromScreenBottom - barCenter
329     }
330 
331     override fun getStashedHandleTranslationForNewBubbleAnimation(): Float {
332         return -handleCenterFromScreenBottom
333     }
334 
335     override fun getStashedHandlePhysicsAnimator(): PhysicsAnimator<View>? {
336         return bubbleStashedHandleViewController?.physicsAnimator
337     }
338 
339     override fun updateTaskbarTouchRegion() {
340         taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
341     }
342 
343     override fun setHandleTranslationY(translationY: Float) {
344         bubbleStashedHandleViewController?.setTranslationYForSwipe(translationY)
345     }
346 
347     override fun getHandleTranslationY(): Float? = bubbleStashedHandleViewController?.translationY
348 
349     override fun getHandleBounds(bounds: Rect) {
350         bubbleStashedHandleViewController?.getBounds(bounds)
351     }
352 
353     private fun getStashTranslation(): Float {
354         return (bubbleBarTranslationY - stashedHeight) / 2f
355     }
356 
357     @VisibleForTesting
358     fun getStashScaleX(): Float {
359         val handleWidth = bubbleStashedHandleViewController?.stashedWidth ?: 0
360         return handleWidth / bubbleBarViewController.bubbleBarCollapsedWidth
361     }
362 
363     @VisibleForTesting
364     fun getStashScaleY(): Float {
365         val handleHeight = bubbleStashedHandleViewController?.stashedHeight ?: 0
366         return handleHeight / bubbleBarViewController.bubbleBarCollapsedHeight
367     }
368 
369     /**
370      * Create a stash animation.
371      *
372      * @param isStashed whether it's a stash animation or an unstash animation
373      * @param duration duration of the animation
374      * @return the animation
375      */
376     @Suppress("SameParameterValue")
377     private fun createStashAnimator(isStashed: Boolean, duration: Long): AnimatorSet {
378         val animatorSet = AnimatorSet()
379 
380         animatorSet.play(
381             createBackgroundAlphaAnimator(isStashed).apply {
382                 val alphaDuration =
383                     if (isStashed) duration else TRANSIENT_TASKBAR_STASH_ALPHA_DURATION
384                 val alphaDelay = if (isStashed) TASKBAR_STASH_ALPHA_START_DELAY else 0L
385                 this.duration = max(0L, alphaDuration - alphaDelay)
386                 this.startDelay = alphaDelay
387                 this.interpolator = LINEAR
388             }
389         )
390 
391         animatorSet.play(
392             bubbleBarBubbleAlpha
393                 .animateToValue(getBarAlphaStart(isStashed), getBarAlphaEnd(isStashed))
394                 .apply {
395                     this.duration = TRANSIENT_TASKBAR_STASH_ALPHA_DURATION
396                     this.startDelay = TASKBAR_STASH_ALPHA_START_DELAY
397                     this.interpolator = LINEAR
398                 }
399         )
400 
401         animatorSet.play(
402             createSpringOnStashAnimator(isStashed).apply {
403                 this.duration = duration
404                 this.interpolator = LINEAR
405             }
406         )
407 
408         animatorSet.play(
409             bubbleBarViewController.createRevealAnimatorForStashChange(isStashed).apply {
410                 this.duration = duration
411                 this.interpolator = EMPHASIZED
412             }
413         )
414 
415         // Animate bubble translation to keep reveal animation in the bounds of the bar
416         val bubbleTyStart = if (isStashed) 0f else -bubbleBarTranslationY
417         val bubbleTyEnd = if (isStashed) -bubbleBarTranslationY else 0f
418         animatorSet.play(
419             bubbleBarBubbleTranslationY.animateToValue(bubbleTyStart, bubbleTyEnd).apply {
420                 this.duration = duration
421                 this.interpolator = EMPHASIZED
422             }
423         )
424 
425         animatorSet.play(
426             bubbleStashedHandleViewController?.createRevealAnimToIsStashed(isStashed)?.apply {
427                 this.duration = duration
428                 this.interpolator = EMPHASIZED
429             }
430         )
431 
432         val pivotX = if (bubbleBarViewController.isBubbleBarOnLeft) 0f else 1f
433         animatorSet.play(
434             createScaleAnimator(isStashed).apply {
435                 this.duration = duration
436                 this.interpolator = EMPHASIZED
437                 this.setBubbleBarPivotDuringAnim(pivotX, 1f)
438             }
439         )
440 
441         val translationYTarget = if (isStashed) getStashTranslation() else bubbleBarTranslationY
442         animatorSet.play(
443             bubbleBarTranslationYAnimator.animateToValue(translationYTarget).apply {
444                 this.duration = duration
445                 this.interpolator = EMPHASIZED
446             }
447         )
448 
449         animatorSet.doOnStart {
450             // Update the start value for bubble view and background alpha when the entire animation
451             // begins.
452             // Alpha animation has a delay, and if we set the initial values at the start of the
453             // alpha animation, it will cause flickers.
454             bubbleBarBubbleAlpha.updateValue(getBarAlphaStart(isStashed))
455             bubbleBarBackgroundAlpha.updateValue(getBarAlphaStart(isStashed))
456             // We animate alpha for background and bubble views separately. Make sure the container
457             // is always visible.
458             bubbleBarAlpha.value = 1f
459         }
460         animatorSet.doOnEnd {
461             cancelAnimation()
462             controllersAfterInitAction.runAfterInit {
463                 if (isStashed) {
464                     bubbleBarAlpha.value = 0f
465                     // reset bubble view alpha
466                     bubbleBarBubbleAlpha.updateValue(1f)
467                     bubbleBarBackgroundAlpha.updateValue(1f)
468                     // reset stash translation
469                     translationYDuringStash.updateValue(0f)
470                     bubbleBarBubbleTranslationY.updateValue(0f)
471                     bubbleBarViewController.isExpanded = false
472                 }
473                 taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
474             }
475         }
476         return animatorSet
477     }
478 
479     private fun createBackgroundAlphaAnimator(isStashed: Boolean): AnimatorSet {
480         return AnimatorSet().apply {
481             play(
482                 bubbleBarBackgroundAlpha.animateToValue(
483                     getBarAlphaStart(isStashed),
484                     getBarAlphaEnd(isStashed),
485                 )
486             )
487             play(stashHandleViewAlpha?.animateToValue(getHandleAlphaEnd(isStashed)))
488         }
489     }
490 
491     private fun getBarAlphaStart(isStashed: Boolean): Float {
492         return if (isStashed) 1f else 0f
493     }
494 
495     private fun getBarAlphaEnd(isStashed: Boolean): Float {
496         return if (isStashed) 0f else 1f
497     }
498 
499     private fun getHandleAlphaEnd(isStashed: Boolean): Float {
500         return if (isStashed) 1f else 0f
501     }
502 
503     private fun createSpringOnStashAnimator(isStashed: Boolean): Animator {
504         if (!isStashed) {
505             // Animate the stash translation back to 0
506             return translationYDuringStash.animateToValue(0f)
507         }
508         // Apply a spring to the handle
509         return SpringAnimationBuilder(context)
510             .setStartValue(translationYDuringStash.value)
511             .setEndValue(0f)
512             .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
513             .setStiffness(SpringForce.STIFFNESS_LOW)
514             .setStartVelocity(stashHandleStashVelocity)
515             .build(translationYDuringStash, AnimatedFloat.VALUE)
516     }
517 
518     private fun createScaleAnimator(isStashed: Boolean): AnimatorSet {
519         val scaleXTarget = if (isStashed) getStashScaleX() else 1f
520         val scaleYTarget = if (isStashed) getStashScaleY() else 1f
521         return AnimatorSet().apply {
522             play(bubbleBarBackgroundScaleX.animateToValue(scaleXTarget))
523             play(bubbleBarBackgroundScaleY.animateToValue(scaleYTarget))
524         }
525     }
526 
527     private fun onIsStashedChanged() {
528         controllersAfterInitAction.runAfterInit {
529             taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
530             bubbleStashedHandleViewController?.onIsStashedChanged()
531         }
532     }
533 
534     private fun animateBubbleBarYToHotseat() {
535         translateBubbleBarYUpdateTouchRegionOnCompletion(bubbleBarTranslationYForHotseat)
536     }
537 
538     private fun animateBubbleBarYToTaskbar() {
539         translateBubbleBarYUpdateTouchRegionOnCompletion(bubbleBarTranslationYForTaskbar)
540     }
541 
542     private fun translateBubbleBarYUpdateTouchRegionOnCompletion(toY: Float) {
543         bubbleBarViewController.bubbleBarTranslationY
544             .animateToValue(toY)
545             .updateTouchRegionOnAnimationEnd()
546             .setDuration(BAR_TRANSLATION_DURATION)
547             .start()
548     }
549 
550     @VisibleForTesting
551     fun updateStashedAndExpandedState(
552         stash: Boolean,
553         expand: Boolean,
554         bubbleBarGesture: Boolean = false,
555         updateTouchRegionOnEnd: Boolean = true,
556     ) {
557         if (bubbleBarViewController.isHiddenForNoBubbles) {
558             // If there are no bubbles the bar and handle are invisible, nothing to do here.
559             return
560         }
561         val isStashed = stash && !isBubblesShowingOnHome && !isBubblesShowingOnOverview
562         if (this.isStashed != isStashed) {
563             this.isStashed = isStashed
564 
565             // notify the view controller that the stash state is about to change so that it can
566             // cancel an ongoing animation if there is one.
567             bubbleBarViewController.onStashStateChanging()
568             cancelAnimation()
569             animator =
570                 createStashAnimator(isStashed, BAR_STASH_DURATION).apply {
571                     updateBarVisibility(isStashed)
572                     if (updateTouchRegionOnEnd) {
573                         updateTouchRegionOnAnimationEnd()
574                     }
575                     start()
576                 }
577         }
578         if (bubbleBarViewController.isExpanded != expand) {
579             val maybeShowEdu = expand && bubbleBarGesture
580             bubbleBarViewController.setExpanded(expand, maybeShowEdu)
581         }
582     }
583 
584     private fun cancelAnimation() {
585         animator?.cancel()
586         animator = null
587     }
588 
589     override fun getHandleViewAlpha(): MultiPropertyFactory<View>.MultiProperty? =
590         // only return handle alpha if the bubble bar is stashed and has bubbles
591         if (isStashed && bubbleBarViewController.hasBubbles()) {
592             stashHandleViewAlpha
593         } else {
594             null
595         }
596 
597     private fun Animator.updateTouchRegionOnAnimationEnd(): Animator {
598         doOnEnd { onIsStashedChanged() }
599         return this
600     }
601 
602     private fun <T : Animator> T.updateBarVisibility(stashed: Boolean): T {
603         if (stashed) {
604             doOnEnd { bubbleBarViewController.setHiddenForStashed(true) }
605         } else {
606             doOnStart { bubbleBarViewController.setHiddenForStashed(false) }
607         }
608         return this
609     }
610 
611     // TODO(b/399678274) add tests
612     private fun createHandleInAnimator(location: BubbleBarLocation): Animator? {
613         val stashHandleViewController = bubbleStashedHandleViewController ?: return null
614         val handleAlpha = stashHandleViewController.stashedHandleAlpha.get(0)
615         val shift = context.inShiftX
616         val startX = if (location.isOnLeft(context.isRtl)) shift else -shift
617         val handleTranslationX =
618             ValueAnimator.ofFloat(startX, 0f).apply {
619                 addUpdateListener { v ->
620                     stashHandleViewController.setTranslationX(v.animatedValue as Float)
621                 }
622                 duration = FADE_IN_ANIM_ALPHA_DURATION_MS
623             }
624         val handleAlphaAnimation =
625             handleAlpha.animateToValue(1f).apply { duration = FADE_IN_ANIM_ALPHA_DURATION_MS }
626         return AnimatorSet().apply {
627             playTogether(handleTranslationX, handleAlphaAnimation)
628             doOnStart { stashBubbleBarImmediateVisually() }
629         }
630     }
631 
632     private fun createHandleOutAnimator(location: BubbleBarLocation): Animator? {
633         val stashHandleViewController = bubbleStashedHandleViewController ?: return null
634         val handleAlpha = stashHandleViewController.stashedHandleAlpha.get(0)
635         val shift = context.outShift
636         val endX = if (location.isOnLeft(context.isRtl)) -shift else shift
637         val handleTranslationX =
638             ValueAnimator.ofFloat(0f, endX).apply {
639                 addUpdateListener { v ->
640                     stashHandleViewController.setTranslationX(v.animatedValue as Float)
641                 }
642                 duration = FADE_OUT_ANIM_POSITION_DURATION_MS
643                 // in case item dropped to the opposite side - need to clear translation
644                 doOnEnd { stashHandleViewController.setTranslationX(0f) }
645             }
646         val handleAlphaAnimation =
647             handleAlpha.animateToValue(0f).apply {
648                 duration = FADE_OUT_ANIM_ALPHA_DURATION_MS
649                 startDelay = FADE_OUT_ANIM_ALPHA_DELAY_MS
650             }
651         return AnimatorSet().apply { playTogether(handleTranslationX, handleAlphaAnimation) }
652     }
653 
654     private fun Animator.setBubbleBarPivotDuringAnim(pivotX: Float, pivotY: Float): Animator {
655         var initialPivotX = Float.NaN
656         var initialPivotY = Float.NaN
657         doOnStart {
658             initialPivotX = bubbleBarViewController.bubbleBarRelativePivotX
659             initialPivotY = bubbleBarViewController.bubbleBarRelativePivotY
660             bubbleBarViewController.setBubbleBarRelativePivot(pivotX, pivotY)
661         }
662         doOnEnd {
663             if (!initialPivotX.isNaN() && !initialPivotY.isNaN()) {
664                 bubbleBarViewController.setBubbleBarRelativePivot(initialPivotX, initialPivotY)
665             }
666         }
667         return this
668     }
669 
670     private fun BubbleBarLocation.isSameSideWith(anotherLocation: BubbleBarLocation): Boolean {
671         val isRtl = context.isRtl
672         return this.isOnLeft(isRtl) == anotherLocation.isOnLeft(isRtl)
673     }
674 }
675