$30 off During Our Annual Pro Sale. View Details »

Custom Drawing in Compose - Londroid December 2022

Custom Drawing in Compose - Londroid December 2022

Presented at Londroid 2022, custom drawing in Compose covers examples of how to draw custom designs in your Jetpack Compose Apps.

Covering things like:
Modifier.drawBehind
Modifier.drawWithContent
Modifier.drawWithCache
AGSL shaders, Paths and Animations.

Rebecca Franks

December 08, 2022
Tweet

More Decks by Rebecca Franks

Other Decks in Technology

Transcript

  1. • 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
  2. Drawing in Compose Canvas( onDraw = { drawRoundRect(Color.Blue) } )

    Canvas Composable Modifiers Modifier.drawBehind { } Modifier.drawWithContent { } Modifier.drawWithCache { }
  3. 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
  4. • 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]
  5. @Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier )

    { Spacer( modifier = modifier .drawWithCache { onDrawBehind { // this: DrawScope } } .fillMaxWidth() .height(48.dp) ) }
  6. interface DrawScope { val center: Offset val size: Size fun

    drawLine() fun drawRect() fun drawImage() fun drawRoundRect() fun drawPath() // etc }
  7. @Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier )

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

    { Spacer( modifier = modifier .drawWithCache { onDrawBehind { } } .fillMaxWidth() .height(24.dp) ) }
  9. @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) ) }
  10. 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
  11. @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) ) }
  12. @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) ) }
  13. @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) ) }
  14. @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) ) }
  15. @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) ) }
  16. @Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier )

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

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

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

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

    modifier .drawWithCache { // Our drawing code } .fillMaxWidth() .height(24.dp) .clickable { isExpanded = !isExpanded } ) }
  21. 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 } ) }
  22. 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 } ) }
  23. 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 {
  24. 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
  25. 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 {
  26. 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()
  27. 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()
  28. if (previousPeriod != null) { path.lineTo(x = /..., y =

    /...) // STEP 1 } val path = Path() var previousPeriod: SleepPeriod? = null sleepData.sleepPeriods.forEach { period -> previousPeriod = period } return path
  29. 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
  30. 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
  31. 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
  32. val path = generateSleepPath(/*pass data*/) onDrawBehind { // default arg:

    style = Fill drawPath(path, brush = gradientBrush) }
  33. 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(/**/), ) }
  34. 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) ) ) }
  35. 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(/**/), ) }
  36. 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)
  37. @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
  38. @Composable private fun JetLaggedScreen() { Column(modifier = Modifier .drawWithCache {

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

    val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL) val shaderBrush = ShaderBrush(shader) }) { JetLaggedHeader() JetLaggedSleepSummary() } }
  40. @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() } }
  41. @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() } }
  42. @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() } }
  43. @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()
  44. @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()
  45. @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()
  46. @Composable private fun JetLaggedApp() { val paint = remember {

    Paint() } JetLaggedScreen(modifier = Modifier .drawWithContent { drawContent() }) }
  47. @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() } }) }
  48. 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
  49. 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 }
  50. 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
  51. 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) )
  52. 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 } )
  53. 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
  54. @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()
  55. 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 }
  56. 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 }
  57. 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.
  58. 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.
  59. Image(painter = dogImagePainter, modifier = Modifier .drawWithCache { onDrawWithContent {

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

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

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

    compositingStrategy = Offscreen } .drawWithCache { onDrawWithContent { drawContent() drawCircle( Color.Black, blendMode = BlendMode.Clear ) drawCircle( Color.Red ) } } )
  63. 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" ) }
  64. 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")
  65. 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) }
  66. 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)) } }) }
  67. 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)) } }) }
  68. 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)) } }) }
  69. private fun interpolateColors( progress: Float, colorsInput: List<Color>, ): 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) }
  70. private fun interpolateColors( progress: Float, colorsInput: List<Color>, ): 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
  71. 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( //..) } }) }
  72. 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( //..) } } })