Кисть: градиенты и шейдеры

Brush в Compose описывает, как что-то рисуется на экране: она определяет цвет(а), которые рисуются в области рисования (т. е. круг, квадрат, путь). Есть несколько встроенных кистей, которые полезны для рисования, например, LinearGradient , RadialGradient или простая кисть SolidColor .

Кисти можно использовать с вызовами отрисовки Modifier.background() , TextStyle или DrawScope для применения стиля рисования к отрисовываемому содержимому.

Например, горизонтальную градиентную кисть можно применить для рисования круга в DrawScope :

val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue))
Canvas(
    modifier = Modifier.size(200.dp),
    onDraw = {
        drawCircle(brush)
    }
)
Круг, нарисованный с помощью горизонтального градиента
Рисунок 1 : Круг, нарисованный с помощью горизонтального градиента

Градиентные кисти

Существует множество встроенных градиентных кистей, которые можно использовать для достижения различных эффектов градиента. Эти кисти позволяют указать список цветов, из которых вы хотите создать градиент.

Список доступных градиентных кистей и их соответствующих результатов:

Тип градиентной кисти Выход
Brush.horizontalGradient(colorList) Горизонтальный градиент
Brush.linearGradient(colorList) Линейный градиент
Brush.verticalGradient(colorList) Вертикальный градиент
Brush.sweepGradient(colorList)
Примечание: Чтобы получить плавный переход между цветами - установите последний цвет равным начальному цвету.
Градиент развертки
Brush.radialGradient(colorList) Радиальный градиент

Измените распределение цветов с помощью colorStops

Чтобы настроить отображение цветов в градиенте, можно настроить значение colorStops для каждого из них. colorStops следует указывать в виде дроби от 0 до 1. Значения больше 1 приведут к тому, что эти цвета не будут отображаться как часть градиента.

Вы можете настроить цветовые остановки на разное количество, например, больше или меньше одного цвета:

val colorStops = arrayOf(
    0.0f to Color.Yellow,
    0.2f to Color.Red,
    1f to Color.Blue
)
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(Brush.horizontalGradient(colorStops = colorStops))
)

Цвета распределяются с заданным смещением, как определено в паре colorStop , менее желтые, чем красные и синие.

Кисть с различными цветовыми ограничителями
Рисунок 2 : Кисть, настроенная на различные цветовые точки

Повторите узор с помощью TileMode

Каждая градиентная кисть имеет возможность установить TileMode . Вы можете не заметить TileMode , если не установили начало и конец градиента, так как по умолчанию он заполнит всю область. TileMode заполнит градиент только в том случае, если размер области больше размера кисти.

Следующий код повторит шаблон градиента 4 раза, поскольку endX установлен на 50.dp , а размер установлен на 200.dp :

val listColors = listOf(Color.Yellow, Color.Red, Color.Blue)
val tileSize = with(LocalDensity.current) {
    50.dp.toPx()
}
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(
            Brush.horizontalGradient(
                listColors,
                endX = tileSize,
                tileMode = TileMode.Repeated
            )
        )
)

Ниже приведена таблица, в которой подробно описано, что делают различные режимы плитки для приведенного выше примера HorizontalGradient :

TileMode Выход
TileMode.Repeated : край повторяется от последнего цвета до первого. TileMode Повторяется
TileMode.Mirror : Край зеркально отражается от последнего цвета к первому. Зеркало TileMode
TileMode.Clamp : Край фиксируется на конечном цвете. Затем он будет рисовать ближайший цвет для остальной части региона. Зажим для плиточного режима
TileMode.Decal : Рендеринг только до размера границ. TileMode.Decal использует прозрачный черный цвет для выборки содержимого за пределами исходных границ, тогда как TileMode.Clamp выбирает цвет края. Наклейка в режиме плитки

TileMode работает аналогичным образом для других направленных градиентов, разница заключается в направлении, в котором происходит повторение.

Изменить размер кисти

Если вы знаете размер области, в которой будет нарисована ваша кисть, вы можете установить tile endX , как мы видели выше в разделе TileMode . Если вы находитесь в DrawScope , вы можете использовать его свойство size , чтобы получить размер области.

Если вы не знаете размер области рисования (например, если Brush назначена на текст), вы можете расширить Shader и использовать размер области рисования в функции createShader .

В этом примере разделите размер на 4, чтобы повторить узор 4 раза:

val listColors = listOf(Color.Yellow, Color.Red, Color.Blue)
val customBrush = remember {
    object : ShaderBrush() {
        override fun createShader(size: Size): Shader {
            return LinearGradientShader(
                colors = listColors,
                from = Offset.Zero,
                to = Offset(size.width / 4f, 0f),
                tileMode = TileMode.Mirror
            )
        }
    }
}
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(customBrush)
)

Размер шейдера делится на 4
Рисунок 3 : Размер шейдера, деленный на 4

Вы также можете изменить размер кисти любого другого градиента, например радиального градиента. Если вы не укажете размер и центр, градиент займет все границы DrawScope , а центр радиального градиента по умолчанию будет соответствовать центру границ DrawScope . Это приведет к тому, что центр радиального градиента будет отображаться как центр меньшего измерения (ширины или высоты):

Box(
    modifier = Modifier
        .fillMaxSize()
        .background(
            Brush.radialGradient(
                listOf(Color(0xFF2be4dc), Color(0xFF243484))
            )
        )
)

Радиальный градиент установлен без изменения размера
Рисунок 4 : Радиальный градиент установлен без изменения размера

Если изменить радиальный градиент, установив максимальный размер радиуса, можно увидеть, что эффект радиального градиента становится лучше:

val largeRadialGradient = object : ShaderBrush() {
    override fun createShader(size: Size): Shader {
        val biggerDimension = maxOf(size.height, size.width)
        return RadialGradientShader(
            colors = listOf(Color(0xFF2be4dc), Color(0xFF243484)),
            center = size.center,
            radius = biggerDimension / 2f,
            colorStops = listOf(0f, 0.95f)
        )
    }
}

Box(
    modifier = Modifier
        .fillMaxSize()
        .background(largeRadialGradient)
)

Больший радиус радиального градиента в зависимости от размера области
Рисунок 5 : Больший радиус радиального градиента в зависимости от размера области

Стоит отметить, что фактический размер, который передается в создание шейдера, определяется из того, где он вызывается. По умолчанию Brush перераспределит свой Shader внутренне, если размер отличается от последнего создания Brush или если изменился объект состояния, используемый при создании шейдера.

Следующий код создает шейдер три раза с разными размерами, поскольку изменяется размер области рисования:

val colorStops = arrayOf(
    0.0f to Color.Yellow,
    0.2f to Color.Red,
    1f to Color.Blue
)
val brush = Brush.horizontalGradient(colorStops = colorStops)
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .drawBehind {
            drawRect(brush = brush) // will allocate a shader to occupy the 200 x 200 dp drawing area
            inset(10f) {
      /* Will allocate a shader to occupy the 180 x 180 dp drawing area as the
       inset scope reduces the drawing  area by 10 pixels on the left, top, right,
      bottom sides */
                drawRect(brush = brush)
                inset(5f) {
        /* will allocate a shader to occupy the 170 x 170 dp drawing area as the
         inset scope reduces the  drawing area by 5 pixels on the left, top,
         right, bottom sides */
                    drawRect(brush = brush)
                }
            }
        }
)

Используйте изображение как кисть

Чтобы использовать ImageBitmap в качестве Brush , загрузите изображение как ImageBitmap и создайте кисть ImageShader :

val imageBrush =
    ShaderBrush(ImageShader(ImageBitmap.imageResource(id = R.drawable.dog)))

// Use ImageShader Brush with background
Box(
    modifier = Modifier
        .requiredSize(200.dp)
        .background(imageBrush)
)

// Use ImageShader Brush with TextStyle
Text(
    text = "Hello Android!",
    style = TextStyle(
        brush = imageBrush,
        fontWeight = FontWeight.ExtraBold,
        fontSize = 36.sp
    )
)

// Use ImageShader Brush with DrawScope#drawCircle()
Canvas(onDraw = {
    drawCircle(imageBrush)
}, modifier = Modifier.size(200.dp))

Кисть применяется к нескольким различным типам рисунков: фон, текст и холст. Это выводит следующее:

Кисть ImageShader используется по-разному
Рисунок 6 : Использование кисти ImageShader для рисования фона, текста и круга.

Обратите внимание, что текст теперь также визуализируется с использованием ImageBitmap для закрашивания пикселей текста.

Расширенный пример: Пользовательская кисть

Кисть AGSL RuntimeShader

AGSL предлагает подмножество возможностей шейдеров GLSL . Шейдеры можно писать на AGSL и использовать с кистью в Compose.

Чтобы создать кисть Shader, сначала определите Shader как строку шейдера AGSL:

@Language("AGSL")
val CUSTOM_SHADER = """
    uniform float2 resolution;
    layout(color) uniform half4 color;
    layout(color) uniform half4 color2;

    half4 main(in float2 fragCoord) {
        float2 uv = fragCoord/resolution.xy;

        float mixValue = distance(uv, vec2(0, 1));
        return mix(color, color2, mixValue);
    }
""".trimIndent()

Шейдер выше берет два входных цвета, вычисляет расстояние от нижнего левого угла ( vec2(0, 1) ) области рисования и mix два цвета на основе расстояния. Это создает эффект градиента.

Затем создайте Shader Brush и задайте параметры resolution — размер области рисования, а также color и color2 которые вы хотите использовать в качестве входных данных для вашего пользовательского градиента:

val Coral = Color(0xFFF3A397)
val LightYellow = Color(0xFFF8EE94)

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable
@Preview
fun ShaderBrushExample() {
    Box(
        modifier = Modifier
            .drawWithCache {
                val shader = RuntimeShader(CUSTOM_SHADER)
                val shaderBrush = ShaderBrush(shader)
                shader.setFloatUniform("resolution", size.width, size.height)
                onDrawBehind {
                    shader.setColorUniform(
                        "color",
                        android.graphics.Color.valueOf(
                            LightYellow.red, LightYellow.green,
                            LightYellow
                                .blue,
                            LightYellow.alpha
                        )
                    )
                    shader.setColorUniform(
                        "color2",
                        android.graphics.Color.valueOf(
                            Coral.red,
                            Coral.green,
                            Coral.blue,
                            Coral.alpha
                        )
                    )
                    drawRect(shaderBrush)
                }
            }
            .fillMaxWidth()
            .height(200.dp)
    )
}

Запустив это, вы увидите на экране следующее:

Пользовательский шейдер AGSL, работающий в Compose
Рисунок 7 : Пользовательский шейдер AGSL, работающий в Compose

Стоит отметить, что с шейдерами можно делать гораздо больше, чем просто градиенты, поскольку все это основано на математических вычислениях. Для получения дополнительной информации об AGSL ознакомьтесь с документацией AGSL.

Дополнительные ресурсы

Дополнительные примеры использования Brush в Compose можно найти на следующих ресурсах:

{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %}