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