Jetpack Compose 中的倾斜渐变背景。

15

我正在尝试在Jetpack Compose中绘制渐变背景,并且希望无论我要绘制的对象的形状如何,渐变都具有固定的角度。

然而,使用Modifier.background(brush=...),我能找到的最好方法是linearGradient,它从梯度的固定起点和终点计算角度。

例如,有没有办法指定我想要45度角的渐变,而不知道最终大小会是多少?

编辑:我想要一个可以适用于任何给定角度的解决方案,而不仅仅是45度。

7个回答

18

您可以使用参数startend来实现45度角度。

例如:

val gradient45 = Brush.linearGradient(
    colors = listOf(Color.Yellow, Color.Red),
    start = Offset(0f, Float.POSITIVE_INFINITY),
    end = Offset(Float.POSITIVE_INFINITY, 0f)
)

这里输入图片描述


1
我不确定如果形状不是正方形,这是否会给出45度。linearGradient的文档说Offset.Infinite指的是绘图区域的最右侧和底部的位置,我理解为您传递的参数是指绘图区域的右上角和左下角。因此,对于矩形而言,它不会是45度。 - machfour
1
另外,我正在寻找一种更通用的解决方案,可以用于其他角度,而不仅仅是45度。对于问题中允许特殊情况解决方案的措辞,我表示歉意。 - machfour

15
您可以使用 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)

1


1
太棒了。我希望有一种更快的方法来完成这个任务,但这正是我需要的方法。谢谢!我添加了另一个答案,其中包含你的代码的稍微改编版本,但我会接受你的答案 :) - machfour
这个答案对我也很有帮助,但是它有一个错误,请查看此解决方案,如果您感兴趣 https://dev59.com/ZcHqa4cB1Zd3GeqPwUgy#71577924 - Mukhtar Bimurat
Gabriele Mariotti的答案更简洁。 - kc_dev

10
我创建了一个GradientOffset类,可以让您将渐变旋转45度。
枚举存储旋转角度和存储偏移量的数据类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_VALUEFloat.POSITIVE_INFINITY作为Offset的动画值使用:https://stackoverflow.com/questions/76779131/animate-linear-gradient-brush-infinitely-and-reversely-in-compose - user924

8

编辑于2022年4月6日

我意识到原始代码中存在错误,会扭曲梯度角度。需要更多的三角学知识来约束梯度的起点和终点在画布区域内(如果这是所需的),同时保留梯度角度。以下是更新后的解决方案,附带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
    )
}

4

第一个解决方案存在错误,因为偏移量也可以是负值(当您使用60度角检查并与CSS渐变进行比较时,您将注意到它)。

我制作了一个通用的解决方案,支持任何角度,并写了一篇中等文章(感谢第一个解决方案提供的思路)。如有必要,请查看它。


你能解释一下错误具体是什么吗?我仍在使用我自己回答中发布的代码 - 这是你提到的代码的变体 - 在我的应用程序中制作一个60度的渐变,它似乎正好符合我的需求。 - machfour
2
在 CSS 渐变中,startOffset 和 endOffset 可以是负值(详见此处,红蓝点:https://codepen.io/enbee81/pen/zYrXVGo),但在第一个解决方案中从未发生过。 - Mukhtar Bimurat
1
我明白你的意思,如果你想忠实地复制CSS行为,那么你应该意识到这一点。但这并不是一个真正的“错误”,而是一种设计选择,将渐变的起点和终点限制在可见区域内。这取决于你想看到颜色极端程度有多少。对于我的应用程序,我希望更多地看到它们,所以限制是有意义的。 - machfour
3
实际上,当我阅读您发布的文章时,我注意到我的解决方案和您指出的确实存在错误,但并不是偏移量不能为负数。.coerce*()运算符是错误的选择,因为它只影响一个坐标,这会扭曲梯度角度。我已经更新了我的答案,以包含更正后的代码。 - machfour

0
你需要使用偏移量来定义一个方向向量。对于任何角度,你都需要明确指定该向量应该指向最大绘图区域的哪个方向,以百分比的形式表示。

enter image description here


有没有更多相关的资源可以阅读? - Roshan

0
重新制作了machfour的变体,使得渐变超出了背景(这样就没有与下面的例子一样的单色区域)。

> 图像

    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
        )
    }
)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接