Modyfikatory grafiki

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
}

Rysunek 1.: metoda Modifier.drawWithContent użyta na Composable w celu utworzenia interfejsu typu latarka.

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:

Tekst i tło narysowane za pomocą Modifier.drawBehind
Rysunek 2. Tekst i tło narysowane za pomocą metody Modifier.drawBehind

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())
                )
            }
        }
)

Buforowanie obiektu Brush za pomocą metody drawWithCache
Rysunek 3. Zapisywanie obiektu Brush w pamięci podręcznej za pomocą metody drawWithCache

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

scaleXscaleY 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
        }
)

Ilustracja 4. Skala pozioma i pozioma zastosowane do komponentu Image
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()
        }
)

Rysunek 5.: translationX i translationY zastosowane do obrazu za pomocą Modifier.graphicsLayer
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
        }
)

Rysunek 6: parametry rotationX, rotationY i rotationZ ustawione w elementach Image przez Modifier.graphicsLayer
Ź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
        }
)

Rysunek 7.: obrót zastosowany przy ustawieniu wartości TransformOrigin równej 0f, 0f

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:

Klip został zastosowany do możliwości kompozycyjnej w Box
Rysunek 8. Zastosowany klip do elementu kompozytowanego Box

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).

Zastosowano klip z przełożeniem w osi Y i czerwonym obrysem
Rysunek 9. Zastosowany klip z przełożeniem w osi Y i czerwoną obwódką

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))
    )
}

Przycinanie zastosowane na wierzchu przekształcenia warstwy graficznej
Rysunek 10. Przypięty klip nad przekształceniem warstwy graficznej

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
        }
)

Obraz z zastosowaniem przezroczystości
Rys. 11. Obraz z zastosowaniem przezroczystości

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.

Modifier.drawWithContent na obrazie z wskazaniem koła z użyciem BlendMode.Clear w aplikacji
Rysunek 12.: metoda Modifier.drawWithContent zastosowana do obrazu z wskazaniem koła, z parametrami BlendMode.Clear i CompositingStrategy.Offscreen w aplikacji

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:

Modifier.drawWithContent na obiekcie Image, który pokazuje oznaczenie koła, z użyciem BlendMode.Clear i bez ustawionej strategii kompozytowania
Rysunek 13. Metoda Modifier.drawWithContent zastosowana do obrazu z określonym okręgiem, z użyciem metody BlendMode.Clear i bez ustawionej metody CompositingStrategy

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:

Brak ustawionej strategii kompozytowania i użycie BlendMode.Clear w przypadku aplikacji z przezroczystym tłem okna. Różowa tapeta jest widoczna w obszarze wokół czerwonego koła stanu.
Rysunek 14. Brak ustawionej strategii kompozytowania i użycie BlendMode.Clear w przypadku aplikacji z półprzezroczystym tłem okna. Zwróć uwagę, że różowa tapeta jest widoczna w obszarze wokół czerwonego koła stanu.

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()))
        }
    }
}

CompositingStrategy.Auto vs CompositingStrategy.Offscreen – offscreen clips to the region, where auto doesn’t
Rysunek 15. CompositingStrategy.Auto vs. CompositingStrategy.Offscreen – klipy poza ekranem w regionie, w którym automatyczne
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)

ModulateAlpha stosuje zestaw alfa do każdego polecenia rysowania.
Rysunek 16. ModulateAlpha stosuje ustawienie alfa do każdego indywidualnego polecenia rysowania

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()
)

Niestandardowy modyfikator odwrócenia tekstu
Rysunek 17. Zmodyfikowany tekst z niestandardowym przekształceniem

Dodatkowe materiały

Więcej przykładów użycia funkcji graphicsLayer i rysowania niestandardowego znajdziesz w tych materiałach: