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