Modyfikatory grafiki

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 komponentu oraz polecenia rysowania wydawane w ramach modyfikatora. 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 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 w buforze obiektów rysunku

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 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ść kompozytu w warstwę rysowania. Warstwy zapewniają kilka różnych funkcji, takich jak:

  • Izolowanie instrukcji rysowania (podobnie jak w RenderNode). Instrukcje rysowania przechwycone 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 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ż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

scaleXscaleY odpowiednio powiększają lub pomniejszają 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

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

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

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

Zastosowany klip w komponentach 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
Ilustracja 10. Klip zastosowany na wierzchu transformacji warstwy graficznej

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

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 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, która służy do wykonywania poleceń (aplikowanie 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 użyciem metody 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 czarne pierścień wokół czerwonego koła:

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 oznaczeniem koła, z użyciem metody BlendMode.Clear i bez ustawionej strategii kompozytowania

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 ono 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 kółka 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 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 między zwykłymi i pozaekranowymi teksturami:

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

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

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 rozwiązanie dla DrawModifier.

Jeśli na przykład chcesz zastosować Modifier, które odwraca zawartość w pionie, 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óconego tekstu
Rysunek 17. Zmodyfikowany tekst w trybie odbicia lustrzanego

Dodatkowe materiały

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