Oprócz komponentu Canvas
aplikacja Compose zawiera kilka przydatnych komponentów graficznych
Modifiers
, które ułatwiają tworzenie niestandardowych treści. Te modyfikatory są przydatne, ponieważ można je zastosować do dowolnego komponentu.
Modyfikatory rysowania
Wszystkie polecenia rysowania są wykonywane za pomocą modyfikatora rysowania w sekcji Edytowanie. 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ą. drawWithCache
wywołuje w sobie funkcję onDrawBehind
lub onDrawWithContent
i zapewnia mechanizm do buforowania obiektów utworzonych w tych funkcjach.
Modifier.drawWithContent
: wybierz kolejność rysowania
Modifier.drawWithContent
umożliwia wykonywanie operacji DrawScope
przed treścią komponentu lub po niej. Pamiętaj, aby wywołać funkcję drawContent
, aby następnie renderować rzeczywiste treści komponentu. Dzięki temu modyfikatorowi 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 zawartoś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 przechowywanie w pamięci podręcznej obiektów rysowania
Modifier.drawWithCache
przechowuje w pamięci podręcznej obiekty utworzone w ramach tego zasobu. 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 draw.
Możesz też zapisać obiekty w pamięci podręcznej za pomocą funkcji remember
, bez użycia modyfikatora. Nie zawsze jest to jednak 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 podręcznej 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ść kompozytu w warstwę rysowania. Warstwy zapewniają kilka różnych funkcji, takich jak:
- Izolowanie instrukcji rysowania (podobnie jak w
RenderNode
). Instrukcje rysowania zarejestrowane w ramach warstwy mogą być wydajnie ponownie wydawane 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 dla kolejnych klatek 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żesz 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ększanie rozmiaru
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
Wartości translationX
i translationY
można zmieniać za pomocą graphicsLayer
,
translationX
przesuwa kompozycję 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
, aby obrócić poziomo, rotationY
, aby obrócić pionowo,
rotationZ
, aby obrócić wzdłuż osi 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żesz 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 będą się zaczynać w lewym górnym 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. 1.0f
jest całkowicie nieprzezroczysty, a 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 już 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 wartości RenderEffect
lub przewinięcie ponad ekran zawsze powoduje renderowanie treści w buforze poza ekranem niezależnie od ustawionej wartości 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 elementu CompositingStrategy.Offscreen
to element 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 (z zastosowaniem BlendMode
tylko do zawartości tej kompozytowanej treści). 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 czarne pierścień wokół czerwonego koła:
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 pamiętać, że podczas używania CompositingStrategy.Offscreen
tworzona jest tekstura poza ekranem o rozmiarze obszaru rysunku, która jest 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 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ć lepszą skuteczność niż CompositingStrategy.Auto
z wartościami alfa mniejszymi niż 1.
Poniżej znajdziesz kolejny przykład różnych strategii kompozytowych: stosowanie różnych alfanumerycznych wartości do różnych części komponentów i 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)
Zapisywanie zawartości kompozytu na bitmapę
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 opakowanie 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 jest wyświetlany, gdy obsługa JavaScript jest wyłączona
- Grafika w sekcji Nowy post
- Dostosowywanie obrazu {:#customize-image}
- Kotlin w Jetpack Compose