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 shadowAlpha: Float
47     private var shadowBlur = 0f
48     private var keyShadowDistance = 0f
49     private var arrowHeightFraction = 1f
50 
51     var arrowPositionX: Float = 0f
52         private set
53 
54     private var showingArrow: Boolean = false
55 
56     var width: Float = 0f
57 
58     /**
59      * Set whether the drawable is anchored to the left or right edge of the container.
60      *
61      * When `anchorLeft` is set to `true`, drawable left edge aligns up with the container left
62      * edge. Drawable can be drawn outside container bounds on the right edge. When it is set to
63      * `false` (the default), drawable right edge aligns up with the container right edge. Drawable
64      * can be drawn outside container bounds on the left edge.
65      */
66     var anchorLeft: Boolean = false
67         set(value) {
68             if (field != value) {
69                 field = value
70                 invalidateSelf()
71             }
72         }
73 
74     init {
75         val res = context.resources
76         // configure fill paint
77         fillPaint.color = context.getColor(R.color.taskbar_background)
78         fillPaint.flags = Paint.ANTI_ALIAS_FLAG
79         fillPaint.style = Paint.Style.FILL
80         // configure stroke paint
81         strokePaint.color = context.getColor(R.color.taskbar_stroke)
82         strokePaint.flags = Paint.ANTI_ALIAS_FLAG
83         strokePaint.style = Paint.Style.STROKE
84         strokePaint.strokeWidth = res.getDimension(R.dimen.transient_taskbar_stroke_width)
85         // apply theme alpha attributes
86         if (Utilities.isDarkTheme(context)) {
87             strokePaint.alpha = DARK_THEME_STROKE_ALPHA
88             shadowAlpha = DARK_THEME_SHADOW_ALPHA
89         } else {
90             strokePaint.alpha = LIGHT_THEME_STROKE_ALPHA
91             shadowAlpha = LIGHT_THEME_SHADOW_ALPHA
92         }
93 
94         shadowBlur = res.getDimension(R.dimen.transient_taskbar_shadow_blur)
95         keyShadowDistance = res.getDimension(R.dimen.transient_taskbar_key_shadow_distance)
96         arrowWidth = res.getDimension(R.dimen.bubblebar_pointer_width)
97         arrowHeight = res.getDimension(R.dimen.bubblebar_pointer_height)
98         arrowVisibleHeight = res.getDimension(R.dimen.bubblebar_pointer_visible_size)
99         arrowTipRadius = res.getDimension(R.dimen.bubblebar_pointer_radius)
100     }
101 
showArrownull102     fun showArrow(show: Boolean) {
103         showingArrow = show
104     }
105 
setArrowPositionnull106     fun setArrowPosition(x: Float) {
107         arrowPositionX = x
108     }
109 
110     /** Draws the background with the given paint and height, on the provided canvas. */
drawnull111     override fun draw(canvas: Canvas) {
112         canvas.save()
113 
114         // TODO (b/277359345): Should animate the alpha similar to taskbar (see TaskbarDragLayer)
115         // Draw shadows.
116         val newShadowAlpha =
117             mapToRange(fillPaint.alpha.toFloat(), 0f, 255f, 0f, shadowAlpha, Interpolators.LINEAR)
118         fillPaint.setShadowLayer(
119             shadowBlur,
120             0f,
121             keyShadowDistance,
122             setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha))
123         )
124         // Create background path
125         val backgroundPath = Path()
126         val topOffset = backgroundHeight - bounds.height().toFloat()
127         val radius = backgroundHeight / 2f
128         val left = bounds.left + (if (anchorLeft) 0f else bounds.width().toFloat() - width)
129         val right = bounds.left + (if (anchorLeft) width else bounds.width().toFloat())
130         val top = bounds.top - topOffset + arrowVisibleHeight
131 
132         val bottom = bounds.top + bounds.height().toFloat()
133         backgroundPath.addRoundRect(left, top, right, bottom, radius, radius, Path.Direction.CW)
134         addArrowPathIfNeeded(backgroundPath, topOffset)
135 
136         // Draw background.
137         canvas.drawPath(backgroundPath, fillPaint)
138         canvas.drawPath(backgroundPath, strokePaint)
139         canvas.restore()
140     }
141 
addArrowPathIfNeedednull142     private fun addArrowPathIfNeeded(sourcePath: Path, topOffset: Float) {
143         if (!showingArrow || arrowHeightFraction <= 0) return
144         val arrowPath = Path()
145         RoundedArrowDrawable.addDownPointingRoundedTriangleToPath(
146             arrowWidth,
147             arrowHeight,
148             arrowTipRadius,
149             arrowPath
150         )
151         // flip it horizontally
152         val pathTransform = Matrix()
153         pathTransform.setRotate(180f, arrowWidth * 0.5f, arrowHeight * 0.5f)
154         arrowPath.transform(pathTransform)
155         // shift to arrow position
156         val arrowStart = bounds.left + arrowPositionX - (arrowWidth / 2f)
157         val arrowTop = (1 - arrowHeightFraction) * arrowVisibleHeight - topOffset
158         arrowPath.offset(arrowStart, arrowTop)
159         // union with rectangle
160         sourcePath.op(arrowPath, Path.Op.UNION)
161     }
162 
getOpacitynull163     override fun getOpacity(): Int {
164         return when (fillPaint.alpha) {
165             255 -> PixelFormat.OPAQUE
166             0 -> PixelFormat.TRANSPARENT
167             else -> PixelFormat.TRANSLUCENT
168         }
169     }
170 
setAlphanull171     override fun setAlpha(alpha: Int) {
172         fillPaint.alpha = alpha
173         invalidateSelf()
174     }
175 
getAlphanull176     override fun getAlpha(): Int {
177         return fillPaint.alpha
178     }
179 
setColorFilternull180     override fun setColorFilter(colorFilter: ColorFilter?) {
181         fillPaint.colorFilter = colorFilter
182     }
183 
setBackgroundHeightnull184     fun setBackgroundHeight(newHeight: Float) {
185         backgroundHeight = newHeight
186     }
187 
188     /**
189      * Set fraction of the arrow height that should be displayed. Allowed values range are [0..1].
190      * If value passed is out of range it will be converted to the closest value in tha allowed
191      * range.
192      */
setArrowHeightFractionnull193     fun setArrowHeightFraction(arrowHeightFraction: Float) {
194         var newHeightFraction = arrowHeightFraction
195         if (newHeightFraction !in 0f..1f) {
196             newHeightFraction = min(max(newHeightFraction, 0f), 1f)
197         }
198         this.arrowHeightFraction = newHeightFraction
199         invalidateSelf()
200     }
201 
202     companion object {
203         private const val DARK_THEME_STROKE_ALPHA = 51
204         private const val LIGHT_THEME_STROKE_ALPHA = 41
205         private const val DARK_THEME_SHADOW_ALPHA = 51f
206         private const val LIGHT_THEME_SHADOW_ALPHA = 25f
207     }
208 }
209