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.compose.modifiers
18
19 import androidx.compose.ui.Modifier
20 import androidx.compose.ui.geometry.Size
21 import androidx.compose.ui.graphics.Color
22 import androidx.compose.ui.graphics.Outline
23 import androidx.compose.ui.graphics.RectangleShape
24 import androidx.compose.ui.graphics.Shape
25 import androidx.compose.ui.graphics.drawOutline
26 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
27 import androidx.compose.ui.node.DrawModifierNode
28 import androidx.compose.ui.node.ModifierNodeElement
29 import androidx.compose.ui.node.ObserverModifierNode
30 import androidx.compose.ui.node.invalidateDraw
31 import androidx.compose.ui.node.observeReads
32 import androidx.compose.ui.platform.InspectorInfo
33 import androidx.compose.ui.platform.debugInspectorInfo
34 import androidx.compose.ui.unit.LayoutDirection
35
36 /**
37 * Draws a background in a given [shape] and with a [color] or [alpha] that can be animated.
38 *
39 * @param color color to paint background with
40 * @param alpha alpha of the background
41 * @param shape desired shape of the background
42 */
animatedBackgroundnull43 fun Modifier.animatedBackground(
44 color: () -> Color,
45 alpha: () -> Float = DefaultAlpha,
46 shape: Shape = RectangleShape,
47 ) =
48 this.then(
49 BackgroundElement(
50 color = color,
51 alpha = alpha,
52 shape = shape,
53 inspectorInfo =
54 debugInspectorInfo {
55 name = "background"
56 value = color
57 properties["color"] = color
58 properties["alpha"] = alpha
59 properties["shape"] = shape
60 },
61 )
62 )
63
<lambda>null64 private val DefaultAlpha = { 1f }
65
66 private class BackgroundElement(
67 private val color: () -> Color,
68 private val alpha: () -> Float,
69 private val shape: Shape,
70 private val inspectorInfo: InspectorInfo.() -> Unit,
71 ) : ModifierNodeElement<BackgroundNode>() {
createnull72 override fun create(): BackgroundNode {
73 return BackgroundNode(color, alpha, shape)
74 }
75
updatenull76 override fun update(node: BackgroundNode) {
77 node.color = color
78 node.alpha = alpha
79 node.shape = shape
80 }
81
inspectablePropertiesnull82 override fun InspectorInfo.inspectableProperties() {
83 inspectorInfo()
84 }
85
hashCodenull86 override fun hashCode(): Int {
87 var result = color.hashCode()
88 result = 31 * result + alpha.hashCode()
89 result = 31 * result + shape.hashCode()
90 return result
91 }
92
equalsnull93 override fun equals(other: Any?): Boolean {
94 val otherModifier = other as? BackgroundElement ?: return false
95 return color == otherModifier.color &&
96 alpha == otherModifier.alpha &&
97 shape == otherModifier.shape
98 }
99 }
100
101 private class BackgroundNode(var color: () -> Color, var alpha: () -> Float, var shape: Shape) :
102 DrawModifierNode, Modifier.Node(), ObserverModifierNode {
103
104 // Naively cache outline calculation if input parameters are the same, we manually observe
105 // reads inside shape#createOutline separately
106 private var lastSize: Size = Size.Unspecified
107 private var lastLayoutDirection: LayoutDirection? = null
108 private var lastOutline: Outline? = null
109 private var lastShape: Shape? = null
110 private var tmpOutline: Outline? = null
111
drawnull112 override fun ContentDrawScope.draw() {
113 if (shape === RectangleShape) {
114 // shortcut to avoid Outline calculation and allocation
115 drawRect()
116 } else {
117 drawOutline()
118 }
119 drawContent()
120 }
121
onObservedReadsChangednull122 override fun onObservedReadsChanged() {
123 // Reset cached properties
124 lastSize = Size.Unspecified
125 lastLayoutDirection = null
126 lastOutline = null
127 lastShape = null
128 // Invalidate draw so we build the cache again - this is needed because observeReads within
129 // the draw scope obscures the state reads from the draw scope's observer
130 invalidateDraw()
131 }
132
ContentDrawScopenull133 private fun ContentDrawScope.drawRect() {
134 drawRect(color = color(), alpha = alpha())
135 }
136
drawOutlinenull137 private fun ContentDrawScope.drawOutline() {
138 val outline = getOutline()
139 drawOutline(outline, color = color(), alpha = alpha())
140 }
141
getOutlinenull142 private fun ContentDrawScope.getOutline(): Outline {
143 val outline: Outline?
144 if (size == lastSize && layoutDirection == lastLayoutDirection && lastShape == shape) {
145 outline = lastOutline!!
146 } else {
147 // Manually observe reads so we can directly invalidate the outline when it changes
148 // Use tmpOutline to avoid creating an object reference to local var outline
149 observeReads { tmpOutline = shape.createOutline(size, layoutDirection, this) }
150 outline = tmpOutline
151 tmpOutline = null
152 }
153 lastOutline = outline
154 lastSize = size
155 lastLayoutDirection = layoutDirection
156 lastShape = shape
157 return outline!!
158 }
159 }
160