• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.launcher3.taskbar
18 
19 import android.content.res.Resources
20 import android.graphics.Canvas
21 import android.graphics.Color
22 import android.graphics.Paint
23 import android.graphics.Path
24 import android.graphics.RectF
25 import com.android.app.animation.Interpolators
26 import com.android.launcher3.R
27 import com.android.launcher3.Utilities
28 import com.android.launcher3.Utilities.mapRange
29 import com.android.launcher3.Utilities.mapToRange
30 import com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound
31 import com.android.launcher3.taskbar.TaskbarPinningController.Companion.PINNING_PERSISTENT
32 import com.android.launcher3.taskbar.TaskbarPinningController.Companion.PINNING_TRANSIENT
33 import kotlin.math.min
34 
35 /** Helps draw the taskbar background, made up of a rectangle plus two inverted rounded corners. */
36 class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) {
37 
38     private val isInSetup: Boolean = !context.isUserSetupComplete
39 
40     private val maxTransientTaskbarHeight =
41         context.transientTaskbarDeviceProfile.taskbarHeight.toFloat()
42     private val maxPersistentTaskbarHeight =
43         context.persistentTaskbarDeviceProfile.taskbarHeight.toFloat()
44     var backgroundProgress =
45         if (context.isTransientTaskbar) {
46             PINNING_TRANSIENT
47         } else {
48             PINNING_PERSISTENT
49         }
50 
51     var isAnimatingPinning = false
52     var isAnimatingPersistentTaskbar = false
53     var isAnimatingTransientTaskbar = false
54 
55     val paint = Paint()
56     private val strokePaint = Paint()
57     val lastDrawnTransientRect = RectF()
58     var backgroundHeight = context.deviceProfile.taskbarHeight.toFloat()
59     var translationYForSwipe = 0f
60     var translationYForStash = 0f
61     var translationXForBubbleBar = 0f
62 
63     private val transientBackgroundBounds = context.transientTaskbarBounds
64 
65     private val shadowAlpha: Float
66     private val strokeAlpha: Int
67     private var shadowBlur = 0f
68     private var keyShadowDistance = 0f
69     private var bottomMargin = 0
70 
71     private val fullCornerRadius: Float
72     private var cornerRadius = 0f
73     private var widthInsetPercentage = 0f
74     private val square = Path()
75     private val circle = Path()
76     private val invertedLeftCornerPath = Path()
77     private val invertedRightCornerPath = Path()
78 
79     private var stashedHandleWidth =
80         context.resources.getDimensionPixelSize(R.dimen.taskbar_stashed_handle_width)
81 
82     private val stashedHandleHeight =
83         context.resources.getDimensionPixelSize(R.dimen.taskbar_stashed_handle_height)
84 
85     init {
86         paint.color = context.getColor(R.color.taskbar_background)
87         paint.flags = Paint.ANTI_ALIAS_FLAG
88         paint.style = Paint.Style.FILL
89         strokePaint.color = context.getColor(R.color.taskbar_stroke)
90         strokePaint.flags = Paint.ANTI_ALIAS_FLAG
91         strokePaint.style = Paint.Style.STROKE
92         strokePaint.strokeWidth =
93             context.resources.getDimension(R.dimen.transient_taskbar_stroke_width)
94         if (Utilities.isDarkTheme(context)) {
95             strokeAlpha = DARK_THEME_STROKE_ALPHA
96             shadowAlpha = DARK_THEME_SHADOW_ALPHA
97         } else {
98             strokeAlpha = LIGHT_THEME_STROKE_ALPHA
99             shadowAlpha = LIGHT_THEME_SHADOW_ALPHA
100         }
101 
102         fullCornerRadius = context.cornerRadius.toFloat()
103         cornerRadius = fullCornerRadius
104         if (!context.isInDesktopMode()) {
105             setCornerRoundness(MAX_ROUNDNESS)
106         }
107     }
108 
updateStashedHandleWidthnull109     fun updateStashedHandleWidth(context: TaskbarActivityContext, res: Resources) {
110         stashedHandleWidth =
111             res.getDimensionPixelSize(
112                 if (context.isPhoneMode || context.isTinyTaskbar || context.isBubbleBarOnPhone) {
113                     R.dimen.taskbar_stashed_small_screen
114                 } else {
115                     R.dimen.taskbar_stashed_handle_width
116                 }
117             )
118     }
119 
120     /**
121      * Sets the roundness of the round corner above Taskbar. No effect on transient Taskbar.
122      *
123      * @param cornerRoundness 0 has no round corner, 1 has complete round corner.
124      */
setCornerRoundnessnull125     fun setCornerRoundness(cornerRoundness: Float) {
126         if (context.isTransientTaskbar && !transientBackgroundBounds.isEmpty) {
127             return
128         }
129 
130         cornerRadius = fullCornerRadius * cornerRoundness
131 
132         // Create the paths for the inverted rounded corners above the taskbar. Start with a filled
133         // square, and then subtract out a circle from the appropriate corner.
134         square.reset()
135         square.addRect(0f, 0f, cornerRadius, cornerRadius, Path.Direction.CW)
136         circle.reset()
137         circle.addCircle(cornerRadius, 0f, cornerRadius, Path.Direction.CW)
138         invertedLeftCornerPath.op(square, circle, Path.Op.DIFFERENCE)
139 
140         circle.reset()
141         circle.addCircle(0f, 0f, cornerRadius, Path.Direction.CW)
142         invertedRightCornerPath.op(square, circle, Path.Op.DIFFERENCE)
143     }
144 
145     /** Draws the background with the given paint and height, on the provided canvas. */
drawnull146     fun draw(canvas: Canvas) {
147         if (isInSetup) return
148         val isTransientTaskbar = context.isTransientTaskbar
149         canvas.save()
150         if (!isTransientTaskbar || transientBackgroundBounds.isEmpty || isAnimatingPinning) {
151             drawPersistentBackground(canvas)
152         }
153         canvas.restore()
154         canvas.save()
155         if (isAnimatingPinning || isTransientTaskbar) {
156             drawTransientBackground(canvas)
157         }
158         canvas.restore()
159     }
160 
drawPersistentBackgroundnull161     private fun drawPersistentBackground(canvas: Canvas) {
162         if (isAnimatingPinning || isAnimatingPersistentTaskbar) {
163             val persistentTaskbarHeight = maxPersistentTaskbarHeight * backgroundProgress
164             canvas.translate(0f, canvas.height - persistentTaskbarHeight)
165             // Draw the background behind taskbar content.
166             canvas.drawRect(0f, 0f, canvas.width.toFloat(), persistentTaskbarHeight, paint)
167         } else {
168             val persistentTaskbarHeight = min(maxPersistentTaskbarHeight, backgroundHeight)
169             canvas.translate(0f, canvas.height - persistentTaskbarHeight)
170             // Draw the background behind taskbar content.
171             canvas.drawRect(0f, 0f, canvas.width.toFloat(), persistentTaskbarHeight, paint)
172         }
173 
174         // Draw the inverted rounded corners above the taskbar.
175         canvas.translate(0f, -cornerRadius)
176         canvas.drawPath(invertedLeftCornerPath, paint)
177         canvas.translate(0f, cornerRadius)
178         canvas.translate(canvas.width - cornerRadius, -cornerRadius)
179         canvas.drawPath(invertedRightCornerPath, paint)
180     }
181 
drawTransientBackgroundnull182     private fun drawTransientBackground(canvas: Canvas) {
183         val res = context.resources
184         val transientTaskbarHeight = maxTransientTaskbarHeight * (1f - backgroundProgress)
185         val isAnimating = isAnimatingPinning || isAnimatingTransientTaskbar
186         val heightProgressWhileAnimating =
187             if (isAnimating) transientTaskbarHeight else backgroundHeight
188 
189         var progress = heightProgressWhileAnimating / maxTransientTaskbarHeight
190         progress = Math.round(progress * 100f) / 100f
191         if (isAnimating) {
192             var scale = transientTaskbarHeight / maxTransientTaskbarHeight
193             scale = Math.round(scale * 100f) / 100f
194             bottomMargin =
195                 mapRange(
196                         scale,
197                         0f,
198                         res.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin).toFloat(),
199                     )
200                     .toInt()
201             shadowBlur =
202                 mapRange(scale, 0f, res.getDimension(R.dimen.transient_taskbar_shadow_blur))
203             keyShadowDistance =
204                 mapRange(scale, 0f, res.getDimension(R.dimen.transient_taskbar_key_shadow_distance))
205         } else {
206             bottomMargin = res.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin)
207             shadowBlur = res.getDimension(R.dimen.transient_taskbar_shadow_blur)
208             keyShadowDistance = res.getDimension(R.dimen.transient_taskbar_key_shadow_distance)
209         }
210 
211         // At progress 0, we draw the background as the stashed handle.
212         // At progress 1, we draw the background as the full taskbar.
213         // Min height capped to max persistent taskbar height for animation
214         val backgroundHeightWhileAnimating =
215             if (isAnimatingPinning) maxPersistentTaskbarHeight else stashedHandleHeight.toFloat()
216         val newBackgroundHeight =
217             mapRange(progress, backgroundHeightWhileAnimating, maxTransientTaskbarHeight)
218         val fullWidth = transientBackgroundBounds.width()
219         val animationWidth = context.currentTaskbarWidth
220         val backgroundWidthWhileAnimating =
221             if (isAnimatingPinning) animationWidth else stashedHandleWidth.toFloat()
222 
223         val newWidth = mapRange(progress, backgroundWidthWhileAnimating, fullWidth.toFloat())
224         val halfWidthDelta = (fullWidth - newWidth) / 2f
225         val radius = newBackgroundHeight / 2f
226         val bottomMarginProgress = bottomMargin * ((1f - progress) / 2f)
227 
228         // Aligns the bottom with the bottom of the stashed handle.
229         val bottom =
230             canvas.height - bottomMargin +
231                 bottomMarginProgress +
232                 translationYForSwipe +
233                 translationYForStash +
234                 -mapRange(
235                     1f - progress,
236                     0f,
237                     if (isAnimatingPinning) 0f else stashedHandleHeight / 2f,
238                 )
239 
240         // Draw shadow.
241         val newShadowAlpha =
242             mapToRange(paint.alpha.toFloat(), 0f, 255f, 0f, shadowAlpha, Interpolators.LINEAR)
243         paint.setShadowLayer(
244             shadowBlur,
245             0f,
246             keyShadowDistance,
247             setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha)),
248         )
249         strokePaint.alpha = (paint.alpha * strokeAlpha) / 255
250         val currentTranslationX = translationXForBubbleBar * progress
251         lastDrawnTransientRect.set(
252             transientBackgroundBounds.left + halfWidthDelta + currentTranslationX,
253             bottom - newBackgroundHeight,
254             transientBackgroundBounds.right - halfWidthDelta + currentTranslationX,
255             bottom,
256         )
257         val horizontalInset = fullWidth * widthInsetPercentage
258         lastDrawnTransientRect.inset(horizontalInset, 0f)
259 
260         canvas.drawRoundRect(lastDrawnTransientRect, radius, radius, paint)
261         canvas.drawRoundRect(lastDrawnTransientRect, radius, radius, strokePaint)
262     }
263 
264     /**
265      * Sets the width percentage to inset the transient taskbar's background from the left and from
266      * the right.
267      */
setBackgroundHorizontalInsetsnull268     fun setBackgroundHorizontalInsets(insetPercentage: Float) {
269         widthInsetPercentage = insetPercentage
270     }
271 
272     companion object {
273         const val MAX_ROUNDNESS = 1f
274         private const val DARK_THEME_STROKE_ALPHA = 51
275         private const val LIGHT_THEME_STROKE_ALPHA = 41
276         private const val DARK_THEME_SHADOW_ALPHA = 51f
277         private const val LIGHT_THEME_SHADOW_ALPHA = 25f
278     }
279 }
280