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 strokeAlpha: Int 47 private val strokeColor: Int 48 private val strokeColorDropTarget: Int 49 private val shadowAlpha: Int 50 private val shadowBlur: Float 51 private val keyShadowDistance: Float 52 private var arrowHeightFraction = 1f 53 private var isShowingDropTarget: Boolean = false 54 55 var arrowPositionX: Float = 0f 56 private set 57 58 private var showingArrow: Boolean = false 59 60 var width: Float = 0f 61 62 /** 63 * Set whether the drawable is anchored to the left or right edge of the container. 64 * 65 * When `anchorLeft` is set to `true`, drawable left edge aligns up with the container left 66 * edge. Drawable can be drawn outside container bounds on the right edge. When it is set to 67 * `false` (the default), drawable right edge aligns up with the container right edge. Drawable 68 * can be drawn outside container bounds on the left edge. 69 */ 70 var anchorLeft: Boolean = false 71 set(value) { 72 if (field != value) { 73 field = value 74 invalidateSelf() 75 } 76 } 77 78 /** 79 * Scale of the background in the x direction. Pivot is at the left edge if [anchorLeft] is 80 * `true` and at the right edge if it is `false` 81 */ 82 var scaleX: Float = 1f 83 set(value) { 84 if (field != value) { 85 field = value 86 invalidateSelf() 87 } 88 } 89 90 /** Scale of the background in the y direction. Pivot is at the bottom edge. */ 91 var scaleY: Float = 1f 92 set(value) { 93 if (field != value) { 94 field = value 95 invalidateSelf() 96 } 97 } 98 99 init { 100 val res = context.resources 101 // configure fill paint 102 fillPaint.color = context.getColor(R.color.taskbar_background) 103 fillPaint.flags = Paint.ANTI_ALIAS_FLAG 104 fillPaint.style = Paint.Style.FILL 105 // configure stroke paint 106 strokeColor = context.getColor(R.color.taskbar_stroke) 107 strokeColorDropTarget = context.getColor(com.android.internal.R.color.system_primary_fixed) 108 strokePaint.color = strokeColor 109 strokePaint.flags = Paint.ANTI_ALIAS_FLAG 110 strokePaint.style = Paint.Style.STROKE 111 strokePaint.strokeWidth = res.getDimension(R.dimen.transient_taskbar_stroke_width) 112 // apply theme alpha attributes 113 if (Utilities.isDarkTheme(context)) { 114 strokeAlpha = DARK_THEME_STROKE_ALPHA 115 shadowAlpha = DARK_THEME_SHADOW_ALPHA 116 } else { 117 strokeAlpha = LIGHT_THEME_STROKE_ALPHA 118 shadowAlpha = LIGHT_THEME_SHADOW_ALPHA 119 } 120 strokePaint.alpha = strokeAlpha 121 shadowBlur = res.getDimension(R.dimen.transient_taskbar_shadow_blur) 122 keyShadowDistance = res.getDimension(R.dimen.transient_taskbar_key_shadow_distance) 123 arrowWidth = res.getDimension(R.dimen.bubblebar_pointer_width) 124 arrowHeight = res.getDimension(R.dimen.bubblebar_pointer_height) 125 arrowVisibleHeight = res.getDimension(R.dimen.bubblebar_pointer_visible_size) 126 arrowTipRadius = res.getDimension(R.dimen.bubblebar_pointer_radius) 127 } 128 showArrownull129 fun showArrow(show: Boolean) { 130 showingArrow = show 131 } 132 setArrowPositionnull133 fun setArrowPosition(x: Float) { 134 arrowPositionX = x 135 } 136 137 /** Draws the background with the given paint and height, on the provided canvas. */ drawnull138 override fun draw(canvas: Canvas) { 139 canvas.save() 140 141 // Draw shadows. 142 val newShadowAlpha = 143 mapToRange(fillPaint.alpha, 0, 255, 0, shadowAlpha, Interpolators.LINEAR) 144 fillPaint.setShadowLayer( 145 shadowBlur, 146 0f, 147 keyShadowDistance, 148 setColorAlphaBound(Color.BLACK, newShadowAlpha), 149 ) 150 // Create background path 151 val backgroundPath = Path() 152 val scaledBackgroundHeight = backgroundHeight * scaleY 153 val scaledWidth = width * scaleX 154 val topOffset = scaledBackgroundHeight - bounds.height().toFloat() 155 val radius = backgroundHeight / 2f 156 157 val left = bounds.left + (if (anchorLeft) 0f else bounds.width().toFloat() - scaledWidth) 158 val right = bounds.left + (if (anchorLeft) scaledWidth else bounds.width().toFloat()) 159 // Calculate top with scaled heights for background and arrow to align with stash handle 160 val top = bounds.bottom - scaledBackgroundHeight + getScaledArrowVisibleHeight() 161 val bottom = bounds.bottom.toFloat() 162 163 backgroundPath.addRoundRect(left, top, right, bottom, radius, radius, Path.Direction.CW) 164 addArrowPathIfNeeded(backgroundPath, topOffset) 165 166 // Draw background. 167 canvas.drawPath(backgroundPath, fillPaint) 168 canvas.drawPath(backgroundPath, strokePaint) 169 canvas.restore() 170 } 171 addArrowPathIfNeedednull172 private fun addArrowPathIfNeeded(sourcePath: Path, topOffset: Float) { 173 if (!showingArrow || arrowHeightFraction <= 0) return 174 val arrowPath = Path() 175 val scaledHeight = getScaledArrowHeight() 176 RoundedArrowDrawable.addDownPointingRoundedTriangleToPath( 177 arrowWidth, 178 scaledHeight, 179 arrowTipRadius, 180 arrowPath, 181 ) 182 // flip it horizontally 183 val pathTransform = Matrix() 184 pathTransform.setRotate(180f, arrowWidth * 0.5f, scaledHeight * 0.5f) 185 arrowPath.transform(pathTransform) 186 // shift to arrow position 187 val arrowStart = bounds.left + arrowPositionX - (arrowWidth / 2f) 188 val arrowTop = (1 - arrowHeightFraction) * getScaledArrowVisibleHeight() - topOffset 189 arrowPath.offset(arrowStart, arrowTop) 190 // union with rectangle 191 sourcePath.op(arrowPath, Path.Op.UNION) 192 } 193 getOpacitynull194 override fun getOpacity(): Int { 195 return when (fillPaint.alpha) { 196 255 -> PixelFormat.OPAQUE 197 0 -> PixelFormat.TRANSPARENT 198 else -> PixelFormat.TRANSLUCENT 199 } 200 } 201 setAlphanull202 override fun setAlpha(alpha: Int) { 203 fillPaint.alpha = alpha 204 strokePaint.alpha = mapToRange(alpha, 0, 255, 0, strokeAlpha, Interpolators.LINEAR) 205 invalidateSelf() 206 } 207 getAlphanull208 override fun getAlpha(): Int { 209 return fillPaint.alpha 210 } 211 setColorFilternull212 override fun setColorFilter(colorFilter: ColorFilter?) { 213 fillPaint.colorFilter = colorFilter 214 } 215 setBackgroundHeightnull216 fun setBackgroundHeight(newHeight: Float) { 217 backgroundHeight = newHeight 218 invalidateSelf() 219 } 220 221 /** 222 * Set fraction of the arrow height that should be displayed. Allowed values range are [0..1]. 223 * If value passed is out of range it will be converted to the closest value in tha allowed 224 * range. 225 */ setArrowHeightFractionnull226 fun setArrowHeightFraction(arrowHeightFraction: Float) { 227 var newHeightFraction = arrowHeightFraction 228 if (newHeightFraction !in 0f..1f) { 229 newHeightFraction = min(max(newHeightFraction, 0f), 1f) 230 } 231 this.arrowHeightFraction = newHeightFraction 232 invalidateSelf() 233 } 234 getScaledArrowHeightnull235 private fun getScaledArrowHeight(): Float { 236 return arrowHeight * scaleY 237 } 238 getScaledArrowVisibleHeightnull239 private fun getScaledArrowVisibleHeight(): Float { 240 return max(0f, getScaledArrowHeight() - (arrowHeight - arrowVisibleHeight)) 241 } 242 243 /** Set whether the background should show the drop target */ showDropTargetnull244 fun showDropTarget(isDropTarget: Boolean) { 245 if (isShowingDropTarget == isDropTarget) { 246 return 247 } 248 isShowingDropTarget = isDropTarget 249 val strokeColor = if (isDropTarget) strokeColorDropTarget else strokeColor 250 val alpha = if (isDropTarget) DRAG_STROKE_ALPHA else strokeAlpha 251 strokePaint.color = strokeColor 252 strokePaint.alpha = alpha 253 invalidateSelf() 254 } 255 isShowingDropTargetnull256 fun isShowingDropTarget() = isShowingDropTarget 257 258 companion object { 259 private const val DARK_THEME_STROKE_ALPHA = 51 260 private const val LIGHT_THEME_STROKE_ALPHA = 41 261 private const val DRAG_STROKE_ALPHA = 255 262 private const val DARK_THEME_SHADOW_ALPHA = 51 263 private const val LIGHT_THEME_SHADOW_ALPHA = 25 264 } 265 } 266