-
Notifications
You must be signed in to change notification settings - Fork 378
Mesh Gradients in Compose 1.12 #936
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
8c25bc6
1ab00c4
ac130d3
6e37544
e6af48c
eda3469
1f9a2a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,219 @@ | ||
| package com.example.compose.snippets.graphics | ||
|
|
||
| import androidx.compose.animation.core.LinearEasing | ||
| import androidx.compose.animation.core.RepeatMode | ||
| import androidx.compose.animation.core.animateFloat | ||
| import androidx.compose.animation.core.infiniteRepeatable | ||
| import androidx.compose.animation.core.rememberInfiniteTransition | ||
| import androidx.compose.animation.core.tween | ||
| import androidx.compose.foundation.layout.Box | ||
| import androidx.compose.foundation.layout.aspectRatio | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.foundation.layout.safeContentPadding | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.getValue | ||
| import androidx.compose.runtime.remember | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.draw.paint | ||
| import androidx.compose.ui.draw.drawWithContent | ||
| import androidx.compose.ui.geometry.Offset | ||
| import androidx.compose.ui.graphics.Color | ||
| import androidx.compose.ui.graphics.MeshGradientPainter | ||
| import androidx.compose.ui.tooling.preview.Preview | ||
| import androidx.compose.ui.unit.dp | ||
|
|
||
| @Preview | ||
| @Composable | ||
| fun MeshGradientBasic(modifier: Modifier = Modifier) { | ||
| // [START android_compose_graphics_mesh_gradient_basic] | ||
| val rows = 1 | ||
| val columns = 1 | ||
|
|
||
| val gradientPainter = remember { | ||
| MeshGradientPainter(rows, columns) { | ||
| // Parameters: row, column, position, color | ||
| setVertex(0, 0, Offset(0f, 0f), Color.Red) // Top-Left | ||
| setVertex(0, 1, Offset(1f, 0f), Color.Blue) // Top-Right | ||
| setVertex(1, 0, Offset(0f, 1f), Color.Green) // Bottom-Left | ||
| setVertex(1, 1, Offset(1f, 1f), Color.Yellow) // Bottom-Right | ||
| } | ||
| } | ||
|
|
||
| Box( | ||
| modifier = modifier | ||
| .aspectRatio(16/9f) | ||
| .fillMaxWidth() | ||
| .paint(gradientPainter) | ||
| ) | ||
| // [END android_compose_graphics_mesh_gradient_basic] | ||
| } | ||
|
|
||
| @Preview | ||
| @Composable | ||
| fun MeshGradientControlPoints(modifier: Modifier = Modifier) { | ||
| // [START android_compose_graphics_mesh_gradient_control_points] | ||
| val customTangentPainter = remember { | ||
| MeshGradientPainter(rows = 1, columns = 1) { | ||
| // Tweak the top-left vertex to curve outwards to the right and bottom | ||
| setVertex( | ||
| row = 0, | ||
| column = 0, | ||
| position = Offset(0f, 0f), | ||
| color = Color.Magenta, | ||
| rightControlPoint = Offset(0.4f, 0.1f), | ||
| bottomControlPoint = Offset(0.1f, 0.4f) | ||
| ) | ||
|
|
||
| // Other points can remain unspecified to use default inferred fallback tangents | ||
| setVertex(0, 1, Offset(1f, 0f), Color.Cyan) | ||
| setVertex(1, 0, Offset(0f, 1f), Color.Blue) | ||
| setVertex(1, 1, Offset(1f, 1f), Color.Black) | ||
| } | ||
| } | ||
| Box( | ||
| modifier = modifier | ||
| .aspectRatio(16/9f) | ||
| .fillMaxWidth() | ||
| .paint(customTangentPainter) | ||
| ) | ||
| // [END android_compose_graphics_mesh_gradient_control_points] | ||
| } | ||
|
|
||
| @Preview | ||
| @Composable | ||
| fun MeshGradientComplex(modifier: Modifier = Modifier) { | ||
| val purple = Color(0xFFAF52DE) | ||
| val indigo = Color(0xFF5856D6) | ||
| val yellow = Color(0xFFFFCC00) | ||
| val pink = Color(0xFFFF2D55) | ||
| val orange = Color(0xFFFF9500) | ||
| // [START android_compose_graphics_mesh_gradient_complex] | ||
| val points = remember { | ||
| listOf( | ||
| Offset(0.0f, 0.0f), Offset(0.3f, 0.0f), Offset(0.7f, 0.0f), Offset(1.0f, 0.0f), | ||
| Offset(0.0f, 0.3f), Offset(0.2f, 0.4f), Offset(0.7f, 0.2f), Offset(1.0f, 0.3f), | ||
| Offset(0.0f, 0.7f), Offset(0.3f, 0.8f), Offset(0.7f, 0.6f), Offset(1.0f, 0.7f), | ||
| Offset(0.0f, 1.0f), Offset(0.3f, 1.0f), Offset(0.7f, 1.0f), Offset(1.0f, 1.0f) | ||
| ) | ||
| } | ||
|
|
||
| val gradientPainter = remember { | ||
| MeshGradientPainter(rows = 3, columns = 3) { | ||
| // Row 0 | ||
| setVertex(0, 0, points[0], yellow) | ||
| setVertex(0, 1, points[1], orange) | ||
| setVertex(0, 2, points[2], yellow) | ||
| setVertex(0, 3, points[3], purple) | ||
|
|
||
| // Row 1 | ||
| setVertex(1, 0, points[4], pink) | ||
| setVertex(1, 1, points[5], yellow) | ||
| setVertex(1, 2, points[6], pink) | ||
| setVertex(1, 3, points[7], purple) | ||
|
|
||
| // Row 2 | ||
| setVertex(2, 0, points[8], indigo) | ||
| setVertex(2, 1, points[9], pink) | ||
| setVertex(2, 2, points[10], purple) | ||
| setVertex(2, 3, points[11], indigo) | ||
|
|
||
| // Row 3 | ||
| setVertex(3, 0, points[12], purple) | ||
| setVertex(3, 1, points[13], indigo) | ||
| setVertex(3, 2, points[14], pink) | ||
| setVertex(3, 3, points[15], yellow) | ||
| } | ||
| } | ||
|
|
||
| Box( | ||
| modifier = modifier.padding(32.dp) | ||
| .aspectRatio(16 / 9f) | ||
| .fillMaxWidth() | ||
| .paint(gradientPainter) | ||
| // [START_EXCLUDE] | ||
| .drawWithContent { | ||
| drawContent() | ||
| points.forEach { normalizedOffset -> | ||
| val pixelOffset = Offset( | ||
| x = normalizedOffset.x * size.width, | ||
| y = normalizedOffset.y * size.height | ||
| ) | ||
| // Draw a subtle black circle outline and white center for maximum visibility | ||
| drawCircle( | ||
| color = Color.Black, | ||
| radius = 10f, | ||
| center = pixelOffset | ||
| ) | ||
| drawCircle( | ||
| color = Color.White, | ||
| radius = 8f, | ||
| center = pixelOffset | ||
| ) | ||
| } | ||
| } | ||
| // [END_EXCLUDE] | ||
| ) | ||
| // [END android_compose_graphics_mesh_gradient_complex] | ||
| } | ||
|
|
||
| @Preview | ||
| @Composable | ||
| fun MeshGradientAnimation(modifier: Modifier = Modifier) { | ||
| // [START android_compose_graphics_mesh_gradient_animation] | ||
| val infiniteTransition = rememberInfiniteTransition(label = "meshMovement") | ||
| val animatedOffset by infiniteTransition.animateFloat( | ||
| initialValue = -0.1f, | ||
| targetValue = 0.1f, | ||
| animationSpec = infiniteRepeatable( | ||
| animation = tween(2500, easing = LinearEasing), | ||
| repeatMode = RepeatMode.Reverse | ||
| ), | ||
| label = "offset" | ||
| ) | ||
|
|
||
| val coral = Color(255, 90, 90) | ||
| val peach = Color(255, 139, 90) | ||
| val amber = Color(255, 169, 90) | ||
| val sunshine = Color(255, 212, 90) | ||
| val indigo = Color(0xFF5856D6) | ||
| val pink = Color(0xFFFF2D55) | ||
|
|
||
|
|
||
| val gradientPainter = remember { | ||
| MeshGradientPainter(rows = 3, columns = 3) { | ||
| // Row 0 | ||
| setVertex(0, 0, Offset(0.0f, 0.0f), indigo) | ||
| setVertex(0, 1, Offset(0.3f, 0.0f), peach) | ||
| setVertex(0, 2, Offset(0.7f, 0.0f), amber) | ||
| setVertex(0, 3, Offset(1.0f, 0.0f), sunshine) | ||
| // Row 1 | ||
| setVertex(1, 0, Offset(0.0f, 0.3f), pink) | ||
| setVertex(1, 1, Offset(0.2f, 0.4f) + Offset(animatedOffset, animatedOffset), coral) | ||
| setVertex(1, 2, Offset(0.7f, 0.2f) + Offset(animatedOffset, animatedOffset), peach) | ||
| setVertex(1, 3, Offset(1.0f, 0.3f), indigo) | ||
|
|
||
| // Row 2 | ||
| setVertex(2, 0, Offset(0.0f, 0.7f), coral) | ||
| setVertex(2, 1, Offset(0.3f, 0.8f) + Offset(animatedOffset, 0f), pink) | ||
| setVertex(2, 2, Offset(0.7f, 0.6f) + Offset(animatedOffset, 0f), sunshine) | ||
| setVertex(2, 3, Offset(1.0f, 0.7f), amber) | ||
|
|
||
| // Row 3 | ||
| setVertex(3, 0, Offset(0.0f, 1.0f), sunshine) | ||
| setVertex(3, 1, Offset(0.3f, 1.0f), amber) | ||
| setVertex(3, 2, Offset(0.7f, 1.0f), pink) | ||
| setVertex(3, 3, Offset(1.0f, 1.0f), indigo) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Box( | ||
| modifier = modifier.padding(32.dp) | ||
| .safeContentPadding() | ||
| .aspectRatio(16 / 9f) | ||
| .fillMaxWidth() | ||
| .paint(gradientPainter) | ||
| ) | ||
| // [END android_compose_graphics_mesh_gradient_animation] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,7 @@ androidx-appcompat = "1.7.1" | |
| androidx-appfunctions = "1.0.0-alpha08" | ||
| androidx-cameraX = "1.6.0" | ||
| androidx-car = "1.7.0" | ||
| androidx-compose-bom = "2026.05.00" | ||
| androidx-compose-bom = "2026.05.01" | ||
| androidx-compose-ui-test = "1.7.0-alpha08" | ||
| androidx-compose-ui-test-junit4-accessibility = "1.11.1" | ||
| androidx-constraintlayout = "2.2.1" | ||
|
|
@@ -140,7 +140,7 @@ androidx-camera-viewfinder-compose = { module = "androidx.camera.viewfinder:view | |
| androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidx-cameraX" } | ||
| androidx-car = { module = "androidx.car.app:app", version.ref = "androidx-car"} | ||
| androidx-compose-animation-graphics = { module = "androidx.compose.animation:animation-graphics", version.ref = "compose-latest" } | ||
| androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } | ||
| androidx-compose-bom = { module = "androidx.compose:compose-bom-alpha", version.ref = "androidx-compose-bom" } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it okay move the whole repo to alpha?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kkuan2011 no, will leave this as a branch until we are good to go with a stable version :)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok sounds good :) I'll assume no action needed for now then |
||
| androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-latest" } | ||
| androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout", version.ref = "compose-latest" } | ||
| androidx-compose-material = { module = "androidx.compose.material:material", version.ref = "compose-latest" } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
gradientPainteris remembered without any keys, but its builder block captures and reads theanimatedOffsetstate. Becauserememberwithout keys only runs once during initial composition, the painter will not be recreated or updated whenanimatedOffsetchanges, causing the gradient to remain completely static. AddinganimatedOffsetas a key torememberensures the painter is updated/recreated as the animation progresses.