Modyfikatory grafiki

Oprócz Canvaskomponentów kompozycyjnych Compose zawiera kilka przydatnych elementów graficznychModifiers, które pomagają w rysowaniu treści niestandardowych. Te modyfikatory są przydatne, ponieważ można je zastosować do dowolnego komponentu.

Modyfikatory rysowania

W Compose wszystkie polecenia rysowania są wykonywane za pomocą modyfikatora rysowania. W Compose są 3 główne modyfikatory rysowania:

Podstawowym modyfikatorem rysowania jest drawWithContent, w którym możesz określić kolejność rysowania elementu kompozycyjnego i polecenia rysowania wydawane wewnątrz modyfikatora. drawBehind to wygodna otoczka drawWithContent, w której kolejność rysowania jest ustawiona za treścią elementu kompozycyjnego. drawWithCache wywołuje wewnątrz siebie onDrawBehind lub onDrawWithContent i zapewnia mechanizm buforowania obiektów w nich utworzonych.

Modifier.drawWithContent: wybierz kolejność rysowania

Modifier.drawWithContent umożliwia wykonywanie operacji DrawScope przed lub po treści elementu kompozycyjnego. Pamiętaj, aby wywołać funkcję drawContent, aby wyrenderować rzeczywistą treść funkcji kompozycyjnej. Za pomocą tego modyfikatora możesz określić kolejność operacji, czyli czy chcesz, aby Twoje treści były rysowane przed czy po niestandardowych operacjach rysowania.

Jeśli na przykład chcesz renderować gradient promieniowy na treści, aby utworzyć efekt dziurki od klucza latarki 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
}

Rysunek 1. Modyfikator drawWithContent użyty na komponencie, aby utworzyć interfejs użytkownika w stylu latarki.

Modifier.drawBehind: rysowanie za elementem kompozycyjnym

Modifier.drawBehind umożliwia wykonywanie operacjiDrawScope za treściami kompozycyjnymi, które są rysowane na ekranie. Jeśli przyjrzysz się implementacji funkcji Canvas, zauważysz, że jest to tylko wygodna otoczka funkcji Modifier.drawBehind.

Aby narysować zaokrąglony prostokąt za Text:

Text(
    "Hello Compose!",
    modifier = Modifier
        .drawBehind {
            drawRoundRect(
                Color(0xFFBBAAEE),
                cornerRadius = CornerRadius(10.dp.toPx())
            )
        }
        .padding(4.dp)
)

co daje ten wynik:

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

Modifier.drawWithCache: Rysowanie i buforowanie obiektów rysowania

Modifier.drawWithCache przechowuje w pamięci podręcznej obiekty utworzone w jego wnętrzu. Obiekty są przechowywane w pamięci podręcznej tak długo, jak długo rozmiar obszaru rysowania pozostaje taki sam lub nie zmieniły się żadne obiekty stanu, które są odczytywane. Ten modyfikator jest przydatny do zwiększania wydajności wywołań rysowania, ponieważ pozwala uniknąć konieczności ponownego przydzielania obiektów (takich jak Brush, Shader, Path itp.), które są tworzone podczas rysowania.

Możesz też buforować obiekty 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 Brush, aby narysować gradient za Text, używając drawWithCache, obiekt Brush zostanie zapisany w pamięci podręcznej do czasu 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())
                )
            }
        }
)

Buforowanie obiektu Brush za pomocą metody drawWithCache
Ilustracja 3. Buforowanie obiektu Brush za pomocą metody drawWithCache

Modyfikatory grafiki

Modifier.graphicsLayer: stosowanie przekształceń do funkcji kompozycyjnych

Modifier.graphicsLayer to modyfikator, który sprawia, że treść elementu kompozycyjnego 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ć ponownie wydawane przez proces renderowania bez ponownego wykonywania kodu aplikacji.
  • Przekształcenia, które są stosowane do wszystkich instrukcji rysowania zawartych w warstwie.
  • Rasteryzacja na potrzeby kompozycji. Gdy warstwa jest rasteryzowana, wykonywane są jej instrukcje rysowania, a wynik jest zapisywany w buforze poza ekranem. Komponowanie takiego bufora w przypadku kolejnych klatek jest szybsze niż wykonywanie poszczególnych instrukcji, ale w przypadku przekształceń, takich jak skalowanie czy obracanie, będzie się zachowywać jak bitmapa.

Transformacje

Modifier.graphicsLayer zapewnia izolację instrukcji rysowania; na przykład różne przekształcenia można zastosować za pomocą elementu Modifier.graphicsLayer. Można je animować lub modyfikować bez konieczności ponownego wykonywania funkcji lambda rysowania.

Modifier.graphicsLayer nie zmienia zmierzonego rozmiaru ani umiejscowienia komponentu, ponieważ wpływa tylko na fazę rysowania. Oznacza to, że element kompozycyjny może nakładać się na inne, jeśli zostanie narysowany poza granicami układu.

Za pomocą tego modyfikatora można zastosować te przekształcenia:

Skala – zwiększanie rozmiaru

scaleXscaleY powiększają lub zmniejszają odpowiednio 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
        }
)

Rysunek 4. Funkcje scaleX i scaleY zastosowane do komponentu kompozycyjnego Image
Tłumaczenie

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

Rysunek 5. Parametry translationX i translationY zastosowane do obrazu z modyfikatorem Modifier.graphicsLayer
Obrót

Ustaw rotationX, aby obracać w poziomie, rotationY, aby obracać w pionie, i rotationZ, aby obracać wzdłuż 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
        }
)

Rysunek 6.: wartości rotationX, rotationY i rotationZ ustawione na obrazie przez Modifier.graphicsLayer
Pochodzenie

Możesz określić transformOrigin. Jest on następnie używany jako punkt, od którego odbywają się przekształcenia. W dotychczasowych przykładach używaliśmy znaku TransformOrigin.Center, który znajduje się w pozycji (0.5f, 0.5f). Jeśli określisz punkt początkowy na (0f, 0f), przekształcenia będą się rozpoczynać od lewego górnego rogu elementu kompozycyjnego.

Jeśli zmienisz punkt początkowy za pomocą rotationZtransformacji, zobaczysz, że element obraca się wokół lewego górnego rogu elementu kompozycyjnego:

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

Ilustracja 7. Obrót zastosowany przy wartości TransformOrigin ustawionej na 0f, 0f

Przytnij i zmień kształt

Kształt określa kontur, do którego przycinana jest zawartość, gdy clip = true. W tym przykładzie ustawiamy 2 pola z 2 różnymi klipami – jedno z użyciem zmiennej klipu graphicsLayer, a drugie z użyciem wygodnego 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:

Zastosowano klip do komponentu Box
Ilustracja 8. Klip zastosowany do komponentu Box

Jeśli następnie zastosujesz translationY do górnego różowego kółka, zobaczysz, że granice elementu kompozycyjnego pozostają takie same, ale kółko jest rysowane pod dolnym kółkiem (i poza jego granicami).

Klip z zastosowanym przesunięciem w osi Y i czerwonym obramowaniem
Rysunek 9. Klip z zastosowanym tłumaczeniemY i czerwoną obwódką

Aby przyciąć komponent do regionu, w którym jest rysowany, możesz dodać kolejny modyfikator Modifier.clip(RectangleShape) na początku łańcucha modyfikatorów. Treść pozostanie 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 przekształceniu graphicsLayer
Ilustracja 10. Klip zastosowany na przekształceniu graphicsLayer

Alfa

Modifier.graphicsLayer można użyć do ustawienia alpha (nieprzezroczystości) dla całej warstwy. 1.0f jest całkowicie nieprzezroczysty, a 0.0f – niewidoczny.

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "clock",
    modifier = Modifier
        .graphicsLayer {
            this.alpha = 0.5f
        }
)

Obraz z zastosowanym kanałem alfa
Ilustracja 11. Obraz z zastosowanym kanałem alfa

Strategia kompozycji

Praca z wartościami alfa i przezroczystości nie zawsze jest tak prosta jak zmiana pojedynczej wartości alfa. Oprócz zmiany wartości alfa możesz też ustawić CompositingStrategy na graphicsLayer. CompositingStrategy określa sposób komponowania (łączenia) treści funkcji kompozycyjnej z innymi treściami, które są już narysowane na ekranie.

Dostępne strategie to:

Automatycznie (domyślnie)

Strategia kompozycji jest określana przez pozostałe graphicsLayerparametry. Renderuje warstwę do bufora poza ekranem, jeśli wartość alfa jest mniejsza niż 1,0f lub ustawiono RenderEffect. Jeśli wartość alfa jest mniejsza niż 1f, automatycznie tworzona jest warstwa kompozycji, która renderuje zawartość, a następnie rysuje ten bufor poza ekranem w miejscu docelowym z odpowiednią wartością alfa. Ustawienie RenderEffect lub przewijania zawsze renderuje zawartość do bufora poza ekranem niezależnie od ustawionego parametru CompositingStrategy.

Poza ekranem

Zawartość elementu kompozycyjnego jest zawsze rasteryzowana do tekstury lub mapy bitowej poza ekranem przed renderowaniem w miejscu docelowym. Jest to przydatne do stosowania operacji BlendMode w celu 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. Załóżmy, że w poniższym przykładzie chcesz usunąć części Image kompozycji, wydając polecenie rysowania, które używa BlendMode.Clear. Jeśli nie ustawisz wartości compositingStrategy na CompositingStrategy.Offscreen, element BlendMode będzie wchodzić w interakcję z całą zawartością 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 wykonywane są polecenia (zastosowanie BlendMode tylko do zawartości tego komponentu). Następnie jest ona renderowana na tym, co jest już renderowane na ekranie, bez wpływu na już narysowaną zawartość.

Funkcja Modifier.drawWithContent na obrazie przedstawiającym okrąg, w którego środku znajduje się BlendMode.Clear w aplikacji
Ilustracja 12. Funkcja Modifier.drawWithContent na obrazie przedstawiającym okrąg, w którym w aplikacji zastosowano BlendMode.Clear i CompositingStrategy.Offscreen

Jeśli nie użyjesz CompositingStrategy.Offscreen, zastosowanie BlendMode.Clear wyczyści wszystkie piksele w miejscu docelowym niezależnie od tego, co było już ustawione, pozostawiając widoczny bufor renderowania okna (czarny). Wiele BlendModes, które wykorzystują kanał alfa, nie będzie działać zgodnie z oczekiwaniami bez bufora poza ekranem. Zwróć uwagę na czarny pierścień wokół czerwonego wskaźnika:

Modifier.drawWithContent na obrazie przedstawiającym okrąg, z ustawionym BlendMode.Clear i bez CompositingStrategy
Ilustracja 13. Funkcja Modifier.drawWithContent na obrazie przedstawiającym okrąg z ustawieniami BlendMode.Clear i bez ustawionej funkcji CompositingStrategy

Aby to lepiej zrozumieć: jeśli aplikacja miała półprzezroczyste tło okna i nie używasz CompositingStrategy.Offscreen, BlendMode będzie wchodzić w interakcję z całą aplikacją. Wyczyści wszystkie piksele, aby wyświetlić aplikację lub tapetę pod nią, jak w tym przykładzie:

Brak ustawionej wartości CompositingStrategy i używanie BlendMode.Clear w aplikacji z przezroczystym tłem okna. Różowa tapeta jest widoczna w obszarze wokół czerwonego kółka stanu.
Ilustracja 14. Brak ustawionej wartości CompositingStrategy i użycie BlendMode.Clear w aplikacji z półprzezroczystym tłem okna. Zwróć uwagę, jak różowa tapeta jest widoczna w obszarze wokół czerwonego okręgu stanu.

Warto zauważyć, że podczas korzystania z CompositingStrategy.Offscreen tworzona jest tekstura poza ekranem o rozmiarze obszaru rysowania, która jest renderowana z powrotem na ekranie. Wszystkie polecenia rysowania wykonane za pomocą tej strategii będą domyślnie przycinane do tego regionu. Poniższy fragment kodu pokazuje różnice po przejściu 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()))
        }
    }
}

CompositingStrategy.Auto a CompositingStrategy.Offscreen – klipy poza ekranem w regionie, w którym automatyczne nie działają
Rysunek 15. CompositingStrategy.Auto a CompositingStrategy.Offscreen – klipy poza ekranem w regionie, w którym nie działa automatyczne komponowanie
ModulateAlpha

Ta strategia kompozycji moduluje wartość alfa dla każdej instrukcji rysowania zarejestrowanej w graphicsLayer. Nie utworzy bufora poza ekranem dla wartości alfa mniejszej niż 1,0, chyba że ustawiono RenderEffect, więc może być bardziej wydajny w przypadku renderowania alfa. W przypadku nakładających się treści może jednak zwracać różne wyniki. W przypadkach, w których z góry wiadomo, ż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.

Poniższy przykład pokazuje różne strategie kompozycji – stosowanie różnych wartości alfa do różnych części komponentów i 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)
    )
}

val Purple = Color(0xFF7E57C2)
val Yellow = Color(0xFFFFCA28)
val Red = Color(0xFFEF5350)

ModulateAlpha stosuje wartość alfa ustawioną w każdym poleceniu rysowania.
Rysunek 16. Funkcja ModulateAlpha stosuje wartość alfa ustawioną dla każdego polecenia rysowania

Zapisywanie zawartości komponentu do mapy bitowej

Częstym przypadkiem użycia jest tworzenie Bitmap z kompozycji. Aby skopiować zawartość kompozycji do Bitmap, utwórz GraphicsLayer za pomocą funkcji rememberGraphicsLayer().

Przekieruj polecenia rysowania do nowej warstwy za pomocą drawWithContent()graphicsLayer.record{}. Następnie narysuj warstwę na widocznym obszarze roboczym za pomocą 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ć mapę bitową na dysku i ją udostępnić. Więcej informacji znajdziesz w pełnym przykładzie kodu. Zanim spróbujesz zapisać plik na dysku, sprawdź, czy masz odpowiednie 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 udostępniane podczas korzystania z Modifier.drawWithContent(). Możesz następnie wyodrębnić typowe operacje rysowania do niestandardowych modyfikatorów rysowania, aby uporządkować kod i zapewnić wygodne otoczki. Na przykład Modifier.background() to wygodna DrawModifier.

Jeśli na przykład chcesz wdrożyć Modifier, który odwraca zawartość w pionie, możesz utworzyć go 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 odwróconego modyfikatora zastosowanego do klawisza Text:

Text(
    "Hello Compose!",
    modifier = Modifier
        .flipped()
)

Niestandardowy modyfikator odwrócony w tekście
Rysunek 17. Niestandardowy modyfikator odwrócony w tekście

Dodatkowe materiały

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