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.common.bubbles.DismissCircleView 43 import com.android.wm.shell.common.bubbles.DismissView 44 import com.android.wm.shell.shared.animation.PhysicsAnimator 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) : 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 = 90 context.getSystemService(Context.WINDOW_SERVICE) as WindowManager 91 private var gradientDrawable: GradientDrawable? = null 92 93 private val GRADIENT_ALPHA: IntProperty<GradientDrawable> = 94 object : IntProperty<GradientDrawable>("alpha") { setValuenull95 override fun setValue(d: GradientDrawable, percent: Int) { 96 d.alpha = percent 97 } getnull98 override fun get(d: GradientDrawable): Int { 99 return d.alpha 100 } 101 } 102 103 init { 104 clipToPadding = false 105 clipChildren = false 106 visibility = View.INVISIBLE 107 108 // START DragToInteractView modification 109 // Resources included within implementation as we aren't concerned with decoupling them. 110 setup( 111 Config( 112 targetSizeResId = R.dimen.dismiss_circle_size, 113 iconSizeResId = R.dimen.dismiss_target_x_size, 114 bottomMarginResId = R.dimen.floating_dismiss_bottom_margin, 115 floatingGradientHeightResId = R.dimen.floating_dismiss_gradient_height, 116 floatingGradientColorResId = android.R.color.system_neutral1_900, 117 backgroundResId = R.drawable.dismiss_circle_background, 118 iconResId = R.drawable.pip_ic_close_white 119 ) 120 ) 121 122 // Ensure this is unfocusable & uninteractable 123 isClickable = false 124 isFocusable = false 125 importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO 126 127 // END DragToInteractView modification 128 } 129 130 /** 131 * Sets up view with the provided resource ids. 132 * 133 * Decouples resource dependency in order to be used externally (e.g. Launcher). Usually called 134 * with default params in module specific extension: 135 * 136 * @see [DismissView.setup] in DismissViewExt.kt 137 */ setupnull138 fun setup(config: Config) { 139 this.config = config 140 141 // Setup layout 142 layoutParams = 143 LayoutParams( 144 ViewGroup.LayoutParams.MATCH_PARENT, 145 resources.getDimensionPixelSize(config.floatingGradientHeightResId), 146 Gravity.BOTTOM 147 ) 148 updatePadding() 149 150 // Setup gradient 151 gradientDrawable = createGradient(color = config.floatingGradientColorResId) 152 background = gradientDrawable 153 154 // START DragToInteractView modification 155 156 // Setup LinearLayout. Added to organize multiple circles. 157 val linearLayout = LinearLayout(context) 158 linearLayout.layoutParams = 159 LinearLayout.LayoutParams( 160 ViewGroup.LayoutParams.MATCH_PARENT, 161 ViewGroup.LayoutParams.MATCH_PARENT 162 ) 163 linearLayout.weightSum = 0f 164 addView(linearLayout) 165 166 // Setup DismissCircleView. Code block replaced with repeatable functions 167 addSpace(linearLayout) 168 addCircle( 169 config, 170 com.android.systemui.res.R.id.action_remove_menu, 171 R.drawable.pip_ic_close_white, 172 linearLayout 173 ) 174 addCircle( 175 config, 176 com.android.systemui.res.R.id.action_edit, 177 com.android.systemui.res.R.drawable.ic_screenshot_edit, 178 linearLayout 179 ) 180 // END DragToInteractView modification 181 } 182 183 /** Animates this view in. */ shownull184 fun show() { 185 if (isShowing) return 186 val gradientDrawable = checkExists(gradientDrawable) ?: return 187 isShowing = true 188 visibility = View.VISIBLE 189 val alphaAnim = 190 ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA, gradientDrawable.alpha, 255) 191 alphaAnim.duration = INTERACT_SCRIM_FADE_MS 192 alphaAnim.start() 193 194 // START DragToInteractView modification 195 interactMap.forEach { 196 val animator = it.value.second 197 animator.cancel() 198 animator.spring(DynamicAnimation.TRANSLATION_Y, 0f, spring).start() 199 } 200 // END DragToInteractView modification 201 } 202 203 /** 204 * Animates this view out, as well as the circle that encircles the bubbles, if they were 205 * dragged into the target and encircled. 206 */ hidenull207 fun hide() { 208 if (!isShowing) return 209 val gradientDrawable = checkExists(gradientDrawable) ?: return 210 isShowing = false 211 val alphaAnim = 212 ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA, gradientDrawable.alpha, 0) 213 alphaAnim.duration = INTERACT_SCRIM_FADE_MS 214 alphaAnim.start() 215 216 // START DragToInteractView modification 217 interactMap.forEach { 218 val animator = it.value.second 219 animator 220 .spring(DynamicAnimation.TRANSLATION_Y, height.toFloat(), spring) 221 .withEndActions({ visibility = View.INVISIBLE }) 222 .start() 223 } 224 // END DragToInteractView modification 225 } 226 227 /** Cancels the animator for the dismiss target. */ cancelAnimatorsnull228 fun cancelAnimators() { 229 // START DragToInteractView modification 230 interactMap.forEach { 231 val animator = it.value.second 232 animator.cancel() 233 } 234 // END DragToInteractView modification 235 } 236 updateResourcesnull237 fun updateResources() { 238 val config = checkExists(config) ?: return 239 updatePadding() 240 layoutParams.height = resources.getDimensionPixelSize(config.floatingGradientHeightResId) 241 val targetSize = resources.getDimensionPixelSize(config.targetSizeResId) 242 243 // START DragToInteractView modification 244 interactMap.forEach { 245 val circle = it.value.first 246 circle.layoutParams.width = targetSize 247 circle.layoutParams.height = targetSize 248 circle.requestLayout() 249 } 250 // END DragToInteractView modification 251 } 252 createGradientnull253 private fun createGradient(@ColorRes color: Int): GradientDrawable { 254 val gradientColor = ContextCompat.getColor(context, color) 255 val alpha = 0.7f * 255 256 val gradientColorWithAlpha = 257 Color.argb( 258 alpha.toInt(), 259 Color.red(gradientColor), 260 Color.green(gradientColor), 261 Color.blue(gradientColor) 262 ) 263 val gd = 264 GradientDrawable( 265 GradientDrawable.Orientation.BOTTOM_TOP, 266 intArrayOf(gradientColorWithAlpha, Color.TRANSPARENT) 267 ) 268 gd.setDither(true) 269 gd.alpha = 0 270 return gd 271 } 272 updatePaddingnull273 private fun updatePadding() { 274 val config = checkExists(config) ?: return 275 val insets: WindowInsets = wm.currentWindowMetrics.windowInsets 276 val navInset = insets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()) 277 setPadding( 278 0, 279 0, 280 0, 281 navInset.bottom + resources.getDimensionPixelSize(config.bottomMarginResId) 282 ) 283 } 284 285 /** 286 * Checks if the value is set up and exists, if not logs an exception. Used for convenient 287 * logging in case `setup` wasn't called before 288 * 289 * @return value provided as argument 290 */ checkExistsnull291 private fun <T> checkExists(value: T?): T? { 292 if (value == null) Log.e(TAG, SHOULD_SETUP) 293 return value 294 } 295 296 // START DragToInteractView modification addSpacenull297 private fun addSpace(parent: LinearLayout) { 298 val space = Space(context) 299 // Spaces are weighted equally to space out circles evenly 300 space.layoutParams = 301 LinearLayout.LayoutParams( 302 ViewGroup.LayoutParams.WRAP_CONTENT, 303 ViewGroup.LayoutParams.WRAP_CONTENT, 304 1f 305 ) 306 parent.addView(space) 307 parent.weightSum = parent.weightSum + 1f 308 } 309 addCirclenull310 private fun addCircle(config: Config, id: Int, iconResId: Int, parent: LinearLayout) { 311 val targetSize = resources.getDimensionPixelSize(config.targetSizeResId) 312 val circleLayoutParams = LinearLayout.LayoutParams(targetSize, targetSize, 0f) 313 circleLayoutParams.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL 314 val circle = DismissCircleView(context) 315 circle.id = id 316 circle.setup(config.backgroundResId, iconResId, config.iconSizeResId) 317 circle.layoutParams = circleLayoutParams 318 319 // Initial position with circle offscreen so it's animated up 320 circle.translationY = 321 resources.getDimensionPixelSize(config.floatingGradientHeightResId).toFloat() 322 323 interactMap[circle.id] = Pair(circle, PhysicsAnimator.getInstance(circle)) 324 parent.addView(circle) 325 addSpace(parent) 326 } 327 // END DragToInteractView modification 328 } 329