• 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 package com.android.launcher3.taskbar.bubbles
17 
18 import android.content.Context
19 import android.graphics.Canvas
20 import android.graphics.Color
21 import android.graphics.ColorFilter
22 import android.graphics.Matrix
23 import android.graphics.Paint
24 import android.graphics.Path
25 import android.graphics.PixelFormat
26 import android.graphics.drawable.Drawable
27 import com.android.app.animation.Interpolators
28 import com.android.launcher3.R
29 import com.android.launcher3.Utilities
30 import com.android.launcher3.Utilities.mapToRange
31 import com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound
32 import com.android.launcher3.popup.RoundedArrowDrawable
33 import kotlin.math.max
34 import kotlin.math.min
35 
36 /** Drawable for the background of the bubble bar. */
37 class BubbleBarBackground(context: Context, private var backgroundHeight: Float) : Drawable() {
38 
39     private val fillPaint: Paint = Paint()
40     private val strokePaint: Paint = Paint()
41     private val arrowWidth: Float
42     private val arrowHeight: Float
43     private val arrowTipRadius: Float
44     private val arrowVisibleHeight: Float
45 
46     private val strokeAlpha: Int
47     private val strokeColor: Int
48     private val strokeColorDropTarget: Int
49     private val shadowAlpha: Int
50     private val shadowBlur: Float
51     private val keyShadowDistance: Float
52     private var arrowHeightFraction = 1f
53     private var isShowingDropTarget: Boolean = false
54 
55     var arrowPositionX: Float = 0f
56         private set
57 
58     private var showingArrow: Boolean = false
59 
60     var width: Float = 0f
61 
62     /**
63      * Set whether the drawable is anchored to the left or right edge of the container.
64      *
65      * When `anchorLeft` is set to `true`, drawable left edge aligns up with the container left
66      * edge. Drawable can be drawn outside container bounds on the right edge. When it is set to
67      * `false` (the default), drawable right edge aligns up with the container right edge. Drawable
68      * can be drawn outside container bounds on the left edge.
69      */
70     var anchorLeft: Boolean = false
71         set(value) {
72             if (field != value) {
73                 field = value
74                 invalidateSelf()
75             }
76         }
77 
78     /**
79      * Scale of the background in the x direction. Pivot is at the left edge if [anchorLeft] is
80      * `true` and at the right edge if it is `false`
81      */
82     var scaleX: Float = 1f
83         set(value) {
84             if (field != value) {
85                 field = value
86                 invalidateSelf()
87             }
88         }
89 
90     /** Scale of the background in the y direction. Pivot is at the bottom edge. */
91     var scaleY: Float = 1f
92         set(value) {
93             if (field != value) {
94                 field = value
95                 invalidateSelf()
96             }
97         }
98 
99     init {
100         val res = context.resources
101         // configure fill paint
102         fillPaint.color = context.getColor(R.color.taskbar_background)
103         fillPaint.flags = Paint.ANTI_ALIAS_FLAG
104         fillPaint.style = Paint.Style.FILL
105         // configure stroke paint
106         strokeColor = context.getColor(R.color.taskbar_stroke)
107         strokeColorDropTarget = context.getColor(com.android.internal.R.color.system_primary_fixed)
108         strokePaint.color = strokeColor
109         strokePaint.flags = Paint.ANTI_ALIAS_FLAG
110         strokePaint.style = Paint.Style.STROKE
111         strokePaint.strokeWidth = res.getDimension(R.dimen.transient_taskbar_stroke_width)
112         // apply theme alpha attributes
113         if (Utilities.isDarkTheme(context)) {
114             strokeAlpha = DARK_THEME_STROKE_ALPHA
115             shadowAlpha = DARK_THEME_SHADOW_ALPHA
116         } else {
117             strokeAlpha = LIGHT_THEME_STROKE_ALPHA
118             shadowAlpha = LIGHT_THEME_SHADOW_ALPHA
119         }
120         strokePaint.alpha = strokeAlpha
121         shadowBlur = res.getDimension(R.dimen.transient_taskbar_shadow_blur)
122         keyShadowDistance = res.getDimension(R.dimen.transient_taskbar_key_shadow_distance)
123         arrowWidth = res.getDimension(R.dimen.bubblebar_pointer_width)
124         arrowHeight = res.getDimension(R.dimen.bubblebar_pointer_height)
125         arrowVisibleHeight = res.getDimension(R.dimen.bubblebar_pointer_visible_size)
126         arrowTipRadius = res.getDimension(R.dimen.bubblebar_pointer_radius)
127     }
128 
showArrownull129     fun showArrow(show: Boolean) {
130         showingArrow = show
131     }
132 
setArrowPositionnull133     fun setArrowPosition(x: Float) {
134         arrowPositionX = x
135     }
136 
137     /** Draws the background with the given paint and height, on the provided canvas. */
drawnull138     override fun draw(canvas: Canvas) {
139         canvas.save()
140 
141         // Draw shadows.
142         val newShadowAlpha =
143             mapToRange(fillPaint.alpha, 0, 255, 0, shadowAlpha, Interpolators.LINEAR)
144         fillPaint.setShadowLayer(
145             shadowBlur,
146             0f,
147             keyShadowDistance,
148             setColorAlphaBound(Color.BLACK, newShadowAlpha),
149         )
150         // Create background path
151         val backgroundPath = Path()
152         val scaledBackgroundHeight = backgroundHeight * scaleY
153         val scaledWidth = width * scaleX
154         val topOffset = scaledBackgroundHeight - bounds.height().toFloat()
155         val radius = backgroundHeight / 2f
156 
157         val left = bounds.left + (if (anchorLeft) 0f else bounds.width().toFloat() - scaledWidth)
158         val right = bounds.left + (if (anchorLeft) scaledWidth else bounds.width().toFloat())
159         // Calculate top with scaled heights for background and arrow to align with stash handle
160         val top = bounds.bottom - scaledBackgroundHeight + getScaledArrowVisibleHeight()
161         val bottom = bounds.bottom.toFloat()
162 
163         backgroundPath.addRoundRect(left, top, right, bottom, radius, radius, Path.Direction.CW)
164         addArrowPathIfNeeded(backgroundPath, topOffset)
165 
166         // Draw background.
167         canvas.drawPath(backgroundPath, fillPaint)
168         canvas.drawPath(backgroundPath, strokePaint)
169         canvas.restore()
170     }
171 
addArrowPathIfNeedednull172     private fun addArrowPathIfNeeded(sourcePath: Path, topOffset: Float) {
173         if (!showingArrow || arrowHeightFraction <= 0) return
174         val arrowPath = Path()
175         val scaledHeight = getScaledArrowHeight()
176         RoundedArrowDrawable.addDownPointingRoundedTriangleToPath(
177             arrowWidth,
178             scaledHeight,
179             arrowTipRadius,
180             arrowPath,
181         )
182         // flip it horizontally
183         val pathTransform = Matrix()
184         pathTransform.setRotate(180f, arrowWidth * 0.5f, scaledHeight * 0.5f)
185         arrowPath.transform(pathTransform)
186         // shift to arrow position
187         val arrowStart = bounds.left + arrowPositionX - (arrowWidth / 2f)
188         val arrowTop = (1 - arrowHeightFraction) * getScaledArrowVisibleHeight() - topOffset
189         arrowPath.offset(arrowStart, arrowTop)
190         // union with rectangle
191         sourcePath.op(arrowPath, Path.Op.UNION)
192     }
193 
getOpacitynull194     override fun getOpacity(): Int {
195         return when (fillPaint.alpha) {
196             255 -> PixelFormat.OPAQUE
197             0 -> PixelFormat.TRANSPARENT
198             else -> PixelFormat.TRANSLUCENT
199         }
200     }
201 
setAlphanull202     override fun setAlpha(alpha: Int) {
203         fillPaint.alpha = alpha
204         strokePaint.alpha = mapToRange(alpha, 0, 255, 0, strokeAlpha, Interpolators.LINEAR)
205         invalidateSelf()
206     }
207 
getAlphanull208     override fun getAlpha(): Int {
209         return fillPaint.alpha
210     }
211 
setColorFilternull212     override fun setColorFilter(colorFilter: ColorFilter?) {
213         fillPaint.colorFilter = colorFilter
214     }
215 
setBackgroundHeightnull216     fun setBackgroundHeight(newHeight: Float) {
217         backgroundHeight = newHeight
218         invalidateSelf()
219     }
220 
221     /**
222      * Set fraction of the arrow height that should be displayed. Allowed values range are [0..1].
223      * If value passed is out of range it will be converted to the closest value in tha allowed
224      * range.
225      */
setArrowHeightFractionnull226     fun setArrowHeightFraction(arrowHeightFraction: Float) {
227         var newHeightFraction = arrowHeightFraction
228         if (newHeightFraction !in 0f..1f) {
229             newHeightFraction = min(max(newHeightFraction, 0f), 1f)
230         }
231         this.arrowHeightFraction = newHeightFraction
232         invalidateSelf()
233     }
234 
getScaledArrowHeightnull235     private fun getScaledArrowHeight(): Float {
236         return arrowHeight * scaleY
237     }
238 
getScaledArrowVisibleHeightnull239     private fun getScaledArrowVisibleHeight(): Float {
240         return max(0f, getScaledArrowHeight() - (arrowHeight - arrowVisibleHeight))
241     }
242 
243     /** Set whether the background should show the drop target */
showDropTargetnull244     fun showDropTarget(isDropTarget: Boolean) {
245         if (isShowingDropTarget == isDropTarget) {
246             return
247         }
248         isShowingDropTarget = isDropTarget
249         val strokeColor = if (isDropTarget) strokeColorDropTarget else strokeColor
250         val alpha = if (isDropTarget) DRAG_STROKE_ALPHA else strokeAlpha
251         strokePaint.color = strokeColor
252         strokePaint.alpha = alpha
253         invalidateSelf()
254     }
255 
isShowingDropTargetnull256     fun isShowingDropTarget() = isShowingDropTarget
257 
258     companion object {
259         private const val DARK_THEME_STROKE_ALPHA = 51
260         private const val LIGHT_THEME_STROKE_ALPHA = 41
261         private const val DRAG_STROKE_ALPHA = 255
262         private const val DARK_THEME_SHADOW_ALPHA = 51
263         private const val LIGHT_THEME_SHADOW_ALPHA = 25
264     }
265 }
266