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