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

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

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

Все команды рисования выполняются с помощью модификатора рисования в 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 : вращение по осям 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») обрезается по форме круга:

Клип, примененный к Box composable
Рисунок 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 или overscroll всегда рендерит содержимое в буфер за пределами экрана независимо от установленного CompositingStrategy .

Закадровый

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

Пример использования CompositingStrategy.OffscreenBlendModes . Взгляните на пример ниже. Допустим, вы хотите удалить части 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 , которые включают альфа, не будут работать так, как ожидается, без буфера offscreen. Обратите внимание на черное кольцо вокруг красного кругового индикатора:

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 из 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 и пользовательского рисования см. на следующих ресурсах:

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

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

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

Все команды рисования выполняются с помощью модификатора рисования в 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 : вращение по осям 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») обрезается по форме круга:

Клип, примененный к Box composable
Рисунок 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 или overscroll всегда рендерит содержимое в буфер за пределами экрана независимо от установленного CompositingStrategy .

Закадровый

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

Пример использования CompositingStrategy.OffscreenBlendModes . Взгляните на пример ниже. Допустим, вы хотите удалить части 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 , которые включают альфа, не будут работать так, как ожидается, без буфера offscreen. Обратите внимание на черное кольцо вокруг красного кругового индикатора:

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 из 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 и пользовательского рисования см. на следующих ресурсах:

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