Oprócz funkcji kompozycyjnej Canvas Compose zawiera kilka przydatnych Modifiers graficznych, które ułatwiają rysowanie treści niestandardowych. Te modyfikatory są przydatne,
ponieważ można je stosować do dowolnego elementu kompozycyjnego.
Modyfikatory rysowania
Wszystkie polecenia rysowania są wykonywane za pomocą modyfikatora rysowania w Compose. W Compose są 3 główne modyfikatory rysowania:
Podstawowy modyfikator rysowania to drawWithContent, który pozwala określić kolejność rysowania elementu kompozycyjnego i polecenia rysowania wydawane w modyfikatorze. drawBehind to wygodny wrapper wokół drawWithContent, który ma kolejność rysowania ustawioną za treścią funkcji kompozycyjnej. drawWithCache wywołuje wewnątrz siebie 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ą funkcji kompozycyjnej lub po niej. Pamiętaj, aby wywołać funkcję drawContent, aby wyrenderować rzeczywistą zawartość funkcji kompozycyjnej. Za pomocą tego modyfikatora możesz określić kolejność operacji, jeśli chcesz, aby treści były rysowane przed niestandardowymi operacjami rysowania lub po nich.
Jeśli na przykład chcesz wyrenderować gradient promieniowy na treści, aby utworzyć efekt dziurki od klucza w interfejsie, możesz wykonać 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 elementem kompozycyjnym
Modifier.drawBehind umożliwia wykonywanie operacji DrawScope za elementem kompozycyjnym wyświetlanym na ekranie. Jeśli przyjrzysz się implementacji Canvas, zauważysz, że jest to tylko wygodna nakładka na 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) )
co daje taki wynik:
Modifier.drawWithCache: rysowanie i buforowanie obiektów rysunkowych
Modifier.drawWithCache przechowuje w pamięci podręcznej obiekty, które są w nim tworzone. Obiekty są przechowywane w pamięci podręcznej, dopóki rozmiar obszaru rysowania jest taki sam lub nie zmieniły się żadne odczytywane obiekty stanu. Ten modyfikator jest przydatny do poprawy wydajności wywołań rysowania, ponieważ pozwala uniknąć konieczności ponownego przydzielania obiektów (takich jak Brush, Shader, Path), które są tworzone podczas rysowania.
Możesz też zapisać obiekty w pamięci podręcznej za pomocą remember poza modyfikatorem. Nie zawsze jest to jednak możliwe, ponieważ nie zawsze masz dostęp do kompozycji. Jeśli obiekty są używane tylko do rysowania, wydajniejsze może być użycie drawWithCache.
Jeśli na przykład utworzysz obiekt Brush, aby narysować gradient za obiektem Text, użycie drawWithCache spowoduje zapisanie obiektu Brush w pamięci podręcznej do momentu zmiany rozmiaru obszaru rysowania:
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 funkcji kompozycyjnych
Modifier.graphicsLayer to modyfikator, który sprawia, że zawartość funkcji kompozycyjnej jest rysowana w warstwie rysowania. Warstwa zapewnia kilka różnych funkcji, takich jak:
- Izolacja instrukcji rysowania (podobnie jak w przypadku
RenderNode). Instrukcje rysowania przechwycone w ramach warstwy mogą być skutecznie ponownie wydawane przez potok renderowania bez ponownego wykonywania kodu aplikacji. - Przekształcenia, które mają zastosowanie do wszystkich instrukcji rysowania zawartych w warstwie.
- Rasteryzacja na potrzeby funkcji kompozycji. Gdy warstwa jest rasteryzowana, wykonywane są jej instrukcje rysowania, a wynik jest zapisywany w buforze poza ekranem. Komponowanie takiego bufora dla kolejnych klatek jest szybsze niż wykonywanie poszczególnych instrukcji, ale po zastosowaniu przekształceń, takich jak skalowanie lub obracanie, będzie on zachowywać się jak bitmapa.
Transformacje
Modifier.graphicsLayer zapewnia izolację instrukcji rysowania. Na przykład można zastosować różne przekształcenia za pomocą Modifier.graphicsLayer. Można je animować lub modyfikować bez konieczności ponownego wykonywania funkcji lambda rysowania.
Modifier.graphicsLayer nie zmienia zmierzonego rozmiaru ani miejsca docelowego elementu kompozycyjnego, ponieważ wpływa tylko na fazę rysowania. Oznacza to, że element kompozycyjny może nakładać się na inne elementy, jeśli zostanie narysowany poza granicami układu.
Za pomocą tego modyfikatora można stosować te przekształcenia:
Skalowanie – zwiększanie rozmiaru
scaleX i scaleY powiększa lub zmniejsza zawartość odpowiednio w poziomie lub w pionie. Wartość 1.0f oznacza brak zmiany skali, a wartość 0.5f – 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
translationX i translationY można zmienić za pomocą graphicsLayer,
translationX przesuwa element kompozycyjny w lewo lub w prawo. translationY przesuwa element kompozycyjny 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 obracać w poziomie, rotationY, aby obracać w pionie, i rotationZ, aby obracać wokół osi Z (standardowy obrót). 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 } )
Pochodzenie
Możesz określić transformOrigin. Jest on następnie używany jako punkt, od którego odbywają się przekształcenia. We wszystkich dotychczasowych przykładach używaliśmy TransformOrigin.Center, który znajduje się pod adresem (0.5f, 0.5f). Jeśli określisz punkt początkowy jako (0f, 0f), przekształcenia będą się zaczynać od lewego górnego rogu elementu kompozycyjnego.
Jeśli zmienisz punkt początkowy za pomocą przekształcenia rotationZ, zobaczysz, że element obraca się wokół lewego górnego rogu funkcji kompozycyjnej:
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łtowanie
Kształt określa kontur, do którego zawartość jest przycinana, gdy clip = true. W tym przykładzie ustawiamy 2 pola, aby miały 2 różne klipy – jeden używa zmiennej klipu graphicsLayer, a drugi wygodnego wrappera 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 koła, zobaczysz, że granice elementu kompozycyjnego są takie same, ale koło jest rysowane pod dolnym kołem (i poza jego granicami).
Aby przyciąć funkcję kompozycyjną do obszaru, w którym jest rysowana, możesz dodać kolejny modyfikator Modifier.clip(RectangleShape) na początku łańcucha modyfikatorów. Treść pozostaje 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
Modifier.graphicsLayer można użyć do ustawienia alpha (nieprzezroczystości) całej warstwy. 1.0f oznacza pełną nieprzezroczystość, a 0.0f – niewidoczność.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "clock", modifier = Modifier .graphicsLayer { this.alpha = 0.5f } )
Strategia kompozycji
Praca z kanałem alfa i przezroczystością nie zawsze jest tak prosta jak zmiana pojedynczej wartości alfa. Oprócz zmiany wartości alfa można też ustawić CompositingStrategy na graphicsLayer. CompositingStrategy określa, jak zawartość elementu kompozycyjnego jest łączona z innymi treściami wyświetlanymi już na ekranie.
Dostępne strategie:
Automatycznie (domyślnie)
Strategia tworzenia kompozycji jest określana przez pozostałe parametry graphicsLayer. Renderuje warstwę w buforze poza ekranem, jeśli wartość alfa jest mniejsza niż 1,0f lub ustawiono RenderEffect. Gdy wartość alfa jest mniejsza niż 1f, automatycznie tworzona jest warstwa kompozycji, która renderuje zawartość, a następnie rysuje ten bufor poza ekranem do miejsca docelowego z odpowiednią wartością alfa. Ustawienie RenderEffect lub overscroll zawsze powoduje renderowanie treści do bufora poza ekranem, niezależnie od ustawienia CompositingStrategy.
Poza ekranem
Zawartość funkcji kompozycyjnej jest zawsze rasteryzowana do tekstury lub mapy bitowej poza ekranem przed renderowaniem do miejsca docelowego. Jest to przydatne do stosowania operacji BlendMode do maskowania treści oraz do zwiększania wydajności podczas renderowania złożonych zestawów instrukcji rysowania.
Przykładem użycia CompositingStrategy.Offscreen jest BlendModes.
W tym przykładzie załóżmy, że chcesz usunąć części funkcji kompozycyjnej Image, wydając polecenie rysowania, które używa BlendMode.Clear. Jeśli nie ustawisz właściwości compositingStrategy na CompositingStrategy.Offscreen, BlendMode będzie wchodzić w interakcje ze wszystkimi treściami w tle.
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 CompositingStrategy na Offscreen powoduje utworzenie tekstury poza ekranem, na której będą wykonywane polecenia (BlendMode jest stosowany tylko do zawartości tego elementu kompozycyjnego). Następnie renderuje go na tym, co jest już wyrenderowane na ekranie, nie wpływając na już narysowane treści.
Jeśli nie użyto CompositingStrategy.Offscreen, zastosowanie BlendMode.Clear powoduje wyczyszczenie wszystkich pikseli w miejscu docelowym, niezależnie od tego, co zostało już ustawione, pozostawiając widoczny bufor renderowania okna (czarny). Wiele BlendModes, które obejmują kanał alfa, nie będzie działać zgodnie z oczekiwaniami bez bufora poza ekranem. Zwróć uwagę na czarny pierścień wokół czerwonego kółka:
Aby lepiej to zrozumieć: jeśli aplikacja miała półprzezroczyste tło okna i nie użyto CompositingStrategy.Offscreen, BlendMode będzie oddziaływać na całą aplikację. Spowoduje to wyczyszczenie wszystkich pikseli, aby wyświetlić aplikację lub tapetę pod spodem, jak w tym przykładzie:
Warto zauważyć, że gdy używasz CompositingStrategy.Offscreen, tworzona jest tekstura poza ekranem o rozmiarze obszaru rysowania, która jest renderowana z powrotem na ekranie. Wszystkie polecenia rysowania wykonywane za pomocą tej strategii są domyślnie przycinane do tego regionu. Poniższy fragment kodu ilustruje różnice podczas przełączania się na tekstury 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 kompozycji moduluje kanał alfa dla każdej instrukcji rysowania zarejestrowanej w graphicsLayer. Nie utworzy bufora poza ekranem dla wartości alfa mniejszej niż 1,0f, chyba że ustawiono RenderEffect, więc może być bardziej wydajny w przypadku renderowania alfa. Może jednak dawać różne wyniki
w przypadku pokrywających się treści. W przypadkach, gdy wiadomo z góry, że treści nie nakładają się na siebie, może to zapewnić lepszą wydajność niż CompositingStrategy.Auto z wartościami alfa mniejszymi niż 1.
W tym przykładzie pokazujemy różne strategie kompozycji – stosowanie różnych wartości alfa do różnych części elementów kompozycyjnych oraz stosowanie 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) ) } @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)
Przesunięcia warstwy
Gdy element kompozycyjny jest rysowany w buforze poza ekranem (np. gdy alpha jest mniejsze niż 1.0f, ustawiono RenderEffect lub użyto CompositingStrategy.Offscreen), tekstura poza ekranem domyślnie przyjmuje zmierzone granice układu elementu kompozycyjnego. Wszelkie rysunki lub cienie wewnątrz warstwy, które wykraczają poza granice układu elementu kompozycyjnego (np. zewnętrzny blask narysowany za pomocą Modifier.dropShadow), są domyślnie przycinane na granicy bufora poza ekranem.
Zamiast dodawać Modifier.padding() (co powoduje przesunięcie pomiaru układu sąsiednich elementów interfejsu), przekaż LayerOutsets do parametru outsets elementu Modifier.graphicsLayer. Zwiększa to wizualne granice bufora poza ekranem z każdej strony bez zmiany zmierzonego rozmiaru układu elementu kompozycyjnego. Pamiętaj, że LayerOutsets nie rozszerza kształtu wycinka warstwy ani granic cienia, które pozostają oparte na pierwotnym zmierzonym rozmiarze elementu kompozycyjnego (dlatego zachowaj clip = false w graphicsLayer, aby uniknąć przycinania do pierwotnych granic):
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 (góra, przycięty do granic prostokątnego układu funkcji kompozycyjnej) i z LayerOutsets(24.dp) (dół, nieprzycięty)Zapisywanie zawartości funkcji kompozycyjnej w bitmapie
Częstym przypadkiem użycia jest tworzenie Bitmap z funkcji kompozycyjnej. Aby skopiować zawartość funkcji kompozycyjnej do Bitmap, utwórz GraphicsLayer za pomocą rememberGraphicsLayer().
Przekieruj polecenia rysowania do nowej warstwy za pomocą drawWithContent() i graphicsLayer.record{}. Następnie narysuj warstwę w widocznym obszarze roboczym, używając 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 ją udostępnić. Więcej informacji znajdziesz w pełnym przykładowym fragmencie kodu. Przed próbą zapisania na dysku sprawdź uprawnienia na urządzeniu.
Modyfikator rysowania niestandardowego
Aby utworzyć własny modyfikator niestandardowy, zaimplementuj interfejs DrawModifier. Dzięki temu uzyskasz dostęp do ContentDrawScope, czyli tego samego, co jest widoczne podczas korzystania z Modifier.drawWithContent(). Następnie możesz wyodrębnić typowe operacje rysowania do niestandardowych modyfikatorów rysowania, aby uporządkować kod i zapewnić wygodne wrappery. Na przykład Modifier.background() to wygodny DrawModifier.
Jeśli na przykład chcesz wdrożyć element Modifier, który odwraca treści w pionie, możesz go 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 użyj tego odwróconego modyfikatora zastosowanego do Text:
Text( "Hello Compose!", modifier = Modifier .flipped() )
Dodatkowe materiały
Więcej przykładów użycia graphicsLayer i niestandardowego rysowania znajdziesz w tych materiałach:
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Grafika w Compose
- Dostosowywanie obrazu {:#customize-image}
- Kotlin w Jetpack Compose