xref: /aosp_15_r20/frameworks/base/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DismissView.kt (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
1 /*
2  * 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.wm.shell.shared.bubbles
18 
19 import android.animation.ObjectAnimator
20 import android.content.Context
21 import android.graphics.Color
22 import android.graphics.drawable.GradientDrawable
23 import android.util.IntProperty
24 import android.util.Log
25 import android.view.Gravity
26 import android.view.View
27 import android.view.ViewGroup
28 import android.view.WindowInsets
29 import android.view.WindowManager
30 import android.widget.FrameLayout
31 import androidx.annotation.ColorRes
32 import androidx.annotation.DimenRes
33 import androidx.annotation.DrawableRes
34 import androidx.core.content.ContextCompat
35 import androidx.dynamicanimation.animation.DynamicAnimation
36 import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY
37 import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW
38 import com.android.wm.shell.shared.animation.PhysicsAnimator
39 
40 /**
41  * View that handles interactions between DismissCircleView and BubbleStackView.
42  *
43  * @note [setup] method should be called after initialisation
44  */
45 class DismissView(context: Context) : FrameLayout(context) {
46     /**
47      * The configuration is used to provide module specific resource ids
48      *
49      * @see [setup] method
50      */
51     data class Config(
52             /** The resource id to set on the dismiss target circle view */
53             val dismissViewResId: Int,
54             /** dimen resource id of the dismiss target circle view size */
55             @DimenRes val targetSizeResId: Int,
56             /** dimen resource id of the icon size in the dismiss target */
57             @DimenRes val iconSizeResId: Int,
58             /** dimen resource id of the bottom margin for the dismiss target */
59             @DimenRes var bottomMarginResId: Int,
60             /** dimen resource id of the height for dismiss area gradient */
61             @DimenRes val floatingGradientHeightResId: Int,
62             /** color resource id of the dismiss area gradient color */
63             @ColorRes val floatingGradientColorResId: Int,
64             /** drawable resource id of the dismiss target background */
65             @DrawableRes val backgroundResId: Int,
66             /** drawable resource id of the icon for the dismiss target */
67             @DrawableRes val iconResId: Int
68     )
69 
70     companion object {
71         private const val SHOULD_SETUP =
72                 "The view isn't ready. Should be called after `setup`"
73         private val TAG = DismissView::class.simpleName
74     }
75 
76     var circle = DismissCircleView(context)
77     var isShowing = false
78     var config: Config? = null
79 
80     private val animator = PhysicsAnimator.getInstance(circle)
81     private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY)
82     private val DISMISS_SCRIM_FADE_MS = 200L
83     private var wm: WindowManager =
84             context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
85     private var gradientDrawable: GradientDrawable? = null
86 
87     private val GRADIENT_ALPHA: IntProperty<GradientDrawable> =
88             object : IntProperty<GradientDrawable>("alpha") {
setValuenull89         override fun setValue(d: GradientDrawable, percent: Int) {
90             d.alpha = percent
91         }
getnull92         override fun get(d: GradientDrawable): Int {
93             return d.alpha
94         }
95     }
96 
97     init {
98         setClipToPadding(false)
99         setClipChildren(false)
100         setVisibility(View.INVISIBLE)
101         addView(circle)
102     }
103 
104     /**
105      * Sets up view with the provided resource ids.
106      *
107      * Decouples resource dependency in order to be used externally (e.g. Launcher). Usually called
108      * with default params in module specific extension:
109      * @see [DismissView.setup] in DismissViewExt.kt
110      */
setupnull111     fun setup(config: Config) {
112         this.config = config
113 
114         // Setup layout
115         layoutParams = LayoutParams(
116                 ViewGroup.LayoutParams.MATCH_PARENT,
117                 resources.getDimensionPixelSize(config.floatingGradientHeightResId),
118                 Gravity.BOTTOM)
119         updatePadding()
120 
121         // Setup gradient
122         gradientDrawable = createGradient(color = config.floatingGradientColorResId)
123         setBackgroundDrawable(gradientDrawable)
124 
125         // Setup DismissCircleView
126         circle.id = config.dismissViewResId
127         circle.setup(config.backgroundResId, config.iconResId, config.iconSizeResId)
128         val targetSize: Int = resources.getDimensionPixelSize(config.targetSizeResId)
129         circle.layoutParams = LayoutParams(targetSize, targetSize,
130                 Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)
131         // Initial position with circle offscreen so it's animated up
132         circle.translationY = resources.getDimensionPixelSize(config.floatingGradientHeightResId)
133                 .toFloat()
134     }
135 
136     /**
137      * Animates this view in.
138      */
shownull139     fun show() {
140         if (isShowing) return
141         val gradientDrawable = checkExists(gradientDrawable) ?: return
142         isShowing = true
143         setVisibility(View.VISIBLE)
144         val alphaAnim = ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA,
145                 gradientDrawable.alpha, 255)
146         alphaAnim.setDuration(DISMISS_SCRIM_FADE_MS)
147         alphaAnim.start()
148 
149         animator.cancel()
150         animator
151             .spring(DynamicAnimation.TRANSLATION_Y, 0f, spring)
152             .start()
153     }
154 
155     /**
156      * Animates this view out, as well as the circle that encircles the bubbles, if they
157      * were dragged into the target and encircled.
158      */
hidenull159     fun hide() {
160         if (!isShowing) return
161         val gradientDrawable = checkExists(gradientDrawable) ?: return
162         isShowing = false
163         val alphaAnim = ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA,
164                 gradientDrawable.alpha, 0)
165         alphaAnim.setDuration(DISMISS_SCRIM_FADE_MS)
166         alphaAnim.start()
167         animator
168             .spring(DynamicAnimation.TRANSLATION_Y, height.toFloat(),
169                 spring)
170             .withEndActions({
171                 visibility = View.INVISIBLE
172                 circle.scaleX = 1f
173                 circle.scaleY = 1f
174             })
175             .start()
176     }
177 
178     /**
179      * Cancels the animator for the dismiss target.
180      */
cancelAnimatorsnull181     fun cancelAnimators() {
182         animator.cancel()
183     }
184 
updateResourcesnull185     fun updateResources() {
186         val config = checkExists(config) ?: return
187         updatePadding()
188         layoutParams.height = resources.getDimensionPixelSize(config.floatingGradientHeightResId)
189         val targetSize = resources.getDimensionPixelSize(config.targetSizeResId)
190         circle.layoutParams.width = targetSize
191         circle.layoutParams.height = targetSize
192         circle.requestLayout()
193     }
194 
createGradientnull195     private fun createGradient(@ColorRes color: Int): GradientDrawable {
196         val gradientColor = ContextCompat.getColor(context, color)
197         val alpha = 0.7f * 255
198         val gradientColorWithAlpha = Color.argb(alpha.toInt(),
199                 Color.red(gradientColor),
200                 Color.green(gradientColor),
201                 Color.blue(gradientColor))
202         val gd = GradientDrawable(
203                 GradientDrawable.Orientation.BOTTOM_TOP,
204                 intArrayOf(gradientColorWithAlpha, Color.TRANSPARENT))
205         gd.setDither(true)
206         gd.setAlpha(0)
207         return gd
208     }
209 
updatePaddingnull210     private fun updatePadding() {
211         val config = checkExists(config) ?: return
212         val insets: WindowInsets = wm.getCurrentWindowMetrics().getWindowInsets()
213         val navInset = insets.getInsetsIgnoringVisibility(
214                 WindowInsets.Type.navigationBars())
215         setPadding(0, 0, 0, navInset.bottom +
216                 resources.getDimensionPixelSize(config.bottomMarginResId))
217     }
218 
219     /**
220      * Checks if the value is set up and exists, if not logs an exception.
221      * Used for convenient logging in case `setup` wasn't called before
222      *
223      * @return value provided as argument
224      */
checkExistsnull225     private fun <T>checkExists(value: T?): T? {
226         if (value == null) Log.e(TAG, SHOULD_SETUP)
227         return value
228     }
229 }
230