Oprócz funkcji Canvas
kompozycyjnej aplikacja Compose zawiera kilka przydatnych grafikiModifiers
, które ułatwiają rysowanie niestandardowych treści. Te modyfikatory są przydatne,
bo można je stosować w dowolnym elemencie kompozycyjnym.
Modyfikatory rysowania
Wszystkie polecenia rysowania są wykonywane przy użyciu modyfikatora rysunku w narzędziu Compose. W edytorze Compose dostępne są 3 główne modyfikatory rysowania:
Podstawowym modyfikatorem rysunku jest drawWithContent
, w którym możesz określić kolejność rysowania kompozytowanego obiektu i poleceń rysowania wydanych w modyfikatorze. drawBehind
to wygodny element opakowujący drawWithContent
, który ma ustawienie kolejności rysowania za treścią kompozytową. Funkcja drawWithCache
wywołuje w nim onDrawBehind
lub onDrawWithContent
i zapewnia mechanizm buforowania utworzonych w nich obiektów.
Modifier.drawWithContent
: wybierz kolejność rysowania
Modifier.drawWithContent
umożliwia wykonywanie operacji DrawScope
przed treścią kompozytową lub po niej. Pamiętaj, aby wywołać funkcję drawContent
, aby następnie renderować rzeczywiste treści komponentu. Za pomocą tego modyfikatora możesz określić kolejność operacji, jeśli chcesz, aby treści były rysowane przed lub po operacjach niestandardowego rysowania.
Jeśli na przykład chcesz wyrenderować gradient promienisty na wierzchu treści, aby uzyskać efekt świetlika w UI, wykonaj te czynności:
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
: rysowanie za komponentem
Modifier.drawBehind
umożliwia wykonywanie operacji DrawScope
na treściach kompozytowych wyświetlanych na ekranie. Jeśli przyjrzysz się implementacji funkcji Canvas
, zauważysz, że jest to tylko wygodny element opakowujący funkcję Modifier.drawBehind
.
Aby narysować zaokrąglony prostokąt za elementem Text
:
Text( "Hello Compose!", modifier = Modifier .drawBehind { drawRoundRect( Color(0xFFBBAAEE), cornerRadius = CornerRadius(10.dp.toPx()) ) } .padding(4.dp) )
Daje to następujący wynik:
Modifier.drawWithCache
: rysowanie i zapisywanie obiektów rysowanych w pamięci podręcznej
Modifier.drawWithCache
przechowuje utworzone w niej obiekty w pamięci podręcznej. Obiekty są przechowywane w pamięci podręcznej, dopóki rozmiar obszaru rysunku jest taki sam lub żadne obiekty stanu, które są odczytywane, nie uległy zmianie. Ten modyfikator jest przydatny do zwiększania wydajności wywołań funkcji rysowania, ponieważ eliminuje konieczność ponownego przydzielania obiektów (takich jak Brush, Shader, Path
itp.), które są tworzone w ramach funkcji rysowania.
Można też buforować obiekty za pomocą remember
(bez użycia modyfikatora). Jednak nie zawsze jest to możliwe, ponieważ nie zawsze masz dostęp do kompozycji. Jeśli obiekty są używane tylko do rysowania, lepszym rozwiązaniem może być użycie drawWithCache
.
Jeśli np. utworzysz element Brush
, aby narysować gradient za elementem Text
, element drawWithCache
będzie przechowywać w pamięci obiekt Brush
, dopóki nie zmieni się rozmiar obszaru rysunku:
Text( "Hello Compose!", modifier = Modifier .drawWithCache { val brush = Brush.linearGradient( listOf( Color(0xFF9E82F0), Color(0xFF42A5F5) ) ) onDrawBehind { drawRoundRect( brush, cornerRadius = CornerRadius(10.dp.toPx()) ) } } )
Modyfikatory grafiki
Modifier.graphicsLayer
: stosowanie przekształceń do komponentów
Modifier.graphicsLayer
to modyfikator, który przekształca zawartość kompozytowanego obiektu rysowania w warstwę rysowania. Warstwy zapewniają kilka różnych funkcji, takich jak:
- Izolowanie instrukcji rysowania (podobnie jak w
RenderNode
). Instrukcje rysowania przechwycone w ramach warstwy można efektywnie ponownie wydać przez proces renderowania bez ponownego wykonywania kodu aplikacji. - Przekształcenia, które mają zastosowanie do wszystkich instrukcji rysowania zawartych w warstwie.
- Rastryzowanie w ramach możliwości kompozytowych. Gdy warstwa jest rastrowana, instrukcje rysowania są wykonywane, a wyjście jest przechwytywane w buforze poza ekranem. Kompozycja takiego bufora na kolejne klatki jest szybsza niż wykonywanie poszczególnych instrukcji, ale po zastosowaniu przekształceń, takich jak skalowanie lub obrót, będzie działać jak bitmapa.
Transformacje
Modifier.graphicsLayer
zapewnia izolację dla instrukcji rysowania; na przykład za pomocą Modifier.graphicsLayer
można stosować różne przekształcenia.
Można je animować lub modyfikować bez konieczności ponownego wykonywania funkcji lambda.
Modifier.graphicsLayer
nie zmienia zmierzonego rozmiaru ani położenia kompozytowanego obiektu, ponieważ wpływa tylko na fazę rysowania. Oznacza to, że kompozyt może nakładać się na inne, jeśli rysowanie wykracza poza jego granice.
Za pomocą tego modyfikatora można zastosować te przekształcenia:
Skala – zwiększ rozmiar
scaleX
i scaleY
odpowiednio powiększają lub zmniejszają zawartość w kierunku poziomym lub pionowym. Wartość 1.0f
oznacza brak zmiany skali, a wartość 0.5f
oznacza połowę wymiaru.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.scaleX = 1.2f this.scaleY = 0.8f } )
Tłumaczenie
Elementy translationX
i translationY
można zmienić za pomocą przycisku graphicsLayer
, a funkcja translationX
przesuwa element kompozycyjny w lewo lub w prawo. translationY
przesuwa kompozyt w górę lub w dół.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.translationX = 100.dp.toPx() this.translationY = 10.dp.toPx() } )
Obrót
Ustaw rotationX
, by obracać elementy w poziomie, rotationY
, żeby obracać je w pionie, a rotationZ
, żeby obracać je w okolicy Z (obrót standardowy). Ta wartość jest podawana w stopniach (0–360).
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
Źródło
Można określić transformOrigin
. Jest on następnie używany jako punkt, od którego odbywają się przekształcenia. Do tej pory wszystkie przykłady używały wartości TransformOrigin.Center
, która wynosi (0.5f, 0.5f)
. Jeśli określisz punkt początkowy w miejscu (0f, 0f)
, przekształcenia rozpoczną się od lewego górnego rogu komponentu.
Jeśli zmienisz punkt początkowy za pomocą transformacji rotationZ
, zobaczysz, że element obraca się wokół lewego górnego rogu kompozytu:
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 } )
Przycinanie i kształt
Shape określa kontur, do którego przycinane są treści, gdy clip = true
. W tym przykładzie ustawiliśmy 2 pudełka z 2 różnymi klipami – jeden wykorzystuje zmienną klipu graphicsLayer
, a drugi wygodny element opakowania 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)) ) }
Zawartość pierwszego pola (tekst „Hello Compose”) jest przycięta do kształtu koła:
Jeśli następnie zastosujesz translationY
do górnego różowego okręgu, zobaczysz, że granice kompozytowego obiektu pozostają takie same, ale okręg jest rysowany pod dolnym okręgiem (i poza jego granicami).
Aby przyciąć kompozyt do regionu, w którym jest on narysowany, możesz dodać kolejną Modifier.clip(RectangleShape)
na początku ciągu modyfikatorów. Treści pozostają wtedy w pierwotnych granicach.
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)) ) }
Alfa
Za pomocą atrybutu Modifier.graphicsLayer
można ustawić alpha
(przezroczystość) dla całego poziomu. Element 1.0f
jest całkowicie nieprzezroczysty, a element 0.0f
jest niewidoczny.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "clock", modifier = Modifier .graphicsLayer { this.alpha = 0.5f } )
Strategia kompozytowania
Praca z wartością alfa i przezroczystością może nie być tak prosta jak zmiana jednej wartości alfa. Oprócz zmiany alfa możesz też ustawić CompositingStrategy
w przypadku graphicsLayer
. CompositingStrategy
określa, jak treści komponenta są łączone (zestawiane) z innymi treściami wyświetlanymi na ekranie.
Dostępne strategie to:
Automatycznie (domyślnie)
Strategia kompilacji jest określana przez pozostałe parametry graphicsLayer
. Warstwę renderuje do bufora poza ekranem, jeśli alfa jest mniejsza niż 1.0f lub jeśli ustawiona jest wartość RenderEffect
. Gdy wartość alfa jest mniejsza niż 1f, automatycznie tworzona jest warstwa kompozytowa, która służy do renderowania zawartości, a potem do rysowania tego bufora poza ekranem do miejsca docelowego z odpowiednią wartością alfa. Ustawienie RenderEffect
lub nadmierne przewinięcie zawsze powoduje renderowanie treści w buforze poza ekranem niezależnie od ustawienia CompositingStrategy
.
Poza ekranem
Treści kompozytowe są zawsze rastrowane do tekstury pozaekranowej lub bitmapy przed wyrenderowaniem do miejsca docelowego. Jest to przydatne do maskowania zawartości za pomocą operacji BlendMode
oraz do zwiększenia wydajności podczas renderowania złożonych zbiorów instrukcji rysowania.
Przykład użycia CompositingStrategy.Offscreen
z BlendModes
. W przykładzie poniżej załóżmy, że chcesz usunąć części komponentu Image
, wykonując polecenie rysowania, które używa funkcji BlendMode.Clear
. Jeśli nie ustawisz wartości compositingStrategy
na CompositingStrategy.Offscreen
, element BlendMode
będzie oddziaływać na wszystkie elementy znajdujące się pod nim.
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 ) ) } } )
Ustawienie wartości CompositingStrategy
na Offscreen
powoduje utworzenie tekstury poza ekranem, aby wykonać polecenia (zastosowanie BlendMode
tylko do zawartości tej składanki). Następnie renderuje je na wierzchu tego, co już jest renderowane na ekranie, nie wpływając na już narysowane treści.
Jeśli nie użyjesz parametru CompositingStrategy.Offscreen
, zastosowanie parametru BlendMode.Clear
spowoduje wyczyszczenie wszystkich pikseli w miejscu docelowym, niezależnie od tego, co było już ustawione. W rezultacie bufor renderowania okna (czarny) pozostanie widoczny. Wiele funkcji BlendModes
, które wykorzystują alfa, nie będzie działać zgodnie z oczekiwaniami bez bufora poza ekranem. Zwróć uwagę na czarny pierścień wokół wskaźnika czerwonego okręgu:
Aby lepiej to zrozumieć: jeśli aplikacja ma przezroczyste tło okna, a nie używasz CompositingStrategy.Offscreen
, BlendMode
będzie oddziaływać na całą aplikację. Wyczyści ona wszystkie piksele, aby pokazać aplikację lub tapetę znajdującą się pod spodem, jak w tym przykładzie:
Warto zauważyć, że przy korzystaniu z funkcji CompositingStrategy.Offscreen
tekstura pozaekranowa o rozmiarze obszaru rysowania jest tworzona i renderowana z powrotem na ekranie. Wszystkie polecenia rysowania wykonywane za pomocą tej strategii są domyślnie ograniczone do tego obszaru. Poniższy fragment kodu pokazuje różnice, jakie występują po przejściu na korzystanie z tekstur poza ekranem:
@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
Ta strategia tworzenia kompozycji zmienia przezroczystość każdego z instrukcji rysowania zapisanych w graphicsLayer
. Nie będzie tworzyć bufora offscreenowego dla wartości alfa poniżej 1,0f, chyba że ustawisz wartość RenderEffect
, co może zwiększyć wydajność podczas renderowania alfa. Może jednak podawać różne wyniki w przypadku treści nakładających się na siebie. W przypadkach, gdy wiadomo z wyprzedzeniem, że treści się nie nakładają, może to zapewnić większą skuteczność niż CompositingStrategy.Auto
z wartością alfa mniejszą niż 1.
Poniżej znajdziesz kolejny przykład różnych strategii kompozycyjnych – zastosowanie różnych alfa do różnych części komponentów i zastosowanie strategii 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)
Zapisz zawartość elementu kompozycyjnego w bitmapie
Typowym przypadkiem użycia jest tworzenie Bitmap
z komponowalnych. Aby skopiować zawartość komponentu do Bitmap
, utwórz GraphicsLayer
za pomocą rememberGraphicsLayer()
.
Przekieruj polecenia rysowania do nowej warstwy za pomocą drawWithContent()
i graphicsLayer.record{}
. Następnie na widocznym płótnie narysuj warstwę za pomocą narzędzia 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) }
Możesz zapisać bitmapę na dysku i udostępnić ją. Więcej informacji znajdziesz w pełnym przykładowym fragmencie kodu. Zanim spróbujesz zapisać plik na dysku, sprawdź uprawnienia na urządzeniu.
Modyfikator rysunku niestandardowego
Aby utworzyć własny modyfikator niestandardowy, zaimplementuj interfejs DrawModifier
. Daje Ci to dostęp do ContentDrawScope
, który jest taki sam jak w przypadku Modifier.drawWithContent()
. Następnie możesz wyodrębnić typowe operacje rysowania do niestandardowych modyfikatorów rysowania, aby uporządkować kod i zapewnić wygodne opakowania. Na przykład Modifier.background()
to wygodne rozwiązanie dla DrawModifier
.
Jeśli na przykład chcesz zastosować Modifier
, które odwraca zawartość w poziomie, możesz je utworzyć w ten sposób:
class FlippedModifier : DrawModifier { override fun ContentDrawScope.draw() { scale(1f, -1f) { this@draw.drawContent() } } } fun Modifier.flipped() = this.then(FlippedModifier())
Następnie zastosuj ten odwrócony modyfikator do Text
:
Text( "Hello Compose!", modifier = Modifier .flipped() )
Dodatkowe materiały
Więcej przykładów użycia funkcji graphicsLayer
i rysowania niestandardowego znajdziesz w tych materiałach:
Polecane dla Ciebie
- Uwaga: tekst linku wyświetla się, gdy JavaScript jest wyłączony
- Grafika w funkcji tworzenia wiadomości
- Dostosowywanie obrazu {:#customize-image}
- Kotlin w Jetpack Compose