Share
Embed
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
Transcript
111 slides
-
• 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 -
Drawing in Compose Canvas( onDraw = { drawRoundRect(Color.Blue) } )
Canvas Composable Modifiers Modifier.drawBehind { } Modifier.drawWithContent { } Modifier.drawWithCache { } -
@Composable fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) = Spacer(modifier.drawBehind(onDraw))
Canvas implementation Just a spacer 🕵 -
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 -
• 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] -
@Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier )
{ Spacer( modifier = modifier .drawWithCache { onDrawBehind { // this: DrawScope } } .fillMaxWidth() .height(48.dp) ) } -
DrawScope Declarative, stateless drawing API to draw shapes, paths etc
without needing to maintain your own state. -
interface DrawScope { val center: Offset val size: Size fun
drawLine() fun drawRect() fun drawImage() fun drawRoundRect() fun drawPath() // etc } -
@Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier )
{ Spacer( modifier = modifier .drawWithCache { onDrawBehind { // this: DrawScope } } .fillMaxWidth() .height(24.dp) ) } -
@Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier )
{ Spacer( modifier = modifier .drawWithCache { onDrawBehind { } } .fillMaxWidth() .height(24.dp) ) } -
@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) ) } -
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 -
@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) ) } -
@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) ) } -
@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) ) } -
@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) ) } -
@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) ) } -
@Composable fun SleepBar( sleepData: SleepDayData, modifier: Modifier = Modifier )
{ val textMeasurer = rememberTextMeasurer() Spacer( modifier = modifier .drawWithCache { // Our drawing code } .fillMaxWidth() .height(24.dp) ) } -
Spacer( modifier = modifier .drawWithCache { // Our drawing code
} .fillMaxWidth() .height(24.dp) ) } -
var isExpanded by remember { mutableStateOf(false) } Spacer( modifier =
modifier .drawWithCache { // Our drawing code } .fillMaxWidth() .height(24.dp) ) } -
var isExpanded by remember { mutableStateOf(false) } Spacer( modifier =
modifier .drawWithCache { // Our drawing code } .fillMaxWidth() .height(24.dp) ) } -
var isExpanded by remember { mutableStateOf(false) } Spacer( modifier =
modifier .drawWithCache { // Our drawing code } .fillMaxWidth() .height(24.dp) .clickable { isExpanded = !isExpanded } ) } -
var isExpanded by remember { mutableStateOf(false) } Spacer( modifier =
modifier .drawWithCache { // Our drawing code } .fillMaxWidth() .height(24.dp) .clickable { isExpanded = !isExpanded } ) } -
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 } ) } -
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 } ) } -
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 { -
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 -
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 { -
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() -
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() -
val path = Path() var previousPeriod: SleepPeriod? = null sleepData.sleepPeriods.forEach
{ period -> previousPeriod = period } return path -
if (previousPeriod != null) { path.lineTo(x = /..., y =
/...) // STEP 1 } val path = Path() var previousPeriod: SleepPeriod? = null sleepData.sleepPeriods.forEach { period -> previousPeriod = period } return path -
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 -
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 -
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 -
val path = generateSleepPath(/*pass data*/) onDrawBehind { // default arg:
style = Fill drawPath(path, brush = gradientBrush) } -
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(/**/), ) } -
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) ) ) } -
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(/**/), ) } -
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) -
@Composable private fun JetLaggedScreen() { Column() { JetLaggedHeader() JetLaggedSleepSummary() }
JetLaggedHeaderTabs() TimeGraph() } -
@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 -
@Composable private fun JetLaggedScreen() { Column(modifier = Modifier .drawWithCache {
}) { JetLaggedHeader() JetLaggedSleepSummary() } } -
@Composable private fun JetLaggedScreen() { Column(modifier = Modifier .drawWithCache {
val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL) }) { JetLaggedHeader() JetLaggedSleepSummary() } } -
@Composable private fun JetLaggedScreen() { Column(modifier = Modifier .drawWithCache {
val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL) val shaderBrush = ShaderBrush(shader) }) { JetLaggedHeader() JetLaggedSleepSummary() } } -
@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() } } -
@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() } } -
@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() } } -
@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() -
@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() -
@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() -
@Composable private fun JetLaggedApp() { val paint = remember {
Paint() } JetLaggedScreen() } -
@Composable private fun JetLaggedApp() { val paint = remember {
Paint() } JetLaggedScreen(modifier = Modifier .drawWithContent { drawContent() }) } -
@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() } }) } -
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 -
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 } -
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 -
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) ) -
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 } ) -
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 -
@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() -
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 } -
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 } -
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. -
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. -
Image(painter = dogImagePainter, modifier = Modifier .drawWithCache { onDrawWithContent {
drawContent() drawCircle( Color.Black, blendMode = BlendMode.Clear ) drawCircle( Color.Red ) } } ) -
Image(painter = dogImagePainter, modifier = Modifier .drawWithCache { onDrawWithContent {
drawContent() drawCircle( Color.Black, blendMode = BlendMode.Clear ) drawCircle( Color.Red ) } } ) -
CompositingStrategy.Auto Image(painter = dogImagePainter, modifier = Modifier .graphicsLayer { compositingStrategy
= Auto } .drawWithCache { onDrawWithContent { drawContent() drawCircle( Color.Black, blendMode = BlendMode.Clear ) drawCircle( Color.Red ) } } ) -
CompositingStrategy.Auto CompositingStrategy.Offscreen Image(painter = dogImagePainter, modifier = Modifier .graphicsLayer {
compositingStrategy = Offscreen } .drawWithCache { onDrawWithContent { drawContent() drawCircle( Color.Black, blendMode = BlendMode.Clear ) drawCircle( Color.Red ) } } ) -
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" ) } -
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") -
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) } -
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)) } }) } -
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)) } }) } -
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)) } }) } -
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) } -
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 -
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( //..) } }) } -
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( //..) } } })