В дополнение к элементу 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 }
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.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()) ) } } )

Графические модификаторы
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 } )
Перевод
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() } )
Вращение
Установите 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 } )
Источник
Можно указать 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 } )
Обрежьте и придайте форму
Параметр 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») обрезается по контуру круга:

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

Чтобы обрезать составной элемент по области, в которой он нарисован, можно добавить еще один 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)) ) }

Альфа
Modifier.graphicsLayer позволяет задать alpha (прозрачность) для всего слоя. 1.0f означает полную непрозрачность, а 0.0f — невидимость.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "clock", modifier = Modifier .graphicsLayer { this.alpha = 0.5f } )

Стратегия композитинга
Работа с альфа-каналом и прозрачностью может быть не такой простой, как изменение одного значения альфа-канала. Помимо изменения альфа-канала, существует также возможность установить 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 только к содержимому этого композитного объекта). Затем она отображается поверх уже отображаемого на экране содержимого, не затрагивая уже отрисованный контент.

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

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

Стоит отметить, что при использовании 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())) } } }

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)

Запись содержимого составного объекта в растровое изображение.
Один из распространенных вариантов использования — создание 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() )

Дополнительные ресурсы
Дополнительные примеры использования graphicsLayer и пользовательской отрисовки можно найти в следующих ресурсах:
Рекомендуем вам
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Графика в композиции
- Настройте изображение {:#customize-image}
- Kotlin для Jetpack Compose