'Angled gradient background in Jetpack Compose
I am trying to draw a gradient background in Jetpack Compose, and I would like the gradient to have a fixed angle regardless of the shape of the object I'm drawing into.
However, using Modifier.background(brush=...)
, the best I can find is linearGradient
which calculates the angle from a fixed start and end point of a gradient.
For example, is there a way I can specify that I want a 45 degree angle for my gradient without knowing the final size it's going to be?
Edit: I would like a solution that can work for any given angle, not just 45 degrees.
Solution 1:[1]
Edit 2022-04-06
I realised there is an error in the original code, which distorts the gradient angle. Some more trigonometry is needed in order to constrain the gradient start and ends to within the canvas area (if that is what is desired) while also preserving the gradient angle. Here is the updated solution, with bonus ASCII art.
fun Modifier.angledGradientBackground(colors: List<Color>, degrees: Float) = this.then(
drawBehind {
/*
Have to compute length of gradient vector so that it lies within
the visible rectangle.
--------------------------------------------
| length of gradient ^ / |
| ---> / / |
| / / <- rotation angle |
| / o --------------------| y
| / / |
| / / |
| v / |
--------------------------------------------
x
diagonal angle = atan2(y, x)
(it's hard to draw the diagonal)
Simply rotating the diagonal around the centre of the rectangle
will lead to points outside the rectangle area. Further, just
truncating the coordinate to be at the nearest edge of the
rectangle to the rotated point will distort the angle.
Let ? be the desired gradient angle (in radians) and ? be the
angle of the diagonal of the rectangle.
The correct for the length of the gradient is given by:
x/|cos(?)| if -? <= ? <= ?, or ? - ? <= ? <= ? + ?
y/|sin(?)| if ? <= ? <= ? - ?, or ? + ? <= ? <= 2? - ?
where ? ? (0, ?/2) is the angle that the diagonal makes with
the base of the rectangle.
*/
val (x, y) = size
val gamma = atan2(y, x)
if (gamma == 0f || gamma == (PI / 2).toFloat()) {
// degenerate rectangle
return@drawBehind
}
val degreesNormalised = (degrees % 360).let { if (it < 0) it + 360 else it }
val alpha = (degreesNormalised * PI / 180).toFloat()
val gradientLength = when (alpha) {
// ray from centre cuts the right edge of the rectangle
in 0f..gamma, in (2*PI - gamma)..2*PI -> { x / cos(alpha) }
// ray from centre cuts the top edge of the rectangle
in gamma..(PI - gamma).toFloat() -> { y / sin(alpha) }
// ray from centre cuts the left edge of the rectangle
in (PI - gamma)..(PI + gamma) -> { x / -cos(alpha) }
// ray from centre cuts the bottom edge of the rectangle
in (PI + gamma)..(2*PI - gamma) -> { y / -sin(alpha) }
// default case (which shouldn't really happen)
else -> hypot(x, y)
}
val centerOffsetX = cos(alpha) * gradientLength / 2
val centerOffsetY = sin(alpha) * gradientLength / 2
drawRect(
brush = Brush.linearGradient(
colors = colors,
// negative here so that 0 degrees is left -> right
and 90 degrees is top -> bottom
start = Offset(center.x - centerOffsetX,center.y - centerOffsetY),
end = Offset(center.x + centerOffsetX, center.y + centerOffsetY)
),
size = size
)
}
)
Old answer
This was my final solution based on @Ehan msz's code. I tweaked his solution so that 0 degrees corresponds to a left-to-right gradient direction, and 90 degrees corresponds to a top-to-bottom direction.
fun Modifier.angledGradient(colors: List<Color>, degrees: Float) = this.then(
Modifier.drawBehind {
val rad = (degrees * PI / 180).toFloat()
val diagonal = sqrt(size.width * size.width + size.height * size.height)
val centerOffsetX = cos(rad) * diagonal / 2
val centerOffsetY = sin(rad) * diagonal / 2
// negative so that 0 degrees is left -> right and 90 degrees is top -> bottom
val startOffset = Offset(
x = (center.x - centerOffsetX).coerceIn(0f, size.width),
y = (center.y - centerOffsetY).coerceIn(0f, size.height)
)
val endOffset = Offset(
x = (center.x + centerOffsetX).coerceIn(0f, size.width),
y = (center.y + centerOffsetY).coerceIn(0f, size.height)
)
drawRect(
brush = Brush.linearGradient(
colors = colors,
start = startOffset,
end = endOffset
),
size = size
)
}
Solution 2:[2]
You can use Modifier.drawBehind()
and calculate the coordinates of points to draw gradient color.
fun Modifier.gradientBackground(colors: List<Color>, angle: Float) = this.then(
Modifier.drawBehind {
val angleRad = angle / 180f * PI
val x = cos(angleRad).toFloat() //Fractional x
val y = sin(angleRad).toFloat() //Fractional y
val radius = sqrt(size.width.pow(2) + size.height.pow(2)) / 2f
val offset = center + Offset(x * radius, y * radius)
val exactOffset = Offset(
x = min(offset.x.coerceAtLeast(0f), size.width),
y = size.height - min(offset.y.coerceAtLeast(0f), size.height)
)
drawRect(
brush = Brush.linearGradient(
colors = colors,
start = Offset(size.width, size.height) - exactOffset,
end = exactOffset
),
size = size
)
}
)
example:
Modifier
.gradientBackground(listOf(Color.Red, Color.Green), angle = 45f)
Solution 3:[3]
You can use the parameters start
and end
to achieve a 45 degree angle.
Something like:
val gradient45 = Brush.linearGradient(
colors = listOf(Color.Yellow, Color.Red),
start = Offset(0f, Float.POSITIVE_INFINITY),
end = Offset(Float.POSITIVE_INFINITY, 0f)
)
Solution 4:[4]
The first solution has an error, as offset can be negative values as well (you will notice it when you check with 60 degree angle and compare with CSS gradient).
I've made a universal solution that supports any angle, and wrote a medium article about it (thanks to the first solution for the idea). Check it out if necessary
Solution 5:[5]
I created a GradientOffset class which lets you rotate gradients by 45 degrees.
Enum that stores rotation angles and data class that stores Offset
s.
/**
* Offset for [Brush.linearGradient] to rotate gradient depending on [start] and [end] offsets.
*/
data class GradientOffset(val start: Offset, val end: Offset)
enum class GradientAngle {
CW0, CW45, CW90, CW135, CW180, CW225, CW270, CW315
}
Rotation function
/**
*
* Get a [GradientOffset] that rotate a gradient clockwise with specified angle in degrees.
* Default value for [GradientOffset] is [GradientAngle.CW0] which is 0 degrees
* that returns a horizontal gradient.
*
* Get start and end offsets that are limited between [0f, Float.POSITIVE_INFINITY] in x and
* y axes wrapped in [GradientOffset].
* Infinity is converted to Composable width on x axis, height on y axis in shader.
*
* Default angle for [Brush.linearGradient] when no offset is 0 degrees in Compose ,
* [Brush.verticalGradient] is [Brush.linearGradient] with 90 degrees.
*
* ```
* 0 degrees
* start = Offset(0f,0f),
* end = Offset(Float.POSITIVE_INFINITY,0f)
*
* 45 degrees
* start = Offset(0f, Float.POSITIVE_INFINITY),
* end = Offset(Float.POSITIVE_INFINITY, 0f)
*
* 90 degrees
* start = Offset(0f, Float.POSITIVE_INFINITY),
* end = Offset.Zero
*
* 135 degrees
* start = Offset.Infinity,
* end = Offset.Zero
*
* 180 degrees
* start = Offset(Float.POSITIVE_INFINITY, 0f),
* end = Offset.Zero,
*
* ```
*/
fun GradientOffset(angle: GradientAngle = GradientAngle.CW0): GradientOffset {
return when (angle) {
GradientAngle.CW45 -> GradientOffset(
start = Offset.Zero,
end = Offset.Infinite
)
GradientAngle.CW90 -> GradientOffset(
start = Offset.Zero,
end = Offset(0f, Float.POSITIVE_INFINITY)
)
GradientAngle.CW135 -> GradientOffset(
start = Offset(Float.POSITIVE_INFINITY, 0f),
end = Offset(0f, Float.POSITIVE_INFINITY)
)
GradientAngle.CW180 -> GradientOffset(
start = Offset(Float.POSITIVE_INFINITY, 0f),
end = Offset.Zero,
)
GradientAngle.CW225 -> GradientOffset(
start = Offset.Infinite,
end = Offset.Zero
)
GradientAngle.CW270 -> GradientOffset(
start = Offset(0f, Float.POSITIVE_INFINITY),
end = Offset.Zero
)
GradientAngle.CW315 -> GradientOffset(
start = Offset(0f, Float.POSITIVE_INFINITY),
end = Offset(Float.POSITIVE_INFINITY, 0f)
)
else -> GradientOffset(
start = Offset.Zero,
end = Offset(Float.POSITIVE_INFINITY, 0f)
)
}
}
Usage is very simple, rotate any gradient clockwise by setting GradientAngle.CW
// Offsets for gradients based on selected angle
var gradientOffset by remember {
mutableStateOf(GradientOffset(GradientAngle.CW45))
}
Brush.linearGradient(
listOf(Color.Red, Color.Green, Color.Blue),
start = gradientOffset.start,
end = gradientOffset.end
)
Result
Repo link if you wish to try it
Note
To have gradients that can rotate to any angle you need to implement your own LinearGradient
class that extends ShaderBrush
then calculate rotation to position using simple trigonometry.
Solution 6:[6]
You need to define a directional vector using the offsets. For any angle, you would need to explicitly specify % within the maximum drawing area in respect to which way the vector should be pointing.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | |
Solution 3 | Gabriele Mariotti |
Solution 4 | |
Solution 5 | |
Solution 6 | EpicPandaForce |