android Jetpack Compose中的倾斜渐变背景

ryevplcw  于 2023-05-05  发布在  Android
关注(0)|答案(7)|浏览(148)

我试图在Jetpack Compose中绘制渐变背景,我希望渐变具有固定的Angular ,而不管我正在绘制的对象的形状如何。
然而,使用Modifier.background(brush=...),我能找到的最好的是linearGradient,它从梯度的固定起点和终点计算Angular 。
例如,有没有一种方法可以指定我想要一个45度角的渐变,而不知道它的最终大小?
编辑:我想要一个解决方案,可以为任何给定的Angular 工作,而不仅仅是45度。

sg3maiej

sg3maiej1#

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

4xy9mtcn

4xy9mtcn2#

可以使用参数startend来实现45度角。
类似于:

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

njthzxwz

njthzxwz3#

编辑2022-04-06

我意识到原始代码中有一个错误,这扭曲了梯度角。需要更多的三角函数,以便将渐变的开始和结束限制在画布区域内(如果需要的话),同时还保留渐变Angular 。这里是更新的解决方案,与奖金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
    )
}
cgh8pdjw

cgh8pdjw4#

我创建了一个GradientOffset类,它可以让你将渐变旋转45度。
存储旋转Angular 的枚举和存储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
    }

旋转函数

/**
 *
 * 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
)

结果

Repo link如果你想试试

注意事项

要使渐变可以旋转到任何Angular ,您需要实现自己的LinearGradient类,它扩展了ShaderBrush,然后使用简单的三角函数计算旋转到位置。

des4xlb0

des4xlb05#

first解决方案有一个错误,因为偏移量也可以是负值(当您使用60度角检查并与CSS gradient进行比较时,您会注意到这一点)。
我已经做了一个通用的解决方案,支持任何Angular ,并写了一个medium article(感谢第一个解决方案的想法)。有必要的话可以查一下

guz6ccqo

guz6ccqo6#

需要使用偏移定义方向向量。对于任何Angular ,都需要明确指定最大绘图区域内的%,以确定矢量应指向的方向。

tcomlyy6

tcomlyy67#

修改了machfour变体,使渐变超出背景(这样就没有单色区域,如下例所示)。

  • Mukhtar Bimurat有一个更完整的版本,但我很好奇自己尝试一下

> image

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

相关问题