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