Skip to content

Commit bd17187

Browse files
authored
Add custom modifier snippets (#169)
* Add custom modifier snippets * Apply Spotless
1 parent 6bd8781 commit bd17187

File tree

2 files changed

+353
-2
lines changed

2 files changed

+353
-2
lines changed
Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
/*
2+
* Copyright 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+
* https://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.example.compose.snippets.modifiers
18+
19+
import android.annotation.SuppressLint
20+
import androidx.compose.animation.core.Animatable
21+
import androidx.compose.animation.core.DecayAnimationSpec
22+
import androidx.compose.animation.core.RepeatMode
23+
import androidx.compose.animation.core.animateFloatAsState
24+
import androidx.compose.animation.core.infiniteRepeatable
25+
import androidx.compose.animation.core.tween
26+
import androidx.compose.animation.splineBasedDecay
27+
import androidx.compose.foundation.background
28+
import androidx.compose.foundation.layout.Box
29+
import androidx.compose.foundation.layout.padding
30+
import androidx.compose.foundation.shape.RoundedCornerShape
31+
import androidx.compose.material3.LocalContentColor
32+
import androidx.compose.runtime.Composable
33+
import androidx.compose.runtime.CompositionLocalProvider
34+
import androidx.compose.runtime.getValue
35+
import androidx.compose.ui.Modifier
36+
import androidx.compose.ui.graphics.Color
37+
import androidx.compose.ui.graphics.Shape
38+
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
39+
import androidx.compose.ui.graphics.graphicsLayer
40+
import androidx.compose.ui.layout.Measurable
41+
import androidx.compose.ui.layout.MeasureResult
42+
import androidx.compose.ui.layout.MeasureScope
43+
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
44+
import androidx.compose.ui.node.DelegatableNode
45+
import androidx.compose.ui.node.DelegatingNode
46+
import androidx.compose.ui.node.DrawModifierNode
47+
import androidx.compose.ui.node.LayoutModifierNode
48+
import androidx.compose.ui.node.ModifierNodeElement
49+
import androidx.compose.ui.node.ObserverModifierNode
50+
import androidx.compose.ui.node.currentValueOf
51+
import androidx.compose.ui.node.invalidateDraw
52+
import androidx.compose.ui.node.invalidateMeasurement
53+
import androidx.compose.ui.node.observeReads
54+
import androidx.compose.ui.platform.LocalDensity
55+
import androidx.compose.ui.unit.Constraints
56+
import androidx.compose.ui.unit.Density
57+
import androidx.compose.ui.unit.IntSize
58+
import androidx.compose.ui.unit.constrain
59+
import androidx.compose.ui.unit.constrainHeight
60+
import androidx.compose.ui.unit.constrainWidth
61+
import androidx.compose.ui.unit.dp
62+
import androidx.compose.ui.unit.offset
63+
import kotlinx.coroutines.launch
64+
65+
@SuppressLint("ModifierFactoryUnreferencedReceiver") // graphics layer does the reference
66+
// [START android_compose_custom_modifiers_1]
67+
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
68+
// [END android_compose_custom_modifiers_1]
69+
70+
// [START android_compose_custom_modifiers_2]
71+
fun Modifier.myBackground(color: Color) = this then Modifier
72+
.padding(16.dp)
73+
.clip(RoundedCornerShape(8.dp))
74+
.background(color)
75+
// [END android_compose_custom_modifiers_2]
76+
77+
// [START android_compose_custom_modifiers_3]
78+
@Composable
79+
fun Modifier.fade(enable: Boolean): Modifier {
80+
val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f)
81+
return this then Modifier.graphicsLayer { this.alpha = alpha }
82+
}
83+
// [END android_compose_custom_modifiers_3]
84+
85+
// [START android_compose_custom_modifiers_4]
86+
@Composable
87+
fun Modifier.fadedBackground(): Modifier {
88+
val color = LocalContentColor.current
89+
return this then Modifier.background(color.copy(alpha = 0.5f))
90+
}
91+
// [END android_compose_custom_modifiers_4]
92+
93+
private object CustomModifierSnippets5 {
94+
// [START android_compose_custom_modifiers_5]
95+
@Composable
96+
fun Modifier.myBackground(): Modifier {
97+
val color = LocalContentColor.current
98+
return this then Modifier.background(color.copy(alpha = 0.5f))
99+
}
100+
101+
@Composable
102+
fun MyScreen() {
103+
CompositionLocalProvider(LocalContentColor provides Color.Green) {
104+
// Background modifier created with green background
105+
val backgroundModifier = Modifier.myBackground()
106+
107+
// LocalContentColor updated to red
108+
CompositionLocalProvider(LocalContentColor provides Color.Red) {
109+
110+
// Box will have green background, not red as expected.
111+
Box(modifier = backgroundModifier)
112+
}
113+
}
114+
}
115+
// [END android_compose_custom_modifiers_5]
116+
}
117+
118+
// [START android_compose_custom_modifiers_6]
119+
val extractedModifier = Modifier.background(Color.Red) // Hoisted to save allocations
120+
121+
@Composable
122+
fun Modifier.composableModifier(): Modifier {
123+
val color = LocalContentColor.current.copy(alpha = 0.5f)
124+
return this then Modifier.background(color)
125+
}
126+
127+
@Composable
128+
fun MyComposable() {
129+
val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher
130+
}
131+
// [END android_compose_custom_modifiers_6]
132+
133+
// [START android_compose_custom_modifiers_7]
134+
// Modifier.Node
135+
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
136+
override fun ContentDrawScope.draw() {
137+
drawCircle(color)
138+
}
139+
}
140+
// [END android_compose_custom_modifiers_7]
141+
142+
// [START android_compose_custom_modifiers_8]
143+
// ModifierNodeElement
144+
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
145+
override fun create() = CircleNode(color)
146+
147+
override fun update(node: CircleNode) {
148+
node.color = color
149+
}
150+
}
151+
// [END android_compose_custom_modifiers_8]
152+
153+
// [START android_compose_custom_modifiers_9]
154+
// Modifier factory
155+
fun Modifier.circle(color: Color) = this then CircleElement(color)
156+
// [END android_compose_custom_modifiers_9]
157+
158+
private object CustomModifierSnippets10 {
159+
// [START android_compose_custom_modifiers_10]
160+
// Modifier factory
161+
fun Modifier.circle(color: Color) = this then CircleElement(color)
162+
163+
// ModifierNodeElement
164+
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
165+
override fun create() = CircleNode(color)
166+
167+
override fun update(node: CircleNode) {
168+
node.color = color
169+
}
170+
}
171+
172+
// Modifier.Node
173+
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
174+
override fun ContentDrawScope.draw() {
175+
drawCircle(color)
176+
}
177+
}
178+
// [END android_compose_custom_modifiers_10]
179+
}
180+
181+
// [START android_compose_custom_modifiers_11]
182+
fun Modifier.fixedPadding() = this then FixedPaddingElement
183+
184+
data object FixedPaddingElement : ModifierNodeElement<FixedPaddingNode>() {
185+
override fun create() = FixedPaddingNode()
186+
override fun update(node: FixedPaddingNode) {}
187+
}
188+
189+
class FixedPaddingNode : LayoutModifierNode, Modifier.Node() {
190+
private val PADDING = 16.dp
191+
192+
override fun MeasureScope.measure(
193+
measurable: Measurable,
194+
constraints: Constraints
195+
): MeasureResult {
196+
val paddingPx = PADDING.roundToPx()
197+
val horizontal = paddingPx * 2
198+
val vertical = paddingPx * 2
199+
200+
val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
201+
202+
val width = constraints.constrainWidth(placeable.width + horizontal)
203+
val height = constraints.constrainHeight(placeable.height + vertical)
204+
return layout(width, height) {
205+
placeable.place(paddingPx, paddingPx)
206+
}
207+
}
208+
}
209+
// [END android_compose_custom_modifiers_11]
210+
211+
// [START android_compose_custom_modifiers_12]
212+
class BackgroundColorConsumerNode :
213+
Modifier.Node(),
214+
DrawModifierNode,
215+
CompositionLocalConsumerModifierNode {
216+
override fun ContentDrawScope.draw() {
217+
val currentColor = currentValueOf(LocalContentColor)
218+
drawRect(color = currentColor)
219+
drawContent()
220+
}
221+
}
222+
// [END android_compose_custom_modifiers_12]
223+
224+
private object UnityDensity : Density {
225+
override val density: Float
226+
get() = 1f
227+
override val fontScale: Float
228+
get() = 1f
229+
}
230+
data class DefaultFlingBehavior(var flingDecay: DecayAnimationSpec<Density>)
231+
// [START android_compose_custom_modifiers_13]
232+
class ScrollableNode :
233+
Modifier.Node(),
234+
ObserverModifierNode,
235+
CompositionLocalConsumerModifierNode {
236+
237+
// Place holder fling behavior, we'll initialize it when the density is available.
238+
val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity))
239+
240+
override fun onAttach() {
241+
updateDefaultFlingBehavior()
242+
observeReads { currentValueOf(LocalDensity) } // monitor change in Density
243+
}
244+
245+
override fun onObservedReadsChanged() {
246+
// if density changes, update the default fling behavior.
247+
updateDefaultFlingBehavior()
248+
}
249+
250+
private fun updateDefaultFlingBehavior() {
251+
val density = currentValueOf(LocalDensity)
252+
defaultFlingBehavior.flingDecay = splineBasedDecay(density)
253+
}
254+
}
255+
// [END android_compose_custom_modifiers_13]
256+
257+
object CustomModifierSnippets14 {
258+
// [START android_compose_custom_modifiers_14]
259+
class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode {
260+
private val alpha = Animatable(1f)
261+
262+
override fun ContentDrawScope.draw() {
263+
drawCircle(color = color, alpha = alpha.value)
264+
drawContent()
265+
}
266+
267+
override fun onAttach() {
268+
coroutineScope.launch {
269+
alpha.animateTo(
270+
0f,
271+
infiniteRepeatable(tween(1000), RepeatMode.Reverse)
272+
) {
273+
}
274+
}
275+
}
276+
}
277+
// [END android_compose_custom_modifiers_14]
278+
}
279+
280+
class InteractionData
281+
class FocusableNode(val interactionData: InteractionData) : DelegatableNode {
282+
override val node: Modifier.Node
283+
get() = TODO("Not yet implemented")
284+
}
285+
class IndicationNode(val interactionData: InteractionData) : DelegatableNode {
286+
override val node: Modifier.Node
287+
get() = TODO("Not yet implemented")
288+
}
289+
// [START android_compose_custom_modifiers_15]
290+
class ClickableNode : DelegatingNode() {
291+
val interactionData = InteractionData()
292+
val focusableNode = delegate(
293+
FocusableNode(interactionData)
294+
)
295+
val indicationNode = delegate(
296+
IndicationNode(interactionData)
297+
)
298+
}
299+
// [END android_compose_custom_modifiers_15]
300+
301+
class ClickablePointerInputNode(var onClick: () -> Unit) : Modifier.Node(), DelegatableNode {
302+
fun update(onClick: () -> Unit) {
303+
this.onClick = onClick
304+
}
305+
}
306+
// [START android_compose_custom_modifiers_16]
307+
class SampleInvalidatingNode(
308+
var color: Color,
309+
var size: IntSize,
310+
var onClick: () -> Unit
311+
) : DelegatingNode(), LayoutModifierNode, DrawModifierNode {
312+
override val shouldAutoInvalidate: Boolean
313+
get() = false
314+
315+
private val clickableNode = delegate(
316+
ClickablePointerInputNode(onClick)
317+
)
318+
319+
fun update(color: Color, size: IntSize, onClick: () -> Unit) {
320+
if (this.color != color) {
321+
this.color = color
322+
// Only invalidate draw when color changes
323+
invalidateDraw()
324+
}
325+
326+
if (this.size != size) {
327+
this.size = size
328+
// Only invalidate layout when size changes
329+
invalidateMeasurement()
330+
}
331+
332+
// If only onClick changes, we don't need to invalidate anything
333+
clickableNode.update(onClick)
334+
}
335+
336+
override fun ContentDrawScope.draw() {
337+
drawRect(color)
338+
}
339+
340+
override fun MeasureScope.measure(
341+
measurable: Measurable,
342+
constraints: Constraints
343+
): MeasureResult {
344+
val size = constraints.constrain(size)
345+
val placeable = measurable.measure(constraints)
346+
return layout(size.width, size.height) {
347+
placeable.place(0, 0)
348+
}
349+
}
350+
}
351+
// [END android_compose_custom_modifiers_16]

gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ androidxHiltNavigationCompose = "1.0.0"
2121
coil = "2.4.0"
2222
# @keep
2323
compileSdk = "34"
24-
compose-compiler = "1.4.7"
24+
compose-compiler = "1.5.4"
2525
coroutines = "1.7.3"
2626
google-maps = "18.2.0"
2727
gradle-versions = "0.49.0"
2828
hilt = "2.48.1"
2929
junit = "4.13.2"
3030
# @pin Update in conjuction with Compose Compiler
31-
kotlin = "1.8.21"
31+
kotlin = "1.9.20"
3232
ksp = "1.8.0-1.0.9"
3333
maps-compose = "3.1.1"
3434
material = "1.11.0-beta01"

0 commit comments

Comments
 (0)