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

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.Repeated : край повторяется от последнего цвета к первому. TileMode повторяется
TileMode.Mirror : край зеркально отображается от последнего цвета к первому. Зеркало в режиме плитки
TileMode.Clamp : край фиксируется до конечного цвета. Затем он окрасит остальную часть региона в ближайший цвет. Зажим режима плитки
TileMode.Decal : рендеринг только до размера границ. TileMode.Decal использует прозрачный черный цвет для выборки содержимого за пределами исходных границ, тогда как TileMode.Clamp производит выборку цвета края. Наклейка режима плитки

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

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

Если вы знаете размер области, в которой будет рисоваться кисть, вы можете установить 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 Brush используется по-разному
Рис. 6. Использование кисти ImageShader Brush для рисования фона, текста и круга.

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

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

Кисть AGSL RuntimeShader

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

Чтобы создать шейдерную кисть, сначала определите шейдер как строку шейдера 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.

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

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

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