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

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

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

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

Базовым модификатором для рисования является drawWithContent , где вы можете определить порядок отрисовки вашего Composable и команды рисования, выдаваемые внутри модификатора. 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. 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.

Зажим и форма

Форма определяет контур, по которому обрезается содержимое, когда clip = true . В этом примере мы установили два поля для двух разных клипов — один с помощью переменной клипа 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))
    )
}

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

Клип применен к составному блоку
Рис. 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))
    )
}

Клип применен поверх преобразования 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 . Он отображает слой во внеэкранном буфере, если альфа меньше 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 , включающих альфа-канал, не будут работать должным образом без закадрового буфера. Обратите внимание на черное кольцо вокруг красного круга-индикатора:

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

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