Помимо composable-функции Canvas, в Compose есть несколько полезных графических Modifiers, которые помогают рисовать пользовательский контент. Эти модификаторы полезны,
потому что их можно применить к любой composable-функции.
Модификаторы рисования
Все команды рисования выполняются с помощью модификатора рисования в Compose. В Compose есть три основных модификатора рисования:
Базовый модификатор для рисования – drawWithContent. С его помощью можно задать порядок рисования composable-функции и команды рисования, которые выполняются внутри модификатора. drawBehind – это удобная оболочка для drawWithContent, в которой порядок отрисовки задан так, чтобы она находилась за контентом composable-функции. drawWithCache
вызывает внутри себя onDrawBehind или onDrawWithContent и предоставляет
механизм для кэширования созданных в них объектов.
Modifier.drawWithContent: выберите порядок рисования
Modifier.drawWithContent позволяет выполнять операции DrawScope до или после контента composable-функции. Обязательно вызовите drawContent, чтобы отрисовать фактический контент composable-функции. С помощью этого модификатора вы можете определить порядок операций, если хотите, чтобы ваш контент был нарисован до или после ваших пользовательских операций рисования.
Например, если вы хотите отобразить радиальный градиент поверх контента, чтобы создать эффект замочной скважины фонарика в пользовательском интерфейсе, вы можете сделать следующее:
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: рисование за composable-функцией
Modifier.drawBehind позволяет выполнять операции DrawScope за контентом composable-функции, который отрисовывается на экране. Если вы посмотрите на реализацию 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: применение преобразований к composable-функциям
Modifier.graphicsLayer
– модификатор, который заставляет контент composable-функции отрисовываться в слое отрисовки. Слой предоставляет несколько различных функций, таких как:
- Изоляция инструкций по отрисовке (аналогично
RenderNode). Инструкции по отрисовке, зафиксированные как часть слоя, могут быть эффективно повторно выданы конвейером рендеринга без повторного выполнения кода приложения. - Преобразования, которые применяются ко всем инструкциям по рисованию, содержащимся в слое.
- Растеризация для возможностей композиции. При растеризации слоя выполняются его инструкции по отрисовке, а выходные данные записываются в закадровый буфер. Компоновка такого буфера для последующих кадров выполняется быстрее, чем выполнение отдельных инструкций, но при применении преобразований, таких как масштабирование или поворот, он будет вести себя как битовая карта.
Преобразования
Modifier.graphicsLayer изолирует инструкции по рисованию. Например, с помощью Modifier.graphicsLayer можно применять различные преобразования. Их можно анимировать или изменять без необходимости повторного выполнения лямбда-выражения рисования.
Modifier.graphicsLayer не меняет размер или расположение composable-функции, так как влияет только на этап отрисовки. Это означает, что ваша composable-функция может перекрывать другие, если она в конечном итоге рисует за пределами своих границ макета.
С помощью этого модификатора можно применять следующие преобразования:
Масштаб – увеличить размер
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 перемещает composable-функцию влево или вправо. translationY перемещает composable-функцию вверх или вниз.
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), преобразования будут начинаться с левого верхнего угла composable-функции.
Если изменить начало координат с помощью преобразования rotationZ, то можно увидеть, что элемент вращается вокруг верхнего левого угла composable-функции:
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 имеет значение 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)) ) }
Содержимое первого поля (текст "Hello Compose") обрезается по форме круга:
Если затем применить translationY к верхнему розовому кругу, вы увидите, что границы Composable-функции остались прежними, но круг рисуется под нижним кругом (и за его пределами).
Чтобы обрезать 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 определяет, как контент composable-функции компонуется с другим контентом, уже отображаемым на экране.
Доступны следующие стратегии:
Автоматически (по умолчанию)
Стратегия композитинга определяется остальными параметрами graphicsLayer. Если значение альфа-канала меньше 1.0f или задан параметр RenderEffect, слой отрисовывается в закадровом буфере. Если значение альфа-канала меньше 1f, автоматически создается слой композиции для рендеринга контента, а затем этот закадровый буфер отрисовывается в месте назначения с соответствующим альфа-каналом. Если задано свойство RenderEffect или эффект достижения конца прокрутки, контент всегда отрисовывается в буфере за пределами экрана независимо от значения CompositingStrategy.
Отключенный экран
Содержимое composable-функции всегда растеризуется в текстуру или битовую карту за пределами экрана перед отрисовкой в месте назначения. Это полезно для применения операций BlendMode к маскируемому контенту и для повышения производительности при рендеринге сложных наборов инструкций по рисованию.
Пример использования CompositingStrategy.Offscreen с BlendModes.
В примере ниже показано, как удалить части composable-функции 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 будет применяться только к содержимому этой composable-функции). Затем он отрисовывает его поверх того, что уже отрисовано на экране, не затрагивая уже нарисованный контент.
Если не использовать 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.
В следующем примере показаны разные стратегии композиции: применение разных значений альфа-канала к разным частям composable-функций и применение стратегии 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) ) } @Preview @Composable fun ModifierGraphicsLayerOutsets() { Column( modifier = Modifier .background(Color(0xFF0A0E14)) .padding(48.dp), verticalArrangement = Arrangement.spacedBy(48.dp), horizontalAlignment = Alignment.CenterHorizontally ) { val pillShape = RoundedCornerShape(percent = 50) // Without outsets: alpha = 0.5f creates an offscreen buffer that clips the 24.dp glow Box( modifier = Modifier .size( width = 140.dp, height = 56.dp ) .graphicsLayer( alpha = 0.5f, clip = false ) .dropShadow( shape = pillShape, shadow = Shadow(radius = 24.dp, color = Color.Cyan) ) .background(color = Color(0xFF141824), shape = pillShape), contentAlignment = Alignment.Center ) { Text( text = "✨ Glow", color = Color.White, fontSize = 18.sp ) } Box( modifier = Modifier .size( width = 140.dp, height = 56.dp ) .graphicsLayer( alpha = 0.5f, outsets = LayerOutsets(24.dp), clip = false ) .dropShadow( shape = pillShape, shadow = Shadow(radius = 24.dp, color = Color.Cyan) ) .background(color = Color(0xFF141824), shape = pillShape), contentAlignment = Alignment.Center ) { Text( text = "✨ Glow", color = Color.White, fontSize = 18.sp ) } } } val Purple = Color(0xFF7E57C2) val Yellow = Color(0xFFFFCA28) val Red = Color(0xFFEF5350)
Отступы слоя
Когда composable-функция отрисовывается в закадровом буфере (например, если alpha меньше 1.0f, задано свойство RenderEffect или используется CompositingStrategy.Offscreen), закадровая текстура по умолчанию соответствует границам макета composable-функции. Любой рисунок или тень внутри слоя, выходящие за границы макета composable-функции (например, внешнее свечение, нарисованное с помощью Modifier.dropShadow), неявно обрезаются по границе закадрового буфера.
Вместо того чтобы добавлять Modifier.padding() (что сдвигает размеры макета соседних элементов интерфейса), передайте LayerOutsets в параметр outsets элемента Modifier.graphicsLayer. Это расширяет визуальные границы закадрового буфера с каждой стороны, не изменяя измеренный размер макета составного объекта. Обратите внимание, что LayerOutsets не расширяет форму обрезки слоя или границы тени, которые по-прежнему основаны на исходном измеренном размере composable-функции (поэтому оставьте clip = false в graphicsLayer, чтобы избежать обрезки до исходных границ):
Box( modifier = Modifier .size( width = 140.dp, height = 56.dp ) .graphicsLayer( alpha = 0.5f, outsets = LayerOutsets(24.dp), clip = false ) .dropShadow( shape = pillShape, shadow = Shadow(radius = 24.dp, color = Color.Cyan) ) .background(color = Color(0xFF141824), shape = pillShape), contentAlignment = Alignment.Center ) { Text( text = "✨ Glow", color = Color.White, fontSize = 18.sp ) }
LayerOutsets (сверху, обрезано по границам прямоугольного макета composable-функции) и с LayerOutsets(24.dp) (снизу, не обрезано)Запись содержимого composable-функции в битовую карту
Распространенный вариант использования – создание 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 отключен.
- Графика в Compose
- Как настроить изображение {:#customize-image}
- Kotlin для Jetpack Compose