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

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

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

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

Базовым модификатором для отрисовки является drawWithContent , где вы можете определить порядок отрисовки вашего Composable и команды отрисовки, выполняемые внутри модификатора. drawBehind — это удобная обертка над drawWithContent , которая устанавливает порядок отрисовки таким образом, чтобы объекты находились позади содержимого Composable. 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 : параметры rotationX, rotationY и rotationZ заданы для изображения с помощью 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 : Клип, примененный к составному блоку Box.

Если затем применить 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))
    )
}

Clip применяется поверх преобразования graphicsLayer.
Рисунок 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 . Она отрисовывает слой в буфер за пределами экрана, если alpha меньше 1.0f или задан параметр RenderEffect . Всякий раз, когда alpha меньше 1f, автоматически создается слой композитинга для отрисовки содержимого, а затем отрисовки этого буфера за пределами экрана в целевом месте с соответствующим значением alpha. Установка параметра RenderEffect или overscroll всегда отрисовывает содержимое в буфер за пределами экрана независимо от заданной 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 и без установленной стратегии композитинга.
Рисунок 13 : Применение Modifier.drawWithContent к изображению, отображающему круг, с параметром BlendMode.Clear и без установленной CompositingStrategy.

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

Стратегия композитинга не задана, используется 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 — внеэкранные фрагменты обрезаются в область, где автоматический режим этого не делает.
Рисунок 15 : CompositingStrategy.Auto против CompositingStrategy.Offscreen — кадрирование вне экрана происходит в той области, где автоматический режим этого не делает.
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 из Composable. Чтобы скопировать содержимое вашего Composable в 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 и пользовательской отрисовки можно найти в следующих ресурсах:

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}