Modyfikatory grafiki

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
}

Rysunek 1. Modifier.drawWithContent użyty nad elementem kompozycyjnym w celu utworzenia interfejsu przypominającego latarkę.

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:

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

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

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

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

Rysunek 4. Właściwości scaleX i scaleY zastosowane do funkcji kompozycyjnej Image
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()
        }
)

Rysunek 5. Właściwości translationX i translationY zastosowane do obrazu za pomocą Modifier.graphicsLayer
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
        }
)

Rysunek 6. Właściwości rotationX, rotationY i rotationZ ustawione w obrazie za pomocą Modifier.graphicsLayer
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
        }
)

Rysunek 7. Obrót zastosowany z właściwością TransformOrigin ustawioną na 0f, 0f

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:

Klip zastosowany do funkcji kompozycyjnej Box
Rysunek 8. Przycięcie zastosowane do funkcji kompozycyjnej Box

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

Klip z zastosowanym parametrem translationY i czerwoną ramką
Rysunek 9. Klip zastosowany z translationY i czerwoną ramką obrysu

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

Klip zastosowany nad przekształceniem graphicsLayer
Rysunek 10. Klip zastosowany na przekształceniu graphicsLayer

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

Obraz z zastosowanym kanałem alfa
Rysunek 11. Obraz z zastosowaną przezroczystością

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.

Modifier.drawWithContent na obrazie przedstawiającym okrąg z BlendMode.Clear w aplikacji
Rysunek 12. Modifier.drawWithContent na obrazie pokazującym okrągły wskaźnik z BlendMode.Clear i CompositingStrategy.Offscreen w aplikacji

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:

Modifier.drawWithContent na obrazie przedstawiającym okrąg, z BlendMode.Clear i bez ustawionej CompositingStrategy
Rysunek 13. Modifier.drawWithContent na obrazie pokazującym okrąg, z BlendMode.Clear i bez ustawionej CompositingStrategy

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:

Brak ustawienia CompositingStrategy i używanie BlendMode.Clear w aplikacji z półprzezroczystym tłem okna. Różowa tapeta jest widoczna w obszarze wokół czerwonego kółka stanu.
Rysunek 14. Brak ustawienia CompositingStrategy i użycie BlendMode.Clear w aplikacji z półprzezroczystym tłem okna. Zwróć uwagę, że różowa tapeta jest widoczna w obszarze wokół czerwonego kółka stanu.

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

CompositingStrategy.Auto i CompositingStrategy.Offscreen – tryb Offscreen przycina do regionu, a tryb Auto nie
Ilustracja 15. CompositingStrategy.Auto i CompositingStrategy.Offscreen – offscreen przycina do regionu, a auto nie
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)

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

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

Porównanie zewnętrznego blasku warstwy poza ekranem bez LayerOutsets (góra, przycięta do prostokątnych granic układu) i z LayerOutsets(24.dp) (dół, nieprzycięta)
Rysunek 17. Zewnętrzny blask warstwy poza ekranem bez 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()
)

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

Dodatkowe materiały

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