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 shadowAlpha: Float 47 private var shadowBlur = 0f 48 private var keyShadowDistance = 0f 49 private var arrowHeightFraction = 1f 50 51 var arrowPositionX: Float = 0f 52 private set 53 54 private var showingArrow: Boolean = false 55 56 var width: Float = 0f 57 58 /** 59 * Set whether the drawable is anchored to the left or right edge of the container. 60 * 61 * When `anchorLeft` is set to `true`, drawable left edge aligns up with the container left 62 * edge. Drawable can be drawn outside container bounds on the right edge. When it is set to 63 * `false` (the default), drawable right edge aligns up with the container right edge. Drawable 64 * can be drawn outside container bounds on the left edge. 65 */ 66 var anchorLeft: Boolean = false 67 set(value) { 68 if (field != value) { 69 field = value 70 invalidateSelf() 71 } 72 } 73 74 init { 75 val res = context.resources 76 // configure fill paint 77 fillPaint.color = context.getColor(R.color.taskbar_background) 78 fillPaint.flags = Paint.ANTI_ALIAS_FLAG 79 fillPaint.style = Paint.Style.FILL 80 // configure stroke paint 81 strokePaint.color = context.getColor(R.color.taskbar_stroke) 82 strokePaint.flags = Paint.ANTI_ALIAS_FLAG 83 strokePaint.style = Paint.Style.STROKE 84 strokePaint.strokeWidth = res.getDimension(R.dimen.transient_taskbar_stroke_width) 85 // apply theme alpha attributes 86 if (Utilities.isDarkTheme(context)) { 87 strokePaint.alpha = DARK_THEME_STROKE_ALPHA 88 shadowAlpha = DARK_THEME_SHADOW_ALPHA 89 } else { 90 strokePaint.alpha = LIGHT_THEME_STROKE_ALPHA 91 shadowAlpha = LIGHT_THEME_SHADOW_ALPHA 92 } 93 94 shadowBlur = res.getDimension(R.dimen.transient_taskbar_shadow_blur) 95 keyShadowDistance = res.getDimension(R.dimen.transient_taskbar_key_shadow_distance) 96 arrowWidth = res.getDimension(R.dimen.bubblebar_pointer_width) 97 arrowHeight = res.getDimension(R.dimen.bubblebar_pointer_height) 98 arrowVisibleHeight = res.getDimension(R.dimen.bubblebar_pointer_visible_size) 99 arrowTipRadius = res.getDimension(R.dimen.bubblebar_pointer_radius) 100 } 101 showArrownull102 fun showArrow(show: Boolean) { 103 showingArrow = show 104 } 105 setArrowPositionnull106 fun setArrowPosition(x: Float) { 107 arrowPositionX = x 108 } 109 110 /** Draws the background with the given paint and height, on the provided canvas. */ drawnull111 override fun draw(canvas: Canvas) { 112 canvas.save() 113 114 // TODO (b/277359345): Should animate the alpha similar to taskbar (see TaskbarDragLayer) 115 // Draw shadows. 116 val newShadowAlpha = 117 mapToRange(fillPaint.alpha.toFloat(), 0f, 255f, 0f, shadowAlpha, Interpolators.LINEAR) 118 fillPaint.setShadowLayer( 119 shadowBlur, 120 0f, 121 keyShadowDistance, 122 setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha)) 123 ) 124 // Create background path 125 val backgroundPath = Path() 126 val topOffset = backgroundHeight - bounds.height().toFloat() 127 val radius = backgroundHeight / 2f 128 val left = bounds.left + (if (anchorLeft) 0f else bounds.width().toFloat() - width) 129 val right = bounds.left + (if (anchorLeft) width else bounds.width().toFloat()) 130 val top = bounds.top - topOffset + arrowVisibleHeight 131 132 val bottom = bounds.top + bounds.height().toFloat() 133 backgroundPath.addRoundRect(left, top, right, bottom, radius, radius, Path.Direction.CW) 134 addArrowPathIfNeeded(backgroundPath, topOffset) 135 136 // Draw background. 137 canvas.drawPath(backgroundPath, fillPaint) 138 canvas.drawPath(backgroundPath, strokePaint) 139 canvas.restore() 140 } 141 addArrowPathIfNeedednull142 private fun addArrowPathIfNeeded(sourcePath: Path, topOffset: Float) { 143 if (!showingArrow || arrowHeightFraction <= 0) return 144 val arrowPath = Path() 145 RoundedArrowDrawable.addDownPointingRoundedTriangleToPath( 146 arrowWidth, 147 arrowHeight, 148 arrowTipRadius, 149 arrowPath 150 ) 151 // flip it horizontally 152 val pathTransform = Matrix() 153 pathTransform.setRotate(180f, arrowWidth * 0.5f, arrowHeight * 0.5f) 154 arrowPath.transform(pathTransform) 155 // shift to arrow position 156 val arrowStart = bounds.left + arrowPositionX - (arrowWidth / 2f) 157 val arrowTop = (1 - arrowHeightFraction) * arrowVisibleHeight - topOffset 158 arrowPath.offset(arrowStart, arrowTop) 159 // union with rectangle 160 sourcePath.op(arrowPath, Path.Op.UNION) 161 } 162 getOpacitynull163 override fun getOpacity(): Int { 164 return when (fillPaint.alpha) { 165 255 -> PixelFormat.OPAQUE 166 0 -> PixelFormat.TRANSPARENT 167 else -> PixelFormat.TRANSLUCENT 168 } 169 } 170 setAlphanull171 override fun setAlpha(alpha: Int) { 172 fillPaint.alpha = alpha 173 invalidateSelf() 174 } 175 getAlphanull176 override fun getAlpha(): Int { 177 return fillPaint.alpha 178 } 179 setColorFilternull180 override fun setColorFilter(colorFilter: ColorFilter?) { 181 fillPaint.colorFilter = colorFilter 182 } 183 setBackgroundHeightnull184 fun setBackgroundHeight(newHeight: Float) { 185 backgroundHeight = newHeight 186 } 187 188 /** 189 * Set fraction of the arrow height that should be displayed. Allowed values range are [0..1]. 190 * If value passed is out of range it will be converted to the closest value in tha allowed 191 * range. 192 */ setArrowHeightFractionnull193 fun setArrowHeightFraction(arrowHeightFraction: Float) { 194 var newHeightFraction = arrowHeightFraction 195 if (newHeightFraction !in 0f..1f) { 196 newHeightFraction = min(max(newHeightFraction, 0f), 1f) 197 } 198 this.arrowHeightFraction = newHeightFraction 199 invalidateSelf() 200 } 201 202 companion object { 203 private const val DARK_THEME_STROKE_ALPHA = 51 204 private const val LIGHT_THEME_STROKE_ALPHA = 41 205 private const val DARK_THEME_SHADOW_ALPHA = 51f 206 private const val LIGHT_THEME_SHADOW_ALPHA = 25f 207 } 208 } 209