Модификаторы графики

Помимо компонуемого элемента Canvas , Compose предлагает несколько полезных графических Modifiers , которые помогают отрисовывать пользовательское содержимое. Эти модификаторы полезны, поскольку их можно применять к любому компонуемому элементу.

Модификаторы рисования

Все команды рисования в Compose выполняются с помощью модификатора рисования. В Compose есть три основных модификатора рисования:

Базовым модификатором для рисования является drawWithContent , где вы можете определить порядок отрисовки вашего компонуемого объекта и команды рисования, выдаваемые внутри модификатора. drawBehind — это удобная оболочка для drawWithContent , в которой порядок отрисовки установлен за содержимым компонуемого объекта. drawWithCache вызывает внутри себя либо onDrawBehind , либо onDrawWithContent и предоставляет механизм кэширования созданных в них объектов.

Modifier.drawWithContent : выбор порядка рисования

Modifier.drawWithContent позволяет выполнять операции DrawScope до или после содержимого компонуемого объекта. Обязательно вызовите drawContent для последующего отображения самого содержимого компонуемого объекта. С помощью этого модификатора вы можете задать порядок операций, чтобы отрисовывать контент до или после ваших собственных операций рисования.

Например, если вы хотите отобразить радиальный градиент поверх своего контента, чтобы создать эффект замочной скважины фонарика в пользовательском интерфейсе, вы можете сделать следующее:

var pointerOffset by remember {
    mutableStateOf(Offset(0f, 0f))
}
Column(
    modifier = Modifier
        .fillMaxSize()
        .pointerInput("dragging") {
            detectDragGestures { change, dragAmount ->
                pointerOffset += dragAmount
            }
        }
        .onSizeChanged {
            pointerOffset = Offset(it.width / 2f, it.height / 2f)
        }
        .drawWithContent {
            drawContent()
            // draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI.
            drawRect(
                Brush.radialGradient(
                    listOf(Color.Transparent, Color.Black),
                    center = pointerOffset,
                    radius = 100.dp.toPx(),
                )
            )
        }
) {
    // Your composables here
}

Рисунок 1 : Modifier.drawWithContent, используемый поверх Composable для создания пользовательского интерфейса по типу фонарика.

Modifier.drawBehind : Рисование позади компонуемого объекта

Modifier.drawBehind позволяет выполнять операции DrawScope за компонуемым содержимым, отображаемым на экране. Если взглянуть на реализацию Canvas , можно заметить, что это всего лишь удобная обёртка вокруг Modifier.drawBehind .

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

Text(
    "Hello Compose!",
    modifier = Modifier
        .drawBehind {
            drawRoundRect(
                Color(0xFFBBAAEE),
                cornerRadius = CornerRadius(10.dp.toPx())
            )
        }
        .padding(4.dp)
)

Что дает следующий результат:

Текст и фон, нарисованные с помощью Modifier.drawBehind
Рисунок 2 : Текст и фон, нарисованные с помощью Modifier.drawBehind

Modifier.drawWithCache : Рисование и кэширование объектов рисования

Modifier.drawWithCache кэширует создаваемые внутри него объекты. Объекты кэшируются до тех пор, пока размер области рисования остаётся неизменным или считываемые объекты состояния не изменяются. Этот модификатор полезен для повышения производительности вызовов отрисовки, поскольку позволяет избежать необходимости перераспределения объектов (таких как Brush, Shader, Path и т. д.), создаваемых при отрисовке.

В качестве альтернативы можно кэшировать объекты с помощью remember , вне модификатора. Однако это не всегда возможно, поскольку у вас не всегда есть доступ к композиции. Использование drawWithCache может оказаться более производительным, если объекты используются только для отрисовки.

Например, если вы создаете Brush для рисования градиента за Text , использование drawWithCache кэширует объект Brush до тех пор, пока не изменится размер области рисования:

Text(
    "Hello Compose!",
    modifier = Modifier
        .drawWithCache {
            val brush = Brush.linearGradient(
                listOf(
                    Color(0xFF9E82F0),
                    Color(0xFF42A5F5)
                )
            )
            onDrawBehind {
                drawRoundRect(
                    brush,
                    cornerRadius = CornerRadius(10.dp.toPx())
                )
            }
        }
)

Кэширование объекта Brush с помощью drawWithCache
Рисунок 3 : Кэширование объекта Brush с помощью drawWithCache

Графические модификаторы

Modifier.graphicsLayer : Применяет преобразования к компонуемым объектам

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

  • Изоляция инструкций отрисовки (аналогично RenderNode ). Инструкции отрисовки, захваченные как часть слоя, могут быть эффективно повторно выданы конвейером рендеринга без повторного выполнения кода приложения.
  • Преобразования, применяемые ко всем инструкциям рисования, содержащимся в слое.
  • Растеризация для возможностей композиции. При растеризации слоя выполняются его инструкции по отрисовке, а выходные данные записываются во внеэкранный буфер. Компоновка такого буфера для последующих кадров выполняется быстрее, чем выполнение отдельных инструкций, но при применении преобразований, таких как масштабирование или поворот, он будет вести себя как растровое изображение.

Трансформации

Modifier.graphicsLayer обеспечивает изоляцию инструкций по отрисовке; например, с помощью Modifier.graphicsLayer можно применять различные преобразования. Их можно анимировать или изменять без необходимости повторного выполнения лямбда-выражения отрисовки.

Modifier.graphicsLayer не изменяет измеренный размер или положение вашего компонуемого объекта, поскольку влияет только на фазу отрисовки. Это означает, что компонуемый объект может перекрывать другие, если он выходит за границы макета.

С помощью этого модификатора можно применять следующие преобразования:

Масштаб - увеличение размера

scaleX и scaleY увеличивают или уменьшают содержимое по горизонтали или вертикали соответственно. Значение 1.0f означает отсутствие изменения масштаба, значение 0.5f — уменьшение вдвое.

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.scaleX = 1.2f
            this.scaleY = 0.8f
        }
)

Рисунок 4 : scaleX и scaleY, применяемые к составному изображению
Перевод

translationX и translationY можно изменять с помощью graphicsLayer , translationX перемещает компонуемый объект влево или вправо. translationY перемещает компонуемый объект вверх или вниз.

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.translationX = 100.dp.toPx()
            this.translationY = 10.dp.toPx()
        }
)

Рисунок 5 : translationX и translationY, примененные к изображению с помощью Modifier.graphicsLayer
Вращение

Установите rotationX для горизонтального вращения, rotationY для вертикального вращения и rotationZ для вращения по оси Z (стандартное вращение). Это значение указывается в градусах (0–360).

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.rotationX = 90f
            this.rotationY = 275f
            this.rotationZ = 180f
        }
)

Рисунок 6 : вращение по осям X, Y и Z, заданные для изображения с помощью Modifier.graphicsLayer
Источник

Можно указать начало transformOrigin . Оно будет использоваться в качестве точки, от которой будут выполняться преобразования. Во всех примерах до сих пор использовался TransformOrigin.Center с координатами (0.5f, 0.5f) . Если указать начало координат в точке (0f, 0f) , преобразования начнутся с левого верхнего угла компонуемого объекта.

Если изменить начало координат с помощью преобразования rotationZ , можно увидеть, что элемент вращается вокруг верхнего левого угла компонуемого объекта:

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.transformOrigin = TransformOrigin(0f, 0f)
            this.rotationX = 90f
            this.rotationY = 275f
            this.rotationZ = 180f
        }
)

Рисунок 7 : Поворот применен с TransformOrigin, установленным на 0f, 0f

Клип и форма

Shape определяет контур, по которому будет обрезано содержимое при clip = true . В этом примере мы задаём двум блокам два разных обреза: один с помощью переменной clip graphicsLayer , а другой — с помощью удобной оболочки Modifier.clip .

Column(modifier = Modifier.padding(16.dp)) {
    Box(
        modifier = Modifier
            .size(200.dp)
            .graphicsLayer {
                clip = true
                shape = CircleShape
            }
            .background(Color(0xFFF06292))
    ) {
        Text(
            "Hello Compose",
            style = TextStyle(color = Color.Black, fontSize = 46.sp),
            modifier = Modifier.align(Alignment.Center)
        )
    }
    Box(
        modifier = Modifier
            .size(200.dp)
            .clip(CircleShape)
            .background(Color(0xFF4DB6AC))
    )
}

Содержимое первого поля (текст «Hello Compose») обрезается по форме круга:

Клип, примененный к компонуемому блоку
Рисунок 8 : Клип, примененный к компонуемому блоку

Если затем применить translationY к верхнему розовому кругу, то можно увидеть, что границы Composable остаются теми же, но круг рисуется под нижним кругом (и за его пределами).

Клип применен с переводом Y и красной рамкой для контура
Рисунок 9 : Клип применен с переводом Y и красной рамкой для контура

Чтобы обрезать компонуемый элемент по области, в которой он отрисован, можно добавить ещё один Modifier.clip(RectangleShape) в начало цепочки модификаторов. В этом случае содержимое останется внутри исходных границ.

Column(modifier = Modifier.padding(16.dp)) {
    Box(
        modifier = Modifier
            .clip(RectangleShape)
            .size(200.dp)
            .border(2.dp, Color.Black)
            .graphicsLayer {
                clip = true
                shape = CircleShape
                translationY = 50.dp.toPx()
            }
            .background(Color(0xFFF06292))
    ) {
        Text(
            "Hello Compose",
            style = TextStyle(color = Color.Black, fontSize = 46.sp),
            modifier = Modifier.align(Alignment.Center)
        )
    }

    Box(
        modifier = Modifier
            .size(200.dp)
            .clip(RoundedCornerShape(500.dp))
            .background(Color(0xFF4DB6AC))
    )
}

Клип, примененный поверх графического слоя. Преобразование слоя.
Рисунок 10 : Клип, примененный поверх преобразования GraphicsLayer

Альфа

Modifier.graphicsLayer можно использовать для установки alpha (непрозрачности) для всего слоя. 1.0f — полная непрозрачность, 0.0f — невидимость.

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "clock",
    modifier = Modifier
        .graphicsLayer {
            this.alpha = 0.5f
        }
)

Изображение с примененным альфа-каналом
Рисунок 11 : Изображение с применением альфа-канала

Стратегия композиции

Работа с альфа-каналом и прозрачностью может быть не так проста, как изменение одного значения альфа-канала. Помимо изменения альфа-канала, существует также возможность задать стратегию компоновки CompositingStrategy для graphicsLayer . CompositingStrategy определяет, как содержимое компонуемого объекта будет компоноваться (сочетаться) с другим содержимым, уже отображённым на экране.

Различные стратегии:

Авто (по умолчанию)

Стратегия компоновки определяется остальными параметрами graphicsLayer . Слой рендерится во внеэкранном буфере, если альфа меньше 1.0f или задано значение RenderEffect . Если альфа меньше 1f, автоматически создаётся композитный слой для рендеринга содержимого и последующей отрисовки этого внеэкранного буфера в месте назначения с соответствующим значением альфа. Установка RenderEffect или прокрутки всегда приводит к рендерингу содержимого во внеэкранном буфере независимо от установленного значения CompositingStrategy .

Закадровый

Содержимое компонуемого объекта всегда растеризуется во внеэкранную текстуру или битовое изображение перед рендерингом в целевую область. Это полезно для применения операций BlendMode для маскирования содержимого и для повышения производительности при рендеринге сложных наборов инструкций рисования.

Примером использования CompositingStrategy.Offscreen является BlendModes . Рассмотрим пример ниже. Допустим, вы хотите удалить части Image , которое можно компоновать, выполнив команду рисования с использованием BlendMode.Clear . Если вы не установите для compositingStrategy значение CompositingStrategy.Offscreen , BlendMode будет взаимодействовать со всем содержимым, находящимся под ним.

Image(
    painter = painterResource(id = R.drawable.dog),
    contentDescription = "Dog",
    contentScale = ContentScale.Crop,
    modifier = Modifier
        .size(120.dp)
        .aspectRatio(1f)
        .background(
            Brush.linearGradient(
                listOf(
                    Color(0xFFC5E1A5),
                    Color(0xFF80DEEA)
                )
            )
        )
        .padding(8.dp)
        .graphicsLayer {
            compositingStrategy = CompositingStrategy.Offscreen
        }
        .drawWithCache {
            val path = Path()
            path.addOval(
                Rect(
                    topLeft = Offset.Zero,
                    bottomRight = Offset(size.width, size.height)
                )
            )
            onDrawWithContent {
                clipPath(path) {
                    // this draws the actual image - if you don't call drawContent, it wont
                    // render anything
                    this@onDrawWithContent.drawContent()
                }
                val dotSize = size.width / 8f
                // Clip a white border for the content
                drawCircle(
                    Color.Black,
                    radius = dotSize,
                    center = Offset(
                        x = size.width - dotSize,
                        y = size.height - dotSize
                    ),
                    blendMode = BlendMode.Clear
                )
                // draw the red circle indication
                drawCircle(
                    Color(0xFFEF5350), radius = dotSize * 0.8f,
                    center = Offset(
                        x = size.width - dotSize,
                        y = size.height - dotSize
                    )
                )
            }
        }
)

Если установить CompositingStrategy в Offscreen , создаётся внеэкранная текстура для выполнения команд (применяя BlendMode только к содержимому этого компонуемого объекта). Затем она визуализируется поверх того, что уже отображается на экране, не затрагивая уже отрисованное содержимое.

Modifier.drawWithContent на изображении, показывающем круговую индикацию, с BlendMode.Clear внутри приложения
Рисунок 12 : Modifier.drawWithContent на изображении, показывающем круговую индикацию, с BlendMode.Clear и CompositingStrategy.Offscreen внутри приложения

Если вы не использовали CompositingStrategy.Offscreen , то в результате применения BlendMode.Clear все пиксели в целевой области будут очищены, независимо от того, что было установлено ранее, оставляя видимым буфер рендеринга окна (черный). Многие BlendModes , использующие альфа-канал, не будут работать должным образом без внеэкранного буфера. Обратите внимание на черное кольцо вокруг красного круга-индикатора:

Modifier.drawWithContent на изображении, отображающем круговую индикацию, с установленным BlendMode.Clear и без CompositingStrategy.
Рисунок 13 : Modifier.drawWithContent на изображении, показывающем круговую индикацию, с установленным BlendMode.Clear и без CompositingStrategy.

Для более подробного понимания: если бы у приложения был полупрозрачный фон окна, и вы не использовали CompositingStrategy.Offscreen , BlendMode взаимодействовал бы со всем приложением. Он бы очистил все пиксели, чтобы отобразить приложение или обои под ним, как в этом примере:

Не задан CompositingStrategy и используется BlendMode.Clear с приложением с полупрозрачным фоном окна. Розовые обои отображаются через область вокруг красного круга состояния.
Рисунок 14 : CompositingStrategy не задана, используется BlendMode.Clear с приложением с полупрозрачным фоном окна. Обратите внимание, как розовые обои отображаются через область вокруг красного круга состояния.

Стоит отметить, что при использовании CompositingStrategy.Offscreen создаётся внеэкранная текстура, размер которой совпадает с размером области рисования, и отображается на экране. Любые команды рисования, выполняемые с использованием этой стратегии, по умолчанию ограничиваются этой областью. Приведённый ниже фрагмент кода иллюстрирует различия при переключении на использование внеэкранных текстур:

@Composable
fun CompositingStrategyExamples() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .wrapContentSize(Alignment.Center)
    ) {
        // Does not clip content even with a graphics layer usage here. By default, graphicsLayer
        // does not allocate + rasterize content into a separate layer but instead is used
        // for isolation. That is draw invalidations made outside of this graphicsLayer will not
        // re-record the drawing instructions in this composable as they have not changed
        Canvas(
            modifier = Modifier
                .graphicsLayer()
                .size(100.dp) // Note size of 100 dp here
                .border(2.dp, color = Color.Blue)
        ) {
            // ... and drawing a size of 200 dp here outside the bounds
            drawRect(color = Color.Magenta, size = Size(200.dp.toPx(), 200.dp.toPx()))
        }

        Spacer(modifier = Modifier.size(300.dp))

        /* Clips content as alpha usage here creates an offscreen buffer to rasterize content
        into first then draws to the original destination */
        Canvas(
            modifier = Modifier
                // force to an offscreen buffer
                .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
                .size(100.dp) // Note size of 100 dp here
                .border(2.dp, color = Color.Blue)
        ) {
            /* ... and drawing a size of 200 dp. However, because of the CompositingStrategy.Offscreen usage above, the
            content gets clipped */
            drawRect(color = Color.Red, size = Size(200.dp.toPx(), 200.dp.toPx()))
        }
    }
}

CompositingStrategy.Auto против CompositingStrategy.Offscreen — закадровые клипы по области, где auto не работает
Рисунок 15 : CompositingStrategy.Auto и CompositingStrategy.Offscreen — закадровые клипы по области, где auto не работает
ModulateAlpha

Эта стратегия композиции модулирует альфа-канал для каждой инструкции отрисовки, записанной в graphicsLayer . Она не создаёт внеэкранный буфер для альфа-канала ниже 1.0f, если не задано свойство RenderEffect , поэтому может быть более эффективна при альфа-рендеринге. Однако она может давать иные результаты при перекрывающемся контенте. В случаях, когда заранее известно, что контент не перекрывается, это может обеспечить более высокую производительность, чем CompositingStrategy.Auto со значениями альфа-канала меньше 1.

Ниже приведен еще один пример различных стратегий композиции — применение различных значений альфы к разным частям компонуемых объектов и применение стратегии Modulate :

@Preview
@Composable
fun CompositingStrategy_ModulateAlpha() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(32.dp)
    ) {
        // Base drawing, no alpha applied
        Canvas(
            modifier = Modifier.size(200.dp)
        ) {
            drawSquares()
        }

        Spacer(modifier = Modifier.size(36.dp))

        // Alpha 0.5f applied to whole composable
        Canvas(
            modifier = Modifier
                .size(200.dp)
                .graphicsLayer {
                    alpha = 0.5f
                }
        ) {
            drawSquares()
        }
        Spacer(modifier = Modifier.size(36.dp))

        // 0.75f alpha applied to each draw call when using ModulateAlpha
        Canvas(
            modifier = Modifier
                .size(200.dp)
                .graphicsLayer {
                    compositingStrategy = CompositingStrategy.ModulateAlpha
                    alpha = 0.75f
                }
        ) {
            drawSquares()
        }
    }
}

private fun DrawScope.drawSquares() {

    val size = Size(100.dp.toPx(), 100.dp.toPx())
    drawRect(color = Red, size = size)
    drawRect(
        color = Purple, size = size,
        topLeft = Offset(size.width / 4f, size.height / 4f)
    )
    drawRect(
        color = Yellow, size = size,
        topLeft = Offset(size.width / 4f * 2f, size.height / 4f * 2f)
    )
}

val Purple = Color(0xFF7E57C2)
val Yellow = Color(0xFFFFCA28)
val Red = Color(0xFFEF5350)

ModulateAlpha применяет альфа-установку к каждой отдельной команде рисования
Рисунок 16 : ModulateAlpha применяет альфа-установку к каждой отдельной команде рисования

Записать содержимое компонуемого объекта в растровое изображение

Распространенный вариант использования — создание Bitmap из компонуемого объекта. Чтобы скопировать содержимое компонуемого объекта в Bitmap , создайте GraphicsLayer с помощью rememberGraphicsLayer() .

Перенаправьте команды рисования на новый слой с помощью drawWithContent() и graphicsLayer.record{} . Затем отрисуйте слой на видимом холсте с помощью drawLayer :

val coroutineScope = rememberCoroutineScope()
val graphicsLayer = rememberGraphicsLayer()
Box(
    modifier = Modifier
        .drawWithContent {
            // call record to capture the content in the graphics layer
            graphicsLayer.record {
                // draw the contents of the composable into the graphics layer
                this@drawWithContent.drawContent()
            }
            // draw the graphics layer on the visible canvas
            drawLayer(graphicsLayer)
        }
        .clickable {
            coroutineScope.launch {
                val bitmap = graphicsLayer.toImageBitmap()
                // do something with the newly acquired bitmap
            }
        }
        .background(Color.White)
) {
    Text("Hello Android", fontSize = 26.sp)
}

Вы можете сохранить растровое изображение на диск и поделиться им. Подробнее см. в полном фрагменте примера . Перед сохранением на диск обязательно проверьте наличие прав доступа к устройству.

Пользовательский модификатор чертежа

Чтобы создать собственный модификатор, реализуйте интерфейс DrawModifier . Это даст вам доступ к объекту ContentDrawScope , который аналогичен объекту, предоставляемому при использовании Modifier.drawWithContent() . Затем вы можете извлечь общие операции рисования в пользовательские модификаторы рисования, чтобы очистить код и предоставить удобные обёртки; например, Modifier.background() — это удобный DrawModifier .

Например, если вы хотите реализовать Modifier , который вертикально переворачивает содержимое, вы можете создать его следующим образом:

class FlippedModifier : DrawModifier {
    override fun ContentDrawScope.draw() {
        scale(1f, -1f) {
            this@draw.drawContent()
        }
    }
}

fun Modifier.flipped() = this.then(FlippedModifier())

Затем используйте этот перевернутый модификатор, примененный к Text :

Text(
    "Hello Compose!",
    modifier = Modifier
        .flipped()
)

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

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

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

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