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