1 /* 2 * Copyright (C) 2023 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.systemui.accessibility.floatingmenu 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.ArrayMap 24 import android.util.IntProperty 25 import android.util.Log 26 import android.view.Gravity 27 import android.view.View 28 import android.view.ViewGroup 29 import android.view.WindowInsets 30 import android.view.WindowManager 31 import android.widget.FrameLayout 32 import android.widget.LinearLayout 33 import android.widget.Space 34 import androidx.annotation.ColorRes 35 import androidx.annotation.DimenRes 36 import androidx.annotation.DrawableRes 37 import androidx.core.content.ContextCompat 38 import androidx.dynamicanimation.animation.DynamicAnimation 39 import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY 40 import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW 41 import com.android.wm.shell.R 42 import com.android.wm.shell.shared.animation.PhysicsAnimator 43 import com.android.wm.shell.shared.bubbles.DismissCircleView 44 import com.android.wm.shell.shared.bubbles.DismissView 45 46 /** 47 * View that handles interactions between DismissCircleView and BubbleStackView. 48 * 49 * @note [setup] method should be called after initialisation 50 */ 51 class DragToInteractView(context: Context, windowManager: WindowManager) : FrameLayout(context) { 52 /** 53 * The configuration is used to provide module specific resource ids 54 * 55 * @see [setup] method 56 */ 57 data class Config( 58 /** dimen resource id of the dismiss target circle view size */ 59 @DimenRes val targetSizeResId: Int, 60 /** dimen resource id of the icon size in the dismiss target */ 61 @DimenRes val iconSizeResId: Int, 62 /** dimen resource id of the bottom margin for the dismiss target */ 63 @DimenRes var bottomMarginResId: Int, 64 /** dimen resource id of the height for dismiss area gradient */ 65 @DimenRes val floatingGradientHeightResId: Int, 66 /** color resource id of the dismiss area gradient color */ 67 @ColorRes val floatingGradientColorResId: Int, 68 /** drawable resource id of the dismiss target background */ 69 @DrawableRes val backgroundResId: Int, 70 /** drawable resource id of the icon for the dismiss target */ 71 @DrawableRes val iconResId: Int 72 ) 73 74 companion object { 75 private const val SHOULD_SETUP = "The view isn't ready. Should be called after `setup`" 76 private val TAG = DragToInteractView::class.simpleName 77 } 78 79 // START DragToInteractView modification 80 // We could technically access each DismissCircleView from their Animator, 81 // but the animators only store a weak reference to their targets. This is safer. 82 var interactMap = ArrayMap<Int, Pair<DismissCircleView, PhysicsAnimator<DismissCircleView>>>() 83 // END DragToInteractView modification 84 var isShowing = false 85 var config: Config? = null 86 87 private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY) 88 private val INTERACT_SCRIM_FADE_MS = 200L 89 private var wm: WindowManager = windowManager 90 private var gradientDrawable: GradientDrawable? = null 91 92 private val GRADIENT_ALPHA: IntProperty<GradientDrawable> = 93 object : IntProperty<GradientDrawable>("alpha") { setValuenull94 override fun setValue(d: GradientDrawable, percent: Int) { 95 d.alpha = percent 96 } getnull97 override fun get(d: GradientDrawable): Int { 98 return d.alpha 99 } 100 } 101 102 init { 103 clipToPadding = false 104 clipChildren = false 105 visibility = View.INVISIBLE 106 107 // START DragToInteractView modification 108 // Resources included within implementation as we aren't concerned with decoupling them. 109 setup( 110 Config( 111 targetSizeResId = R.dimen.dismiss_circle_size, 112 iconSizeResId = R.dimen.dismiss_target_x_size, 113 bottomMarginResId = R.dimen.floating_dismiss_bottom_margin, 114 floatingGradientHeightResId = R.dimen.floating_dismiss_gradient_height, 115 floatingGradientColorResId = android.R.color.system_neutral1_900, 116 backgroundResId = R.drawable.dismiss_circle_background, 117 iconResId = R.drawable.pip_ic_close_white 118 ) 119 ) 120 121 // Ensure this is unfocusable & uninteractable 122 isClickable = false 123 isFocusable = false 124 importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO 125 126 // END DragToInteractView modification 127 } 128 129 /** 130 * Sets up view with the provided resource ids. 131 * 132 * Decouples resource dependency in order to be used externally (e.g. Launcher). Usually called 133 * with default params in module specific extension: 134 * 135 * @see [DismissView.setup] in DismissViewExt.kt 136 */ setupnull137 fun setup(config: Config) { 138 this.config = config 139 140 // Setup layout 141 layoutParams = 142 LayoutParams( 143 ViewGroup.LayoutParams.MATCH_PARENT, 144 resources.getDimensionPixelSize(config.floatingGradientHeightResId), 145 Gravity.BOTTOM 146 ) 147 updatePadding() 148 149 // Setup gradient 150 gradientDrawable = createGradient(color = config.floatingGradientColorResId) 151 background = gradientDrawable 152 153 // START DragToInteractView modification 154 155 // Setup LinearLayout. Added to organize multiple circles. 156 val linearLayout = LinearLayout(context) 157 linearLayout.layoutParams = 158 LinearLayout.LayoutParams( 159 ViewGroup.LayoutParams.MATCH_PARENT, 160 ViewGroup.LayoutParams.MATCH_PARENT 161 ) 162 linearLayout.weightSum = 0f 163 addView(linearLayout) 164 165 // Setup DismissCircleView. Code block replaced with repeatable functions 166 addSpace(linearLayout) 167 addCircle( 168 config, 169 com.android.systemui.res.R.id.action_remove_menu, 170 R.drawable.pip_ic_close_white, 171 linearLayout 172 ) 173 addCircle( 174 config, 175 com.android.systemui.res.R.id.action_edit, 176 com.android.systemui.res.R.drawable.ic_screenshot_edit, 177 linearLayout 178 ) 179 // END DragToInteractView modification 180 } 181 182 /** Animates this view in. */ shownull183 fun show() { 184 if (isShowing) return 185 val gradientDrawable = checkExists(gradientDrawable) ?: return 186 isShowing = true 187 visibility = View.VISIBLE 188 val alphaAnim = 189 ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA, gradientDrawable.alpha, 255) 190 alphaAnim.duration = INTERACT_SCRIM_FADE_MS 191 alphaAnim.start() 192 193 // START DragToInteractView modification 194 interactMap.forEach { 195 val animator = it.value.second 196 animator.cancel() 197 animator.spring(DynamicAnimation.TRANSLATION_Y, 0f, spring).start() 198 } 199 // END DragToInteractView modification 200 } 201 202 /** 203 * Animates this view out, as well as the circle that encircles the bubbles, if they were 204 * dragged into the target and encircled. 205 */ hidenull206 fun hide() { 207 if (!isShowing) return 208 val gradientDrawable = checkExists(gradientDrawable) ?: return 209 isShowing = false 210 val alphaAnim = 211 ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA, gradientDrawable.alpha, 0) 212 alphaAnim.duration = INTERACT_SCRIM_FADE_MS 213 alphaAnim.start() 214 215 // START DragToInteractView modification 216 interactMap.forEach { 217 val animator = it.value.second 218 animator 219 .spring(DynamicAnimation.TRANSLATION_Y, height.toFloat(), spring) 220 .withEndActions({ visibility = View.INVISIBLE }) 221 .start() 222 } 223 // END DragToInteractView modification 224 } 225 226 /** Cancels the animator for the dismiss target. */ cancelAnimatorsnull227 fun cancelAnimators() { 228 // START DragToInteractView modification 229 interactMap.forEach { 230 val animator = it.value.second 231 animator.cancel() 232 } 233 // END DragToInteractView modification 234 } 235 updateResourcesnull236 fun updateResources() { 237 val config = checkExists(config) ?: return 238 updatePadding() 239 layoutParams.height = resources.getDimensionPixelSize(config.floatingGradientHeightResId) 240 val targetSize = resources.getDimensionPixelSize(config.targetSizeResId) 241 242 // START DragToInteractView modification 243 interactMap.forEach { 244 val circle = it.value.first 245 circle.layoutParams.width = targetSize 246 circle.layoutParams.height = targetSize 247 circle.requestLayout() 248 } 249 // END DragToInteractView modification 250 } 251 createGradientnull252 private fun createGradient(@ColorRes color: Int): GradientDrawable { 253 val gradientColor = ContextCompat.getColor(context, color) 254 val alpha = 0.7f * 255 255 val gradientColorWithAlpha = 256 Color.argb( 257 alpha.toInt(), 258 Color.red(gradientColor), 259 Color.green(gradientColor), 260 Color.blue(gradientColor) 261 ) 262 val gd = 263 GradientDrawable( 264 GradientDrawable.Orientation.BOTTOM_TOP, 265 intArrayOf(gradientColorWithAlpha, Color.TRANSPARENT) 266 ) 267 gd.setDither(true) 268 gd.alpha = 0 269 return gd 270 } 271 updatePaddingnull272 private fun updatePadding() { 273 val config = checkExists(config) ?: return 274 val insets: WindowInsets = wm.currentWindowMetrics.windowInsets 275 val navInset = insets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()) 276 setPadding( 277 0, 278 0, 279 0, 280 navInset.bottom + resources.getDimensionPixelSize(config.bottomMarginResId) 281 ) 282 } 283 284 /** 285 * Checks if the value is set up and exists, if not logs an exception. Used for convenient 286 * logging in case `setup` wasn't called before 287 * 288 * @return value provided as argument 289 */ checkExistsnull290 private fun <T> checkExists(value: T?): T? { 291 if (value == null) Log.e(TAG, SHOULD_SETUP) 292 return value 293 } 294 295 // START DragToInteractView modification addSpacenull296 private fun addSpace(parent: LinearLayout) { 297 val space = Space(context) 298 // Spaces are weighted equally to space out circles evenly 299 space.layoutParams = 300 LinearLayout.LayoutParams( 301 ViewGroup.LayoutParams.WRAP_CONTENT, 302 ViewGroup.LayoutParams.WRAP_CONTENT, 303 1f 304 ) 305 parent.addView(space) 306 parent.weightSum = parent.weightSum + 1f 307 } 308 addCirclenull309 private fun addCircle(config: Config, id: Int, iconResId: Int, parent: LinearLayout) { 310 val targetSize = resources.getDimensionPixelSize(config.targetSizeResId) 311 val circleLayoutParams = LinearLayout.LayoutParams(targetSize, targetSize, 0f) 312 circleLayoutParams.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL 313 val circle = DismissCircleView(context) 314 circle.id = id 315 circle.setup(config.backgroundResId, iconResId, config.iconSizeResId) 316 circle.layoutParams = circleLayoutParams 317 318 // Initial position with circle offscreen so it's animated up 319 circle.translationY = 320 resources.getDimensionPixelSize(config.floatingGradientHeightResId).toFloat() 321 322 interactMap[circle.id] = Pair(circle, PhysicsAnimator.getInstance(circle)) 323 parent.addView(circle) 324 addSpace(parent) 325 } 326 // END DragToInteractView modification 327 } 328