• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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