-
-
Save sinasamaki/05725557c945c5329fdba4a3494aaecb to your computer and use it in GitHub Desktop.
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.derivedStateOf | |
| import androidx.compose.runtime.getValue | |
| import androidx.compose.runtime.remember | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.draw.drawBehind | |
| import androidx.compose.ui.geometry.Offset | |
| import androidx.compose.ui.graphics.BlendMode | |
| import androidx.compose.ui.graphics.Color | |
| import androidx.compose.ui.graphics.Paint | |
| import androidx.compose.ui.graphics.Path | |
| import androidx.compose.ui.graphics.PathMeasure | |
| import androidx.compose.ui.graphics.PointMode | |
| import androidx.compose.ui.graphics.StrokeCap | |
| import androidx.compose.ui.graphics.VertexMode | |
| import androidx.compose.ui.graphics.Vertices | |
| import androidx.compose.ui.graphics.drawscope.drawIntoCanvas | |
| import androidx.compose.ui.graphics.drawscope.scale | |
| import androidx.compose.ui.graphics.lerp | |
| @Composable | |
| fun Modifier.meshGradient( | |
| points: List<List<Pair<Offset, Color>>>, | |
| resolutionX: Int = 1, | |
| resolutionY: Int = 1, | |
| showPoints: Boolean = false, | |
| indicesModifier: (List<Int>) -> List<Int> = { it } | |
| ): Modifier { | |
| val pointData by remember(points, resolutionX, resolutionY) { | |
| derivedStateOf { | |
| PointData(points, resolutionX, resolutionY) | |
| } | |
| } | |
| return drawBehind { | |
| drawIntoCanvas { canvas -> | |
| scale( | |
| scaleX = size.width, | |
| scaleY = size.height, | |
| pivot = Offset.Zero | |
| ) { | |
| canvas.drawVertices( | |
| vertices = Vertices( | |
| vertexMode = VertexMode.Triangles, | |
| positions = pointData.offsets, | |
| textureCoordinates = pointData.offsets, | |
| colors = pointData.colors, | |
| indices = indicesModifier(pointData.indices) | |
| ), | |
| blendMode = BlendMode.Dst, | |
| paint = paint, | |
| ) | |
| } | |
| if (showPoints) { | |
| val flattenedPaint = Paint() | |
| flattenedPaint.color = Color.White.copy(alpha = .9f) | |
| flattenedPaint.strokeWidth = 4f * .001f | |
| flattenedPaint.strokeCap = StrokeCap.Round | |
| flattenedPaint.blendMode = BlendMode.SrcOver | |
| scale( | |
| scaleX = size.width, | |
| scaleY = size.height, | |
| pivot = Offset.Zero | |
| ) { | |
| canvas.drawPoints( | |
| pointMode = PointMode.Points, | |
| points = pointData.offsets, | |
| paint = flattenedPaint | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| class PointData( | |
| private val points: List<List<Pair<Offset, Color>>>, | |
| private val stepsX: Int, | |
| private val stepsY: Int, | |
| ) { | |
| val offsets: MutableList<Offset> | |
| val colors: MutableList<Color> | |
| val indices: List<Int> | |
| private val xLength: Int = (points[0].size * stepsX) - (stepsX - 1) | |
| private val yLength: Int = (points.size * stepsY) - (stepsY - 1) | |
| private val measure = PathMeasure() | |
| private val indicesBlocks: List<IndicesBlock> | |
| init { | |
| offsets = buildList { | |
| repeat((xLength - 0) * (yLength - 0)) { | |
| add(Offset(0f, 0f)) | |
| } | |
| }.toMutableList() | |
| colors = buildList { | |
| repeat((xLength - 0) * (yLength - 0)) { | |
| add(Color.Transparent) | |
| } | |
| }.toMutableList() | |
| indicesBlocks = | |
| buildList { | |
| for (y in 0..yLength - 2) { | |
| for (x in 0..xLength - 2) { | |
| val a = (y * xLength) + x | |
| val b = a + 1 | |
| val c = ((y + 1) * xLength) + x | |
| val d = c + 1 | |
| add( | |
| IndicesBlock( | |
| indices = buildList { | |
| add(a) | |
| add(c) | |
| add(d) | |
| add(a) | |
| add(b) | |
| add(d) | |
| }, | |
| x = x, y = y | |
| ) | |
| ) | |
| } | |
| } | |
| } | |
| indices = indicesBlocks.flatMap { it.indices } | |
| generateInterpolatedOffsets() | |
| } | |
| private fun generateInterpolatedOffsets() { | |
| for (y in 0..points.lastIndex) { | |
| for (x in 0..points[y].lastIndex) { | |
| this[x * stepsX, y * stepsY] = points[y][x].first | |
| this[x * stepsX, y * stepsY] = points[y][x].second | |
| if (x != points[y].lastIndex) { | |
| val path = cubicPathX( | |
| point1 = points[y][x].first, | |
| point2 = points[y][x + 1].first, | |
| when (x) { | |
| 0 -> 0 | |
| points[y].lastIndex - 1 -> 2 | |
| else -> 1 | |
| } | |
| ) | |
| measure.setPath(path, false) | |
| for (i in 1..<stepsX) { | |
| measure.getPosition(i / stepsX.toFloat() * measure.length).let { | |
| this[(x * stepsX) + i, (y * stepsY)] = Offset(it.x, it.y) | |
| this[(x * stepsX) + i, (y * stepsY)] = | |
| lerp( | |
| points[y][x].second, | |
| points[y][x + 1].second, | |
| i / stepsX.toFloat(), | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| for (y in 0..<points.lastIndex) { | |
| for (x in 0..<this.xLength) { | |
| val path = cubicPathY( | |
| point1 = this[x, y * stepsY].let { Offset(it.x, it.y) }, | |
| point2 = this[x, (y + 1) * stepsY].let { Offset(it.x, it.y) }, | |
| when (y) { | |
| 0 -> 0 | |
| points[y].lastIndex - 1 -> 2 | |
| else -> 1 | |
| } | |
| ) | |
| measure.setPath(path, false) | |
| for (i in (1..<stepsY)) { | |
| val point3 = measure.getPosition(i / stepsY.toFloat() * measure.length).let { | |
| Offset(it.x, it.y) | |
| } | |
| this[x, ((y * stepsY) + i)] = point3 | |
| this[x, ((y * stepsY) + i)] = lerp( | |
| this.getColor(x, y * stepsY), | |
| this.getColor(x, (y + 1) * stepsY), | |
| i / stepsY.toFloat(), | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| data class IndicesBlock(val indices: List<Int>, val x: Int, val y: Int) | |
| operator fun get(x: Int, y: Int): Offset { | |
| val index = (y * xLength) + x | |
| return offsets[index] | |
| } | |
| private fun getColor(x: Int, y: Int): Color { | |
| val index = (y * xLength) + x | |
| return colors[index] | |
| } | |
| private operator fun set(x: Int, y: Int, offset: Offset) { | |
| val index = (y * xLength) + x | |
| offsets[index] = Offset(offset.x, offset.y) | |
| } | |
| private operator fun set(x: Int, y: Int, color: Color) { | |
| val index = (y * xLength) + x | |
| colors[index] = color | |
| } | |
| } | |
| private fun cubicPathX(point1: Offset, point2: Offset, position: Int): Path { | |
| val path = Path().apply { | |
| moveTo(point1.x, point1.y) | |
| val delta = (point2.x - point1.x) * .5f | |
| when (position) { | |
| 0 -> cubicTo( | |
| point1.x, point1.y, | |
| point2.x - delta, point2.y, | |
| point2.x, point2.y | |
| ) | |
| 2 -> cubicTo( | |
| point1.x + delta, point1.y, | |
| point2.x, point2.y, | |
| point2.x, point2.y | |
| ) | |
| else -> cubicTo( | |
| point1.x + delta, point1.y, | |
| point2.x - delta, point2.y, | |
| point2.x, point2.y | |
| ) | |
| } | |
| lineTo(point2.x, point2.y) | |
| } | |
| return path | |
| } | |
| private fun cubicPathY(point1: Offset, point2: Offset, position: Int): Path { | |
| val path = Path().apply { | |
| moveTo(point1.x, point1.y) | |
| val delta = (point2.y - point1.y) * .5f | |
| when (position) { | |
| 0 -> cubicTo( | |
| point1.x, point1.y, | |
| point2.x, point2.y - delta, | |
| point2.x, point2.y | |
| ) | |
| 2 -> cubicTo( | |
| point1.x, point1.y + delta, | |
| point2.x, point2.y, | |
| point2.x, point2.y | |
| ) | |
| else -> cubicTo( | |
| point1.x, point1.y + delta, | |
| point2.x, point2.y - delta, | |
| point2.x, point2.y | |
| ) | |
| } | |
| lineTo(point2.x, point2.y) | |
| } | |
| return path | |
| } | |
| private val paint = Paint() |
@dauni6 what if you set the Paint colour to white? Cause this seems similar to this Samsung bug... and maybe the workaround for that one works here, too
@rock3r I just tested @sinasamaki's workaround on Android Studio emulator(API 33) and it worked well. So, It's the Samsung bug :(
Thank you @sinasamaki for the workaround and @rock3r for telling me the issue 👍
Hi, @sinasamaki ! Thanks for the awesome code!
I also have the same issue here on a real device. The device information would be of a Samsung M52 on Android 13. Wherever I add the meshGradient it causes a black background, even if it is a simple Box.
I can't think of any quick fix. I tried BlendMode.SrcOver and nothing works on this device. The issue doesn't happen on an emulator on Android 13.
Here is a screenshot of the issue on the device:
Just wanted to say, loving this mesh gradient.
Unfortunately, this has been a bit inconsistent for me and the project I'm working on. Here's some observances from testing that may help.
On my primary device (Pixel 8 Pro, Android 16) and most devices and emulators, it renders fine and is 🔥
On specific devices like a TCL 5001T, Android 9, there's an obscure crash we're investigating:
GraphicsJNI [package] A bad length
And on a Pixel 4, Android 13, it also renders as a black box. Setting the paint's color explicitly to white seems to resolve this on the Pixel 4 like mentioned on the above posts.
canvas.drawVertices(
vertices = Vertices(
//etc
),
blendMode = BlendMode.Dst,
paint = paint.apply {
color = Color.White // <-- explicit color
},
)Also, it seems at least as of Compose 1.7.5, blendMode does nothing for android's canvas for now:

I think a lot of this is down to Compose deferring the Canvas and Paint to the underlying platform (Android, desktop, etc). So some of Android's inconsistencies can creep in

@sinasamaki sorry for my late reply. Yes, the code doesn't work too on Samsung galaxy A51(API 33.