Slide 1

Slide 1 text

Custom Drawing in Compose Rebecca Franks (she/her) Developer Relations Engineer @riggaroo

Slide 2

Slide 2 text

goo.gle/compose-jetlagged-talk

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

goo.gle/compose-jetlagged

Slide 5

Slide 5 text

• Offset + Size of each rect is based on duration of sleep period • Rounded Rectangles joined together with lines in between periods • Gradient across the expanded bars Custom Drawing Sleep Score Emoji Sleep Periods with different durations

Slide 6

Slide 6 text

Drawing in Compose Canvas( onDraw = { drawRoundRect(Color.Blue) } ) Canvas Composable Modifiers Modifier.drawBehind { } Modifier.drawWithContent { } Modifier.drawWithCache { }

Slide 7

Slide 7 text

Canvas( onDraw = { drawRoundRect(Color.Blue) } ) Canvas Composable

Slide 8

Slide 8 text

@Composable fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) = Spacer(modifier.drawBehind(onDraw)) Canvas implementation Just a spacer 🕵

Slide 9

Slide 9 text

Drawing in Compose 🎨 Draws behind the Composables content. Can specify the order of draw calls. Modifier.drawBehind Modifier.drawWithContent Objects that are used during drawing are cached, until size changes or state variables read inside change. Modifier.drawWithCache

Slide 10

Slide 10 text

• Top left has the coordinate of [0,0] • Bottom right has the coordinate of [width, height]. • Draw calls are performed with pixel measurements (not dp). • Relative to the parent Composable Co-ordinate System [width,height] [0,0] X axis Y axis x y [0,height] [width,0]

Slide 11

Slide 11 text

@Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier ) { Spacer( modifier = modifier .drawWithCache { onDrawBehind { // this: DrawScope } } .fillMaxWidth() .height(48.dp) ) }

Slide 12

Slide 12 text

DrawScope Declarative, stateless drawing API to draw shapes, paths etc without needing to maintain your own state.

Slide 13

Slide 13 text

interface DrawScope { val center: Offset val size: Size fun drawLine() fun drawRect() fun drawImage() fun drawRoundRect() fun drawPath() // etc }

Slide 14

Slide 14 text

@Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier ) { Spacer( modifier = modifier .drawWithCache { onDrawBehind { // this: DrawScope } } .fillMaxWidth() .height(24.dp) ) }

Slide 15

Slide 15 text

@Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier ) { Spacer( modifier = modifier .drawWithCache { onDrawBehind { } } .fillMaxWidth() .height(24.dp) ) }

Slide 16

Slide 16 text

@Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier ) { Spacer( modifier = modifier .drawWithCache { val brush = Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/)) onDrawBehind { drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx())) } } .fillMaxWidth() .height(24.dp) ) }

Slide 17

Slide 17 text

fun drawRoundRect( brush: Brush, topLeft: Offset = Offset.Zero, size: Size = this.size.offsetSize(topLeft), cornerRadius: CornerRadius = CornerRadius.Zero, alpha: Float = 1.0f, style: DrawStyle = Fill, colorFilter: ColorFilter? = null, blendMode: BlendMode = DefaultBlendMode ) Default values

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

Draw Text ✍ From Com pose 1.3+

Slide 21

Slide 21 text

@Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier ) { Spacer( modifier = modifier .drawWithCache { val brush = Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/)) onDrawBehind { drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx())) } } .fillMaxWidth() .height(24.dp) ) }

Slide 22

Slide 22 text

@Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier ) { Spacer( modifier = modifier .drawWithCache { val brush = Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/)) onDrawBehind { drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx())) } } .fillMaxWidth() .height(24.dp) ) }

Slide 23

Slide 23 text

@Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier ) { val textMeasurer = rememberTextMeasurer() Spacer( modifier = modifier .drawWithCache { val brush = Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/)) onDrawBehind { drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx())) } } .fillMaxWidth() .height(24.dp) ) }

Slide 24

Slide 24 text

@Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier ) { val textMeasurer = rememberTextMeasurer() Spacer( modifier = modifier .drawWithCache { val brush = Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/)) val textResult = textMeasurer.measure(AnnotatedString("😴")) onDrawBehind { drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx())) } } .fillMaxWidth() .height(24.dp) ) }

Slide 25

Slide 25 text

@Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier ) { val textMeasurer = rememberTextMeasurer() Spacer( modifier = modifier .drawWithCache { val brush = Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/)) val textResult = textMeasurer.measure(AnnotatedString("😴")) onDrawBehind { drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx())) drawText(textResult) } } .fillMaxWidth() .height(24.dp) ) }

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

Animate height on click ✨

Slide 30

Slide 30 text

@Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier ) { val textMeasurer = rememberTextMeasurer() Spacer( modifier = modifier .drawWithCache { // Our drawing code } .fillMaxWidth() .height(24.dp) ) }

Slide 31

Slide 31 text

Spacer( modifier = modifier .drawWithCache { // Our drawing code } .fillMaxWidth() .height(24.dp) ) }

Slide 32

Slide 32 text

var isExpanded by remember { mutableStateOf(false) } Spacer( modifier = modifier .drawWithCache { // Our drawing code } .fillMaxWidth() .height(24.dp) ) }

Slide 33

Slide 33 text

var isExpanded by remember { mutableStateOf(false) } Spacer( modifier = modifier .drawWithCache { // Our drawing code } .fillMaxWidth() .height(24.dp) ) }

Slide 34

Slide 34 text

var isExpanded by remember { mutableStateOf(false) } Spacer( modifier = modifier .drawWithCache { // Our drawing code } .fillMaxWidth() .height(24.dp) .clickable { isExpanded = !isExpanded } ) }

Slide 35

Slide 35 text

var isExpanded by remember { mutableStateOf(false) } Spacer( modifier = modifier .drawWithCache { // Our drawing code } .fillMaxWidth() .height(24.dp) .clickable { isExpanded = !isExpanded } ) }

Slide 36

Slide 36 text

var isExpanded by remember { mutableStateOf(false) } val transition = updateTransition(targetState = isExpanded) val height by transition.animateDp { targetExpanded -> if (targetExpanded) 110.dp else 24.dp } Spacer( modifier = modifier .drawWithCache { // Our drawing code } .fillMaxWidth() .height(24.dp) .clickable { isExpanded = !isExpanded } ) }

Slide 37

Slide 37 text

var isExpanded by remember { mutableStateOf(false) } val transition = updateTransition(targetState = isExpanded) val height by transition.animateDp { targetExpanded -> if (targetExpanded) 110.dp else 24.dp } Spacer( modifier = modifier .drawWithCache { // Our drawing code } .fillMaxWidth() .height(height) .clickable { isExpanded = !isExpanded } ) }

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

Text Offset Animation

Slide 40

Slide 40 text

var isExpanded by remember { mutableStateOf(false) } val transition = updateTransition(targetState = expanded) val height by transition.animateDp { targetExpanded -> if (targetExpanded) 110.dp else 24.dp } Spacer( modifier = Modifier .drawWithCache { val brush = Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/)) val textResult = textMeasurer.measure(AnnotatedString("😴")) onDrawBehind { drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx())) drawText(textResult) } } .fillMaxWidth() .height(height) .clickable {

Slide 41

Slide 41 text

val transition = updateTransition(targetState = expanded) val height by transition.animateDp { targetExpanded -> if (targetExpanded) 110.dp else 24.dp } Spacer( modifier = Modifier .drawWithCache { val brush = Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/)) val textResult = textMeasurer.measure(AnnotatedString("😴")) onDrawBehind { drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx())) drawText(textResult) } } .fillMaxWidth() .height(height) .clickable { expanded = !expanded

Slide 42

Slide 42 text

val transition = updateTransition(targetState = expanded) val height by transition.animateDp { targetExpanded -> if (targetExpanded) 110.dp else 24.dp } val textOffset by transition.animateFloat { targetExpanded -> if (targetExpanded) 1f else 0f } Spacer( modifier = Modifier .drawWithCache { val brush = Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/)) val textResult = textMeasurer.measure(AnnotatedString("😴")) onDrawBehind { drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx())) drawText(textResult) } } .fillMaxWidth() .height(height) .clickable {

Slide 43

Slide 43 text

val transition = updateTransition(targetState = expanded) val height by transition.animateDp { targetExpanded -> if (targetExpanded) 110.dp else 24.dp } val textOffset by transition.animateFloat { targetExpanded -> if (targetExpanded) 1f else 0f } Spacer( modifier = Modifier .drawWithCache { val brush = Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/)) val textResult = textMeasurer.measure(AnnotatedString("😴")) onDrawBehind { drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx())) drawText(textResult) } } .fillMaxWidth()

Slide 44

Slide 44 text

val transition = updateTransition(targetState = expanded) val height by transition.animateDp { targetExpanded -> if (targetExpanded) 110.dp else 24.dp } val textOffset by transition.animateFloat { targetExpanded -> if (targetExpanded) 1f else 0f } Spacer( modifier = Modifier .drawWithCache { val brush = Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/)) val textResult = textMeasurer.measure(AnnotatedString("😴")) onDrawBehind { drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx())) translate(left = -textOffset * textResult.size.width) { drawText(textResult) } } } .fillMaxWidth()

Slide 45

Slide 45 text

Draw Paths ➰

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

val path = Path() var previousPeriod: SleepPeriod? = null sleepData.sleepPeriods.forEach { period -> previousPeriod = period } return path

Slide 48

Slide 48 text

if (previousPeriod != null) { path.lineTo(x = /..., y = /...) // STEP 1 } val path = Path() var previousPeriod: SleepPeriod? = null sleepData.sleepPeriods.forEach { period -> previousPeriod = period } return path

Slide 49

Slide 49 text

path.addRect(rect = /...) // STEP 2 val path = Path() var previousPeriod: SleepPeriod? = null sleepData.sleepPeriods.forEach { period -> if (previousPeriod != null) { path.lineTo(x = /..., y = /...) // STEP 1 } previousPeriod = period } return path

Slide 50

Slide 50 text

val path = Path() var previousPeriod: SleepPeriod? = null sleepData.sleepPeriods.forEach { period -> if (previousPeriod != null) { path.lineTo(x = /..., y = /...) // STEP 1 } path.addRect(rect = /...) // STEP 2 path.moveTo(x = /..., y = /…) // STEP 3 previousPeriod = period } return path gradi

Slide 51

Slide 51 text

val path = Path() var previousPeriod: SleepPeriod? = null sleepData.sleepPeriods.forEach { period -> if (previousPeriod != null) { path.lineTo(x = /..., y = /...) // STEP 1 } path.addRect(rect = /...) // STEP 2 path.moveTo(x = /..., y = /…) // STEP 3 previousPeriod = period } return path gradi

Slide 52

Slide 52 text

val path = generateSleepPath(/*pass data*/) onDrawBehind { drawPath(path, brush = gradientBrush) }

Slide 53

Slide 53 text

val path = generateSleepPath(/*pass data*/) onDrawBehind { drawPath(path, brush = gradientBrush) }

Slide 54

Slide 54 text

val path = generateSleepPath(/*pass data*/) onDrawBehind { // default arg: style = Fill drawPath(path, brush = gradientBrush) }

Slide 55

Slide 55 text

val path = generateSleepPath(/*pass data*/) onDrawBehind { // default arg: style = Fill drawPath(path, brush = gradientBrush) // call draw again with Stroke style drawPath(path, brush = gradientBrush style = Stroke(/**/), ) }

Slide 56

Slide 56 text

val path = generateSleepPath(/*pass data*/) onDrawBehind { // default arg: style = Fill drawPath(path, brush = gradientBrush) // call draw again with Stroke style drawPath(path, brush = gradientBrush style = Stroke(lineThicknessPx, cap = StrokeCap.Round, join = StrokeJoin.Round, pathEffect = PathEffect.cornerPathEffect( cornerRadiusPx * expandedProgress) ) ) }

Slide 57

Slide 57 text

val path = generateSleepPath(/*pass data*/) onDrawBehind { // default arg: style = Fill drawPath(path, brush = gradientBrush) // call draw again with Stroke style drawPath(path, brush = gradientBrush style = Stroke(/**/), ) }

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

No content

Slide 60

Slide 60 text

Custom ShaderBrush with AGSL Using AGSL and want to use it with DrawScope in Compose… From Android T+ • Based on GLSL (OpenGL) • Android T+ • Runs on GPU so very efficient (parallel pixel calculations)

Slide 61

Slide 61 text

Taken from an online shader tool

Slide 62

Slide 62 text

@Composable private fun JetLaggedScreen() { Column() { JetLaggedHeader() JetLaggedSleepSummary() } JetLaggedHeaderTabs() TimeGraph() }

Slide 63

Slide 63 text

@Language("AGSL") val SHADER = """ uniform float2 resolution; uniform float time; layout(color) uniform half4 color; float4 main(in float2 fragCoord) { // do some math to calculate colour per pixel return float4(color, 1.0); } """.trimIndent() For each pixel in the area, main() is called with the coordinate of where its calling from

Slide 64

Slide 64 text

@Composable private fun JetLaggedScreen() { Column(modifier = Modifier .drawWithCache { }) { JetLaggedHeader() JetLaggedSleepSummary() } }

Slide 65

Slide 65 text

@Composable private fun JetLaggedScreen() { Column(modifier = Modifier .drawWithCache { val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL) }) { JetLaggedHeader() JetLaggedSleepSummary() } }

Slide 66

Slide 66 text

@Composable private fun JetLaggedScreen() { Column(modifier = Modifier .drawWithCache { val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL) val shaderBrush = ShaderBrush(shader) }) { JetLaggedHeader() JetLaggedSleepSummary() } }

Slide 67

Slide 67 text

@Composable private fun JetLaggedScreen() { Column(modifier = Modifier .drawWithCache { val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL) val shaderBrush = ShaderBrush(shader) shader.setFloatUniform("iResolution", size.width, size.height) }) { JetLaggedHeader() JetLaggedSleepSummary() } }

Slide 68

Slide 68 text

@Composable private fun JetLaggedScreen() { Column(modifier = Modifier .drawWithCache { val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL) val shaderBrush = ShaderBrush(shader) shader.setFloatUniform("iResolution", size.width, size.height) onDrawBehind { drawRect(shaderBrush) } }) { JetLaggedHeader() JetLaggedSleepSummary() } }

Slide 69

Slide 69 text

No content

Slide 70

Slide 70 text

@Composable private fun JetLaggedScreen() { Column(modifier = Modifier .drawWithCache { val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL) val shaderBrush = ShaderBrush(shader) shader.setFloatUniform("resolution", size.width, size.height) onDrawBehind { drawRect(shaderBrush) } }) { JetLaggedHeader() JetLaggedSleepSummary() } }

Slide 71

Slide 71 text

@Composable private fun JetLaggedScreen() { Column(modifier = Modifier .drawWithCache { val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL) val shaderBrush = ShaderBrush(shader) shader.setFloatUniform("resolution", size.width, size.height) onDrawBehind { drawRect(shaderBrush) } }) { JetLaggedHeader() JetLaggedSleepSummary()

Slide 72

Slide 72 text

@Composable private fun JetLaggedScreen() { val time by produceState(0f) { while(true) { withInfiniteAnimationFrameMillis { value = it / 1000f } } } Column(modifier = Modifier .drawWithCache { val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL) val shaderBrush = ShaderBrush(shader) shader.setFloatUniform("resolution", size.width, size.height) onDrawBehind { drawRect(shaderBrush) } }) { JetLaggedHeader() JetLaggedSleepSummary()

Slide 73

Slide 73 text

@Composable private fun JetLaggedScreen() { val time by produceState(0f) { while(true) { withInfiniteAnimationFrameMillis { value = it / 1000f } } } Column(modifier = Modifier .drawWithCache { val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL) val shaderBrush = ShaderBrush(shader) shader.setFloatUniform("resolution", size.width, size.height) onDrawBehind { shader.setFloatUniform("time", time) drawRect(shaderBrush) } }) { JetLaggedHeader()

Slide 74

Slide 74 text

No content

Slide 75

Slide 75 text

Accessing Canvas Sometimes you might want to have access to it…

Slide 76

Slide 76 text

@Composable private fun JetLaggedApp() { val paint = remember { Paint() } JetLaggedScreen() }

Slide 77

Slide 77 text

@Composable private fun JetLaggedApp() { val paint = remember { Paint() } JetLaggedScreen(modifier = Modifier .drawWithContent { drawContent() }) }

Slide 78

Slide 78 text

@Composable private fun JetLaggedApp() { val paint = remember { Paint() } JetLaggedScreen(modifier = Modifier .drawWithContent { val matrix = ColorMatrix().apply { setToSaturation(0f) } paint.colorFilter = colorMatrix(matrix) drawIntoCanvas { canvas -> canvas.saveLayer(size.toRect(), paint) [email protected]() canvas.restore() } }) }

Slide 79

Slide 79 text

🧛

Slide 80

Slide 80 text

Modifier.drawWithContent Determine the drawing order yourself * Useful for BlendMode operations * Transformations to the content yourself (ie clipping to a complex path) * Or drawing on top of content

Slide 81

Slide 81 text

Column( modifier = Modifier .fillMaxSize() .pointerInput("dragging") { detectDragGestures { change, dragAmount -> pointerOffset += dragAmount } } .onSizeChanged { pointerOffset = Offset(it.width / 2f, it.height /2f) } .drawWithContent { drawContent() drawRect(Brush.radialGradient( listOf(Color.Transparent, Color.Black), center = pointerOffset, radius = 100.dp.toPx(), )) } ) { // content goes here }

Slide 82

Slide 82 text

Modifier.graphicsLayer{ } Modifier used to apply transformations to a Composable, such as: • scaleX, scaleY • rotationX, rotationY, rotationZ • alpha • translationX, translationY *Note: Will not change the layout size and placement of the composable

Slide 83

Slide 83 text

Image(painterResource(id = R.drawable.sun_icon), contentDescription = "", modifier = Modifier .size(320.dp) )

Slide 84

Slide 84 text

val infinite = rememberInfiniteTransition() val rotation by infinite.animateFloat( initialValue = 0f, targetValue = 360f, animationSpec = infiniteRepeatable( animation = tween(5000, easing = LinearEasing), ) ) Image(painterResource(id = R.drawable.sun_icon), contentDescription = "", modifier = Modifier .size(320.dp) )

Slide 85

Slide 85 text

val infinite = rememberInfiniteTransition() val rotation by infinite.animateFloat( initialValue = 0f, targetValue = 360f, animationSpec = infiniteRepeatable( animation = tween(5000, easing = LinearEasing), ) ) Image(painterResource(id = R.drawable.sun_icon), contentDescription = "", modifier = Modifier .size(320.dp) .graphicsLayer { rotationZ = rotation } )

Slide 86

Slide 86 text

No content

Slide 87

Slide 87 text

graphicsLayer: RenderEffects Intermediate rendering step used to render drawing commands with a corresponding visual effect * BlurEffect - apply blur * RuntimeShader: Custom AGSL shader applied to whole composable

Slide 88

Slide 88 text

@Language("AGSL") val WOBBLE_SHADER = """ uniform float2 resolution; uniform float time; uniform shader contents; vec4 main(in vec2 fragCoord) { vec2 uv = fragCoord.xy / resolution.xy * 0.8 + 0.1; uv += sin(time * vec2(1.0, 2.0) + uv* 2.0) * 0.01; return contents.eval(uv * resolution.xy); } """.trimIndent()

Slide 89

Slide 89 text

val shader = RuntimeShader(WOBBLE_SHADER) Column(Modifier .onSizeChanged { size -> shader.setFloatUniform(“resolution", size.width.toFloat(), size.height.toFloat() ) } .graphicsLayer { renderEffect = android.graphics.RenderEffect .createRuntimeShaderEffect( shader, "contents" ) .asComposeRenderEffect() } ) { // your content here }

Slide 90

Slide 90 text

val shader = RuntimeShader(WOBBLE_SHADER) Column(Modifier .onSizeChanged { size -> shader.setFloatUniform(“resolution", size.width.toFloat(), size.height.toFloat() ) } .graphicsLayer { renderEffect = android.graphics.RenderEffect .createRuntimeShaderEffect( shader, "contents" ) .asComposeRenderEffect() } ) { // your content here }

Slide 91

Slide 91 text

graphicsLayer: CompositingStrategy New in Compose 1.4+ (alpha right now) Refers to how the layer will be composited (put together) with existing content on screen.

Slide 92

Slide 92 text

Offscreen Forces an offscreen buffer. Auto ModulateAlpha The default mode - no offscreen buffer. * Unless alpha or RenderEffects are set Each draw call has the alpha applied individually.

Slide 93

Slide 93 text

Image(painter = dogImagePainter, modifier = Modifier .drawWithCache { onDrawWithContent { drawContent() drawCircle( Color.Black, blendMode = BlendMode.Clear ) drawCircle( Color.Red ) } } )

Slide 94

Slide 94 text

Image(painter = dogImagePainter, modifier = Modifier .drawWithCache { onDrawWithContent { drawContent() drawCircle( Color.Black, blendMode = BlendMode.Clear ) drawCircle( Color.Red ) } } )

Slide 95

Slide 95 text

CompositingStrategy.Auto Image(painter = dogImagePainter, modifier = Modifier .graphicsLayer { compositingStrategy = Auto } .drawWithCache { onDrawWithContent { drawContent() drawCircle( Color.Black, blendMode = BlendMode.Clear ) drawCircle( Color.Red ) } } )

Slide 96

Slide 96 text

CompositingStrategy.Auto CompositingStrategy.Offscreen Image(painter = dogImagePainter, modifier = Modifier .graphicsLayer { compositingStrategy = Offscreen } .drawWithCache { onDrawWithContent { drawContent() drawCircle( Color.Black, blendMode = BlendMode.Clear ) drawCircle( Color.Red ) } } )

Slide 97

Slide 97 text

Having (even more) fun with Paths goo.gle/compose-gradient-along-path

Slide 98

Slide 98 text

private object HelloPath { val path = PathParser().parsePathString( "M13.63 248.31C13.63 248.31 51.84 206.67 84.21 169.31C140" + ".84 103.97 202.79 27.66 150.14 14.88C131.01 10.23 116.36 29.88 107.26 45.33C69.7 108.92 58.03 214.33 57.54 302.57C67.75 271.83 104.43 190.85 140.18 193.08C181.47 195.65 145.26 257.57 154.53 284.39C168.85 322.18 208.22 292.83 229.98 277.45C265.92 252.03 288.98 231.22 288.98 200.45C288.98 161.55 235.29 174.02 223.3 205.14C213.93 229.44 214.3 265.89 229.3 284.14C247.49 306.28 287.67 309.93 312.18 288.46C337 266.71 354.66 234.56 368.68 213.03C403.92 158.87 464.36 86.15 449.06 30.03C446.98 22.4 440.36 16.57 432.46 16.26C393.62 14.75 381.84 99.18 375.35 129.31C368.78 159.83 345.17 261.31 373.11 293.06C404.43 328.58 446.29 262.4 464.66 231.67C468.66 225.31 472.59 218.43 476.08 213.07C511.33 158.91 571.77 86.19 556.46 30.07C554.39 22.44 547.77 16.61 539.87 16.3C501.03 14.79 489.25 99.22 482.76 129.35C476.18 159.87 452.58 261.35 480.52 293.1C511.83 328.62 562.4 265.53 572.64 232.86C587.34 185.92 620.94 171.58 660.91 180.29C616 166.66 580.86 199.67 572.64 233.16C566.81 256.93 573.52 282.16 599.25 295.77C668.54 332.41 742.8 211.69 660.91 180.29C643.67 181.89 636.15 204.77 643.29 227.78C654.29 263.97 704.29 268.27 733.08 256" ) }

Slide 99

Slide 99 text

val painter = rememberVectorPainter( viewportHeight = bounds.height, viewportWidth = bounds.width, defaultWidth = bounds.width.dp, defaultHeight = bounds.height.dp, autoMirror = true ) { _, _ -> Path( HelloPath.path.toNodes(), stroke = Brush.linearGradient(colors) ) } Image(painter, contentDescription = "hello")

Slide 100

Slide 100 text

val path = remember { HelloPath.path.toPath() } val bounds = path.getBounds() val totalLength = remember { val pathMeasure = PathMeasure() pathMeasure.setPath(path, false) pathMeasure.length } val lines = remember { path.asAndroidPath().flatten(0.5f) }

Slide 101

Slide 101 text

val totalLength = remember { val pathMeasure = PathMeasure() pathMeasure.setPath(path, false) pathMeasure.length } val lines = remember { path.asAndroidPath().flatten(0.5f) } Box(modifier = Modifier.fillMaxSize()) { Canvas(modifier = Modifier .align(Alignment.Center), onDraw = { lines.forEach { line -> drawLine( brush = SolidColor(Color.Black), start = Offset(line.start.x, line.start.y), end = Offset(line.end.x, line.end.y)) } }) }

Slide 102

Slide 102 text

val totalLength = remember { val pathMeasure = PathMeasure() pathMeasure.setPath(path, false) pathMeasure.length } val lines = remember { path.asAndroidPath().flatten(30f) } Box(modifier = Modifier.fillMaxSize()) { Canvas(modifier = Modifier .align(Alignment.Center), onDraw = { lines.forEach { line -> drawLine( brush = SolidColor(Color.Black), start = Offset(line.start.x, line.start.y), end = Offset(line.end.x, line.end.y)) } }) }

Slide 103

Slide 103 text

val totalLength = remember { val pathMeasure = PathMeasure() pathMeasure.setPath(path, false) pathMeasure.length } val lines = remember { path.asAndroidPath().flatten(0.5f) } Box(modifier = Modifier.fillMaxSize()) { Canvas(modifier = Modifier .align(Alignment.Center), onDraw = { lines.forEach { line -> val startColor = interpolateColors(line.startFraction, colors) val endColor = interpolateColors(line.endFraction, colors) drawLine( brush = Brush.linearGradient(listOf(startColor, endColor)), start = Offset(line.start.x, line.start.y), end = Offset(line.end.x, line.end.y)) } }) }

Slide 104

Slide 104 text

private fun interpolateColors( progress: Float, colorsInput: List, ): Color { val scaledProgress = progress * (colorsInput.size - 1) val oldColor = colorsInput[scaledProgress.toInt()] val newColor = colorsInput[(scaledProgress + 1f).toInt()] val newScaledProgressValue = scaledProgress - floor(progress) return lerp( start = oldColor, stop = newColor, fraction = newScaledProgressValue) }

Slide 105

Slide 105 text

private fun interpolateColors( progress: Float, colorsInput: List, ): Color { val scaledProgress = progress * (colorsInput.size - 1) val oldColor = colorsInput[scaledProgress.toInt()] val newColor = colorsInput[(scaledProgress + 1f).toInt()] val newScaledProgressValue = scaledProgress - floor(progress) return lerp( start = oldColor, stop = newColor, fraction = newScaledProgressValue) } start stop fraction

Slide 106

Slide 106 text

No content

Slide 107

Slide 107 text

val lines = remember { path.asAndroidPath().flatten(30f) } val progress = remember { Animatable(0f) } LaunchedEffect(Unit) { progress.animateTo( 1f, animationSpec = infiniteRepeatable(tween(3000)) ) } Box(modifier = Modifier.fillMaxSize()) { Canvas(modifier = Modifier .align(Alignment.Center), onDraw = { lines.forEach { line -> drawLine( //..) } }) }

Slide 108

Slide 108 text

val lines = remember { path.asAndroidPath().flatten(30f) } val progress = remember { Animatable(0f) } LaunchedEffect(Unit) { progress.animateTo( 1f, animationSpec = infiniteRepeatable(tween(3000)) ) } Box(modifier = Modifier.fillMaxSize()) { Canvas(modifier = Modifier .align(Alignment.Center), onDraw = { val currentLength = totalLength * progress.value lines.forEach { line -> if (line.startFraction * totalLength < currentLength) { drawLine( //..) } } })

Slide 109

Slide 109 text

goo.gle/compose-gradient-along-path

Slide 110

Slide 110 text

goo.gle/compose-graphics-docs goo.gle/compose-jetlagged goo.gle/compose-jellyfish goo.gle/compose-gradient-path

Slide 111

Slide 111 text

Thank you! Rebecca Franks (she/her) Developer Relations Engineer @riggaroo