• 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.flyout
18 
19 import android.graphics.Rect
20 import android.view.Gravity
21 import android.view.ViewGroup
22 import android.widget.FrameLayout
23 import androidx.core.animation.ValueAnimator
24 import com.android.app.animation.InterpolatorsAndroidX
25 import com.android.launcher3.R
26 import com.android.systemui.util.addListener
27 
28 /** Creates and manages the visibility of the [BubbleBarFlyoutView]. */
29 class BubbleBarFlyoutController
30 @JvmOverloads
31 constructor(
32     private val container: FrameLayout,
33     private val positioner: BubbleBarFlyoutPositioner,
34     private val callbacks: FlyoutCallbacks,
35     private val flyoutScheduler: FlyoutScheduler = HandlerScheduler(container),
36 ) {
37 
38     val maximumFlyoutHeight: Int = BubbleBarFlyoutView.getMaximumViewHeight(container.context)
39 
40     private companion object {
41         const val EXPAND_ANIMATION_DURATION_MS = 400L
42         const val COLLAPSE_ANIMATION_DURATION_MS = 350L
43     }
44 
45     private var flyout: BubbleBarFlyoutView? = null
46     private var animator: ValueAnimator? = null
47     private val horizontalMargin =
48         container.context.resources.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin)
49 
50     private enum class AnimationType {
51         /** Morphs the flyout between a dot and a rounded rectangle. */
52         MORPH,
53         /** Fades the flyout in or out. */
54         FADE,
55     }
56 
57     /** The bounds of the flyout. */
58     val flyoutBounds: Rect?
59         get() {
60             val flyout = this.flyout ?: return null
61             val rect = Rect(flyout.bounds)
62             rect.offset(0, flyout.translationY.toInt())
63             return rect
64         }
65 
66     fun setUpAndShowFlyout(message: BubbleBarFlyoutMessage, onInit: () -> Unit, onEnd: () -> Unit) {
67         flyout?.let(container::removeView)
68         val flyout = BubbleBarFlyoutView(container.context, positioner, flyoutScheduler)
69 
70         flyout.translationY = positioner.targetTy
71 
72         val lp =
73             FrameLayout.LayoutParams(
74                 ViewGroup.LayoutParams.WRAP_CONTENT,
75                 ViewGroup.LayoutParams.WRAP_CONTENT,
76                 Gravity.BOTTOM or if (positioner.isOnLeft) Gravity.LEFT else Gravity.RIGHT,
77             )
78         lp.marginStart = horizontalMargin
79         lp.marginEnd = horizontalMargin
80         container.addView(flyout, lp)
81 
82         this.flyout = flyout
83         flyout.showFromCollapsed(message) {
84             flyout.updateExpansionProgress(0f)
85             onInit()
86             showFlyout(AnimationType.MORPH, onEnd)
87         }
88     }
89 
90     private fun showFlyout(animationType: AnimationType, endAction: () -> Unit) {
91         val flyout = this.flyout ?: return
92         val startValue = getCurrentAnimatedValueIfRunning() ?: 0f
93         val duration = (EXPAND_ANIMATION_DURATION_MS * (1f - startValue)).toLong()
94         animator?.cancel()
95         val animator = ValueAnimator.ofFloat(startValue, 1f).setDuration(duration)
96         animator.interpolator = InterpolatorsAndroidX.EMPHASIZED
97         this.animator = animator
98         when (animationType) {
99             AnimationType.FADE ->
100                 animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float }
101             AnimationType.MORPH ->
102                 animator.addUpdateListener { _ ->
103                     flyout.updateExpansionProgress(animator.animatedValue as Float)
104                 }
105         }
106         animator.addListener(
107             onEnd = {
108                 endAction()
109                 flyout.setOnClickListener { callbacks.flyoutClicked() }
110             }
111         )
112         animator.start()
113     }
114 
115     fun updateFlyoutFullyExpanded(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
116         val flyout = flyout ?: return
117         hideFlyout(AnimationType.FADE) {
118             flyout.updateData(message) { showFlyout(AnimationType.FADE, onEnd) }
119         }
120     }
121 
122     fun updateFlyoutWhileExpanding(message: BubbleBarFlyoutMessage) {
123         val flyout = flyout ?: return
124         flyout.updateData(message) {}
125     }
126 
127     fun updateFlyoutWhileCollapsing(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
128         val flyout = flyout ?: return
129         animator?.pause()
130         animator?.removeAllListeners()
131         flyout.updateData(message) { showFlyout(AnimationType.MORPH, onEnd) }
132     }
133 
134     fun cancelFlyout(endAction: () -> Unit) {
135         hideFlyout(AnimationType.FADE) {
136             cleanupFlyoutView()
137             endAction()
138         }
139     }
140 
141     fun collapseFlyout(endAction: () -> Unit) {
142         hideFlyout(AnimationType.MORPH) {
143             cleanupFlyoutView()
144             endAction()
145         }
146     }
147 
148     private fun hideFlyout(animationType: AnimationType, endAction: () -> Unit) {
149         val flyout = this.flyout ?: return
150         val startValue = getCurrentAnimatedValueIfRunning() ?: 1f
151         val duration = (COLLAPSE_ANIMATION_DURATION_MS * startValue).toLong()
152         animator?.cancel()
153         val animator = ValueAnimator.ofFloat(startValue, 0f).setDuration(duration)
154         animator.interpolator = InterpolatorsAndroidX.EMPHASIZED
155         this.animator = animator
156         when (animationType) {
157             AnimationType.FADE ->
158                 animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float }
159             AnimationType.MORPH ->
160                 animator.addUpdateListener { _ ->
161                     flyout.updateExpansionProgress(animator.animatedValue as Float)
162                 }
163         }
164         animator.addListener(
165             onStart = {
166                 flyout.setOnClickListener(null)
167                 if (animationType == AnimationType.MORPH) {
168                     flyout.updateTranslationToCollapsedPosition()
169                 }
170             },
171             onEnd = { endAction() },
172         )
173         animator.start()
174     }
175 
176     private fun cleanupFlyoutView() {
177         container.removeView(flyout)
178         this@BubbleBarFlyoutController.flyout = null
179     }
180 
181     fun hasFlyout() = flyout != null
182 
183     private fun getCurrentAnimatedValueIfRunning(): Float? {
184         val animator = animator ?: return null
185         return if (animator.isRunning) animator.animatedValue as Float else null
186     }
187 }
188