我正在尝试在Jetpack Compose中绘制渐变背景,并且希望无论我要绘制的对象的形状如何,渐变都具有固定的角度。
然而,使用Modifier.background(brush=...)
,我能找到的最好方法是linearGradient
,它从梯度的固定起点和终点计算角度。
例如,有没有办法指定我想要45度角的渐变,而不知道最终大小会是多少?
编辑:我想要一个可以适用于任何给定角度的解决方案,而不仅仅是45度。
我正在尝试在Jetpack Compose中绘制渐变背景,并且希望无论我要绘制的对象的形状如何,渐变都具有固定的角度。
然而,使用Modifier.background(brush=...)
,我能找到的最好方法是linearGradient
,它从梯度的固定起点和终点计算角度。
例如,有没有办法指定我想要45度角的渐变,而不知道最终大小会是多少?
编辑:我想要一个可以适用于任何给定角度的解决方案,而不仅仅是45度。
Modifier.drawBehind()
并计算点的坐标来绘制渐变颜色。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
)
}
)
示例:
Modifier
.gradientBackground(listOf(Color.Red, Color.Green), angle = 45f)
Offset
。 /**
* 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
}
旋转函数
/**
*
* 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)
)
}
}
使用非常简单,只需将任何渐变色顺时针旋转,设置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
)
结果
仓库链接,如果您想尝试
为了实现可以旋转到任何角度的渐变,需要实现自己的LinearGradient
类并扩展ShaderBrush
,然后使用简单的三角函数计算旋转到位置。
Float.MAX_VALUE
或Float.POSITIVE_INFINITY
作为Offset
的动画值使用:https://stackoverflow.com/questions/76779131/animate-linear-gradient-brush-infinitely-and-reversely-in-compose - user924我意识到原始代码中存在错误,会扭曲梯度角度。需要更多的三角学知识来约束梯度的起点和终点在画布区域内(如果这是所需的),同时保留梯度角度。以下是更新后的解决方案,附带ASCII艺术。
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
)
}
)
这是我基于 @Ehan msz 的代码的最终解决方案。我修改了他的解决方案,使得0度对应从左到右的渐变方向,而90度对应从上到下的方向。
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
)
}
fun Modifier.angledGradientBackground(colorStops: Array<Pair<Float, Color>>, degrees: Float) = this.then(
drawBehind {
val (x, y) = size
val degreesNormalised = (degrees % 360).let { if (it < 0) it + 360 else it }
val angleN = 90 - (degreesNormalised % 90)
val angleNRad = Math.toRadians(angleN.toDouble())
val hypot1 = abs((y * cos(angleNRad)))
val x1 = (abs((hypot1 * sin(angleNRad)))).toFloat()
val y1 = (abs((hypot1 * cos(angleNRad)))).toFloat()
val hypot2 = abs((x * cos(angleNRad)))
val x2 = (abs((hypot2 * cos(angleNRad)))).toFloat()
val y2 = (abs((hypot2 * sin(angleNRad)))).toFloat()
val offset = when {
degreesNormalised > 0f && degreesNormalised < 90f -> arrayOf(
0f - x1, y - y1,
x - x2, y + y2)
degreesNormalised == 90f -> arrayOf(0f, 0f, 0f, y)
degreesNormalised > 90f && degreesNormalised < 180f -> arrayOf(
0f + x2, 0f - y2,
0f - x1, y - y1)
degreesNormalised == 180f -> arrayOf(x, 0f, 0f, 0f)
degreesNormalised > 180f && degreesNormalised < 270f -> arrayOf(
x + x1, 0f + y1,
0f + x2, 0f - y2)
degreesNormalised == 270f -> arrayOf(x, y, x, 0f)
degreesNormalised > 270f && degreesNormalised < 360f -> arrayOf(
x - x2, y + y2,
x + x1, 0f + y1)
else -> arrayOf(0f, y, x, y)
}
drawRect(
brush = androidx.compose.ui.graphics.Brush.linearGradient(
colorStops = colorStops,
/*colors = colors,*/
start = Offset(offset[0],offset[1]),
end = Offset(offset[2], offset[3])
),
size = size
)
}
)
linearGradient
的文档说Offset.Infinite
指的是绘图区域的最右侧和底部的位置,我理解为您传递的参数是指绘图区域的右上角和左下角。因此,对于矩形而言,它不会是45度。 - machfour