Помимо компонуемого элемента Canvas
, Compose предлагает несколько полезных графических Modifiers
, которые помогают отрисовывать пользовательское содержимое. Эти модификаторы полезны, поскольку их можно применять к любому компонуемому элементу.
Модификаторы рисования
Все команды рисования в Compose выполняются с помощью модификатора рисования. В Compose есть три основных модификатора рисования:
Базовым модификатором для рисования является drawWithContent
, где вы можете определить порядок отрисовки вашего компонуемого объекта и команды рисования, выдаваемые внутри модификатора. drawBehind
— это удобная оболочка для drawWithContent
, в которой порядок отрисовки установлен за содержимым компонуемого объекта. drawWithCache
вызывает внутри себя либо onDrawBehind
, либо onDrawWithContent
и предоставляет механизм кэширования созданных в них объектов.
Modifier.drawWithContent
: выбор порядка рисования
Modifier.drawWithContent
позволяет выполнять операции DrawScope
до или после содержимого компонуемого объекта. Обязательно вызовите drawContent
для последующего отображения самого содержимого компонуемого объекта. С помощью этого модификатора вы можете задать порядок операций, чтобы отрисовывать контент до или после ваших собственных операций рисования.
Например, если вы хотите отобразить радиальный градиент поверх своего контента, чтобы создать эффект замочной скважины фонарика в пользовательском интерфейсе, вы можете сделать следующее:
var pointerOffset by remember { mutableStateOf(Offset(0f, 0f)) } Column( modifier = Modifier .fillMaxSize() .pointerInput("dragging") { detectDragGestures { change, dragAmount -> pointerOffset += dragAmount } } .onSizeChanged { pointerOffset = Offset(it.width / 2f, it.height / 2f) } .drawWithContent { drawContent() // draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI. drawRect( Brush.radialGradient( listOf(Color.Transparent, Color.Black), center = pointerOffset, radius = 100.dp.toPx(), ) ) } ) { // Your composables here }
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
. Слой рендерится во внеэкранном буфере, если альфа меньше 1.0f или задано значение RenderEffect
. Если альфа меньше 1f, автоматически создаётся композитный слой для рендеринга содержимого и последующей отрисовки этого внеэкранного буфера в месте назначения с соответствующим значением альфа. Установка RenderEffect
или прокрутки всегда приводит к рендерингу содержимого во внеэкранном буфере независимо от установленного значения CompositingStrategy
.
Закадровый
Содержимое компонуемого объекта всегда растеризуется во внеэкранную текстуру или битовое изображение перед рендерингом в целевую область. Это полезно для применения операций BlendMode
для маскирования содержимого и для повышения производительности при рендеринге сложных наборов инструкций рисования.
Примером использования CompositingStrategy.Offscreen
является BlendModes
. Рассмотрим пример ниже. Допустим, вы хотите удалить части Image
, которое можно компоновать, выполнив команду рисования с использованием BlendMode.Clear
. Если вы не установите для compositingStrategy
значение CompositingStrategy.Offscreen
, BlendMode
будет взаимодействовать со всем содержимым, находящимся под ним.
Image( painter = painterResource(id = R.drawable.dog), contentDescription = "Dog", contentScale = ContentScale.Crop, modifier = Modifier .size(120.dp) .aspectRatio(1f) .background( Brush.linearGradient( listOf( Color(0xFFC5E1A5), Color(0xFF80DEEA) ) ) ) .padding(8.dp) .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } .drawWithCache { val path = Path() path.addOval( Rect( topLeft = Offset.Zero, bottomRight = Offset(size.width, size.height) ) ) onDrawWithContent { clipPath(path) { // this draws the actual image - if you don't call drawContent, it wont // render anything this@onDrawWithContent.drawContent() } val dotSize = size.width / 8f // Clip a white border for the content drawCircle( Color.Black, radius = dotSize, center = Offset( x = size.width - dotSize, y = size.height - dotSize ), blendMode = BlendMode.Clear ) // draw the red circle indication drawCircle( Color(0xFFEF5350), radius = dotSize * 0.8f, center = Offset( x = size.width - dotSize, y = size.height - dotSize ) ) } } )
Если установить CompositingStrategy
в Offscreen
, создаётся внеэкранная текстура для выполнения команд (применяя BlendMode
только к содержимому этого компонуемого объекта). Затем она визуализируется поверх того, что уже отображается на экране, не затрагивая уже отрисованное содержимое.

Если вы не использовали 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
из компонуемого объекта. Чтобы скопировать содержимое компонуемого объекта в 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 отключен.
- Графика в Compose
- Настроить изображение {:#customize-image}
- Kotlin для Jetpack Compose