В дополнение к возможности компоновки 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 }
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](https://developer.android.com/static/develop/ui/compose/images/graphics/modifiers/modifier_draw_behind.png?authuser=8&hl=ru)
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](https://developer.android.com/static/develop/ui/compose/images/graphics/modifiers/modifier_draw_with_cache.png?authuser=8&hl=ru)
Модификаторы графики
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 } )
Зажим и форма
Форма определяет контур, по которому обрезается содержимое, когда 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)) ) }
Содержимое первого поля (текст «Привет, напишите») обрезается по форме круга:
![Клип применен к составному блоку](https://developer.android.com/static/develop/ui/compose/images/graphics/modifiers/clip_applied.png?authuser=8&hl=ru)
Если вы затем примените translationY
к верхнему розовому кругу, вы увидите, что границы Composable остались прежними, но круг рисуется под нижним кругом (и за его пределами).
![Клип применен с переводом Y и красной рамкой для контура](https://developer.android.com/static/develop/ui/compose/images/graphics/modifiers/clip_applied_red_border.png?authuser=8&hl=ru)
Чтобы закрепить составной элемент в области, в которой он нарисован, вы можете добавить еще один 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](https://developer.android.com/static/develop/ui/compose/images/graphics/modifiers/clip_applied_everything.png?authuser=8&hl=ru)
Альфа
Modifier.graphicsLayer
можно использовать для установки alpha
(непрозрачности) для всего слоя. 1.0f
полностью непрозрачен, а 0.0f
невидим.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "clock", modifier = Modifier .graphicsLayer { this.alpha = 0.5f } )
![Изображение с примененной альфа-каналом](https://developer.android.com/static/develop/ui/compose/images/graphics/modifiers/image_alpha.png?authuser=8&hl=ru)
Стратегия композиции
Работа с альфа-каналом и прозрачностью может быть не такой простой, как изменение одного значения альфа-канала. Помимо изменения альфа-канала, существует также возможность установить CompositingStrategy
для graphicsLayer
. CompositingStrategy
определяет, как содержимое компонуемого объекта компонуется (собирается) с другим содержимым, уже нарисованным на экране.
Различные стратегии:
Авто (по умолчанию)
Стратегия композиции определяется остальными параметрами graphicsLayer
. Он отображает слой во внеэкранном буфере, если альфа меньше 1,0f или установлен RenderEffect
. Всякий раз, когда альфа меньше 1f, автоматически создается слой композитинга для рендеринга содержимого, а затем рисования этого закадрового буфера в место назначения с соответствующей альфа-каналом. Установка RenderEffect
или overscroll всегда отображает содержимое в закадровый буфер независимо от установленного CompositingStrategy
.
За кадром
Содержимое составного объекта всегда растеризуется в закадровую текстуру или растровое изображение перед рендерингом в место назначения. Это полезно для применения операций BlendMode
для маскировки содержимого, а также для повышения производительности при рендеринге сложных наборов инструкций рисования.
Пример использования CompositingStrategy.Offscreen
— BlendModes
. Взгляните на пример ниже. Допустим, вы хотите удалить части компонуемого Image
, введя команду рисования, использующую BlendMode.Clear
. Если вы не установите для compositingStrategy
значение CompositingStrategy.Offscreen
, BlendMode
будет взаимодействовать со всем содержимым, находящимся под ним.
Image(painter = painterResource(id = R.drawable.dog), contentDescription = "Dog", contentScale = ContentScale.Crop, modifier = Modifier .size(120.dp) .aspectRatio(1f) .background( Brush.linearGradient( listOf( Color(0xFFC5E1A5), Color(0xFF80DEEA) ) ) ) .padding(8.dp) .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } .drawWithCache { val path = Path() path.addOval( Rect( topLeft = Offset.Zero, bottomRight = Offset(size.width, size.height) ) ) onDrawWithContent { clipPath(path) { // this draws the actual image - if you don't call drawContent, it wont // render anything this@onDrawWithContent.drawContent() } val dotSize = size.width / 8f // Clip a white border for the content drawCircle( Color.Black, radius = dotSize, center = Offset( x = size.width - dotSize, y = size.height - dotSize ), blendMode = BlendMode.Clear ) // draw the red circle indication drawCircle( Color(0xFFEF5350), radius = dotSize * 0.8f, center = Offset( x = size.width - dotSize, y = size.height - dotSize ) ) } } )
Установив для CompositingStrategy
значение Offscreen
, он создаст закадровую текстуру для выполнения команд (применяя BlendMode
только к содержимому этого компонуемого объекта). Затем он отображает его поверх того, что уже отображается на экране, не затрагивая уже нарисованный контент.
![Modifier.drawWithContent на изображении, показывающем круг, с внутренним приложением BlendMode.Clear](https://developer.android.com/static/develop/ui/compose/images/graphics/modifiers/complex_modifier_example_drawWithCache_background.png?authuser=8&hl=ru)
Если вы не использовали CompositingStrategy.Offscreen
, в результате применения BlendMode.Clear
все пиксели в месте назначения очищаются независимо от того, что уже было установлено, — оставляя буфер рендеринга окна (черный) видимым. Многие BlendModes
, включающие альфа-канал, не будут работать должным образом без закадрового буфера. Обратите внимание на черное кольцо вокруг красного круга-индикатора:
![Modifier.drawWithContent на изображении, показывающем круг, с BlendMode.Clear и без установленного CompositingStrategy.](https://developer.android.com/static/develop/ui/compose/images/graphics/modifiers/blendmode_without_compositing_strategy.png?authuser=8&hl=ru)
Чтобы понять это немного глубже: если у приложения был полупрозрачный фон окна, и вы не использовали CompositingStrategy.Offscreen
, BlendMode
будет взаимодействовать со всем приложением. Все пиксели будут очищены, чтобы показать приложение или обои под ними, как в этом примере:
![CompositingStrategy не установлен и используется BlendMode.Clear с приложением, имеющим полупрозрачный фон окна. Розовые обои отображаются в области вокруг красного круга состояния.](https://developer.android.com/static/develop/ui/compose/images/graphics/modifiers/compositing_strategy_punch_through_to_wallpaper.png?authuser=8&hl=ru)
Стоит отметить, что при использовании 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 нет](https://developer.android.com/static/develop/ui/compose/images/graphics/modifiers/graphics_compositing_strategy.png?authuser=8&hl=ru)
ModulateAlpha
Эта стратегия композиции модулирует альфу для каждой инструкции рисования, записанной в graphicsLayer
. Он не будет создавать закадровый буфер для альфа ниже 1.0f, если не установлен RenderEffect
, поэтому он может быть более эффективным для альфа-рендеринга. Однако он может давать разные результаты для перекрывающегося контента. В случаях использования, когда заранее известно, что содержимое не перекрывается, это может обеспечить более высокую производительность, чем CompositingStrategy.Auto
со значениями альфа меньше 1.
Ниже приведен еще один пример различных стратегий композиции — применение разных альф к разным частям составных элементов и применение стратегии Modulate
:
@Preview @Composable fun CompositingStratgey_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 применяет набор альфа к каждой отдельной команде рисования.](https://developer.android.com/static/develop/ui/compose/images/graphics/modifiers/modulate_alpha.png?authuser=8&hl=ru)
Записать содержимое составного объекта в растровое изображение
Распространенным вариантом использования является создание 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() )
![Пользовательский перевернутый модификатор для текста](https://developer.android.com/static/develop/ui/compose/images/graphics/modifiers/modifier_flipped.png?authuser=8&hl=ru)
Дополнительные ресурсы
Дополнительные примеры использования graphicsLayer
и пользовательского рисунка можно найти на следующих ресурсах:
Рекомендуется для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Графика в Compose
- Настройка изображения {:#customize-image}
- Котлин для Jetpack Compose