מגבילי גרפיקה

בנוסף לרכיב הקומפוזבילי Canvas, ל-Compose יש כמה רכיבים קומפוזביליים שימושיים לגרפיקה Modifiers שעוזרים לצייר תוכן בהתאמה אישית. המאפיינים האלה שימושיים כי אפשר להחיל אותם על כל רכיב קומפוזבילי.

שינויים בציור

כל פקודות הציור מתבצעות באמצעות משנה ציור ב-Compose. יש שלושה משנים עיקריים של ציור ב-Compose:

המשנה הבסיסי לשרטוט הוא drawWithContent, שבו אפשר להגדיר את סדר השרטוט של רכיב ה-Composable ואת פקודות השרטוט שמוגדרות בתוך המשנה. ‫drawBehind הוא wrapper נוח ל-drawWithContent, שסדר הציור שלו מוגדר מאחורי התוכן של הרכיב הקומפוזבילי. ‫drawWithCache קורא ל-onDrawBehind או ל-onDrawWithContent בתוכו – ומספק מנגנון לאחסון במטמון של האובייקטים שנוצרו בהם.

Modifier.drawWithContent: בחירת סדר הציור

Modifier.drawWithContent מאפשרת לבצע פעולות DrawScope לפני או אחרי התוכן של הרכיב. חשוב להפעיל את הפונקציה drawContent כדי לעבד את התוכן בפועל של הרכיב הקומפוזבילי. באמצעות משנה זה, אתם יכולים להחליט על סדר הפעולות, אם אתם רוצים שהתוכן יוצג לפני או אחרי פעולות הציור המותאמות אישית.

לדוגמה, אם רוצים להציג מעבר צבע רדיאלי מעל התוכן כדי ליצור אפקט של חור מפתח של פנס בממשק המשתמש, אפשר לעשות את הפעולות הבאות:

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
}

איור 1: Modifier.drawWithContent בשימוש מעל Composable כדי ליצור חוויית משתמש מסוג פנס.

Modifier.drawBehind: ציור מאחורי רכיב שאפשר להרכיב

Modifier.drawBehind מאפשרת לבצע פעולות DrawScope מאחורי התוכן שניתן להרכבה שמוצג במסך. אם תבדקו את ההטמעה של Canvas, תראו שזו רק עטיפה נוחה של Modifier.drawBehind.

כדי לצייר מלבן מעוגל מאחורי Text:

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

התוצאה היא:

טקסט ורקע שנוצרו באמצעות Modifier.drawBehind
איור 2: טקסט ורקע שנוצרו באמצעות Modifier.drawBehind

Modifier.drawWithCache: ציור ושמירה במטמון של אובייקטים לציור

Modifier.drawWithCache שומר במטמון את האובייקטים שנוצרים בתוכו. האובייקטים נשמרים במטמון כל עוד הגודל של אזור הציור זהה, או כל עוד לא חל שינוי באובייקטים של המצב שנקראים. המשנה הזה שימושי לשיפור הביצועים של קריאות לציור, כי הוא מונע את הצורך להקצות מחדש אובייקטים (כמו: Brush, Shader, Path וכו') שנוצרים בציור.

אפשר גם לשמור אובייקטים במטמון באמצעות remember, מחוץ לשינוי. עם זאת, זה לא תמיד אפשרי כי לא תמיד יש לכם גישה לקומפוזיציה. יכול להיות שימוש ב-drawWithCache יהיה יעיל יותר אם האובייקטים משמשים רק לציור.

לדוגמה, אם יוצרים Brush כדי לצייר מעבר צבעים מאחורי Text, השימוש ב-drawWithCache שומר במטמון את האובייקט Brush עד שגודל אזור הציור משתנה:

Text(
    "Hello Compose!",
    modifier = Modifier
        .drawWithCache {
            val brush = Brush.linearGradient(
                listOf(
                    Color(0xFF9E82F0),
                    Color(0xFF42A5F5)
                )
            )
            onDrawBehind {
                drawRoundRect(
                    brush,
                    cornerRadius = CornerRadius(10.dp.toPx())
                )
            }
        }
)

שמירת אובייקט המברשת במטמון באמצעות drawWithCache
איור 3: שמירת האובייקט Brush במטמון באמצעות drawWithCache

שינויים בגרפיקה

Modifier.graphicsLayer: החלת טרנספורמציות על רכיבים קומפוזביליים

Modifier.graphicsLayer הוא משנה שגורם לתוכן של רכיב ה-Composable להיכנס לשכבת ציור. שכבה מספקת כמה פונקציות שונות, כמו:

  • בידוד של הוראות הציור (בדומה ל-RenderNode). אפשר להנפיק מחדש ביעילות הוראות ציור שנשמרו כחלק משכבה על ידי צינור העיבוד, בלי להפעיל מחדש את קוד האפליקציה.
  • טרנספורמציות שחלות על כל הוראות הציור שנכללות בשכבה.
  • רסטריזציה ליכולות של יצירות מוזיקליות. כשמבצעים רסטריזציה של שכבה, פקודות הציור שלה מופעלות והפלט נשמר במאגר זמני מחוץ למסך. קומפוזיציה של מאגר כזה עבור פריימים עוקבים מהירה יותר מביצוע ההוראות האישיות, אבל היא תתנהג כמפת סיביות כשמחילים עליה טרנספורמציות כמו שינוי גודל או סיבוב.

טרנספורמציות

Modifier.graphicsLayer מספק בידוד להוראות הציור שלו. לדוגמה, אפשר להחיל טרנספורמציות שונות באמצעות Modifier.graphicsLayer. אפשר להנפיש או לשנות אותם בלי להריץ מחדש את פונקציית ה-lambda של הציור.

Modifier.graphicsLayer לא משנה את הגודל או המיקום של הרכיב הניתן להרכבה, כי הוא משפיע רק על שלב הציור. כלומר, יכול להיות שהרכיב הניתן להרכבה יחפוף לרכיבים אחרים אם הוא מצויר מחוץ לגבולות הפריסה שלו.

אפשר להשתמש במילת המפתח הזו כדי להחיל את השינויים הבאים:

שינוי גודל – הגדלה

scaleX ו-scaleY מגדילים או מקטינים את התוכן בכיוון האופקי או האנכי, בהתאמה. הערך 1.0f מציין שלא חל שינוי בסולם, והערך 0.5f מציין חצי מהמאפיין.

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.scaleX = 1.2f
            this.scaleY = 0.8f
        }
)

איור 4: scaleX ו-scaleY שהוחלו על רכיב Image composable
תרגום

אפשר לשנות את translationX ו-translationY באמצעות graphicsLayer. הפונקציה translationX מזיזה את הרכיב שמאלה או ימינה, והפונקציה translationY מזיזה את הרכיב למעלה או למטה.

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.translationX = 100.dp.toPx()
            this.translationY = 10.dp.toPx()
        }
)

איור 5: המאפיינים translationX ו-translationY מוחלים על Image באמצעות Modifier.graphicsLayer
סיבוב

מגדירים את rotationX לסיבוב אופקי, את rotationY לסיבוב אנכי ואת rotationZ לסיבוב על ציר Z (סיבוב רגיל). הערך הזה מצוין במעלות (0-360).

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.rotationX = 90f
            this.rotationY = 275f
            this.rotationZ = 180f
        }
)

איור 6: rotationX, ‏ rotationY ו-rotationZ מוגדרים בתמונה באמצעות Modifier.graphicsLayer
מקור

אפשר לציין transformOrigin. הוא משמש כנקודה שממנה מתבצעים השינויים. בכל הדוגמאות עד עכשיו השתמשנו ב-TransformOrigin.Center, שנמצא ב-(0.5f, 0.5f). אם מציינים את המקור ב-(0f, 0f), הטרנספורמציות מתחילות מהפינה השמאלית העליונה של ה-composable.

אם משנים את המקור באמצעות טרנספורמציה של rotationZ, אפשר לראות שהפריט מסתובב סביב הפינה השמאלית העליונה של הרכיב:

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

איור 7: סיבוב שהוחל עם TransformOrigin שהוגדר ל-0f, 0f

חיתוך ושינוי צורה

הצורה מציינת את המתאר שהתוכן נחתך לפי כשהאפשרות clip = true מופעלת. בדוגמה הזו, הגדרנו שתי תיבות עם שני קליפים שונים – אחת באמצעות משתנה הקליפ graphicsLayer והשנייה באמצעות רכיב העטיפה הנוח 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))
    )
}

התוכן של התיבה הראשונה (הטקסט 'Hello Compose') נחתך לצורה של העיגול:

ה-Clip הוחל על קומפוזבל Box
איור 8: קליפ שמוחל על Box composable

אם לאחר מכן מוסיפים translationY לעיגול הוורוד העליון, רואים שגבולות ה-Composable נשארים זהים, אבל העיגול מצויר מתחת לעיגול התחתון (ומחוץ לגבולות שלו).

הקליפ הוחל עם תרגום Y ומסגרת אדומה לסימון
איור 9: קליפ עם תרגום Y ומסגרת אדומה לתיאור המתאר

כדי לחתוך את רכיב ה-Composable לאזור שבו הוא מצויר, אפשר להוסיף עוד Modifier.clip(RectangleShape) בתחילת שרשרת ה-Modifier. התוכן יישאר בתוך הגבולות המקוריים.

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

חיתוך שמוחל על טרנספורמציה של graphicsLayer
איור 10: קליפ שמוחל על טרנספורמציה של graphicsLayer

אלפא

אפשר להשתמש ב-Modifier.graphicsLayer כדי להגדיר alpha (אטימות) לשכבה כולה. הצבע 1.0f אטום לחלוטין והצבע 0.0f בלתי נראה.

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

תמונה עם אלפא
איור 11: תמונה עם אלפא

אסטרטגיית קומפוזיציה

העבודה עם אלפא ושקיפות לא תמיד פשוטה כמו שינוי של ערך אלפא יחיד. בנוסף לשינוי אלפא, יש גם אפשרות להגדיר CompositingStrategy ב-graphicsLayer. CompositingStrategy קובע איך התוכן של ה-composable משולב (מורכב) עם התוכן האחר שכבר מצויר על המסך.

השיטות השונות הן:

אוטומטי (ברירת מחדל)

אסטרטגיית השילוב נקבעת על ידי שאר הפרמטרים של graphicsLayer. השכבה עוברת עיבוד לתוך מאגר זמני מחוץ למסך אם ערך האלפא קטן מ-1.0f או אם מוגדר RenderEffect. בכל פעם שערך האלפא קטן מ-1f, נוצרת שכבת שילוב באופן אוטומטי כדי לעבד את התוכן, ואז המאגר הזמני הזה מצויר ביעד עם ערך האלפא המתאים. הגדרה של RenderEffect או של גלילה מעבר לקצה תמיד מעבדת את התוכן לתוך מאגר זמני מחוץ למסך, ללא קשר לערך CompositingStrategy שהוגדר.

מחוץ למסך

התוכן של רכיב ה-Composable עובר רסטריזציה תמיד לטקסטורה או למפת סיביות מחוץ למסך לפני העיבוד ליעד. זה שימושי להחלת פעולות BlendMode כדי להסתיר תוכן, ולשיפור הביצועים כשמעבדים קבוצות מורכבות של הוראות ציור.

דוגמה לשימוש ב-CompositingStrategy.Offscreen היא עם BlendModes. בדוגמה הבאה, נניח שרוצים להסיר חלקים מ-Image composable על ידי הפעלת פקודת ציור שמשתמשת ב-BlendMode.Clear. אם לא מגדירים את compositingStrategy ל-CompositingStrategy.Offscreen, ‏ BlendMode יקיים אינטראקציה עם כל התוכן ברקע.

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

הגדרת CompositingStrategy ל-Offscreen יוצרת טקסטורה מחוץ למסך כדי להריץ את הפקודות (החלת BlendMode רק על התוכן של הפונקציה הקומפוזבילית הזו). לאחר מכן המערכת מרנדרת אותו על גבי מה שכבר עובד במסך, בלי להשפיע על התוכן שכבר צויר.

‫Modifier.drawWithContent בתמונה שמוצג בה סימן עיגול, עם BlendMode.Clear בתוך האפליקציה
איור 12: Modifier.drawWithContent בתמונה שמוצג בה עיגול, עם BlendMode.Clear ו-CompositingStrategy.Offscreen בתוך האפליקציה

אם לא השתמשתם ב-CompositingStrategy.Offscreen, התוצאות של החלת BlendMode.Clear ינקו את כל הפיקסלים ביעד, בלי קשר למה שכבר הוגדר – כך שמאגר העיבוד של החלון (שחור) יהיה גלוי. הרבה מה-BlendModes שכוללים אלפא לא יפעלו כמצופה בלי מאגר מחוץ למסך. שימו לב לטבעת השחורה סביב אינדיקטור העיגול האדום:

‫Modifier.drawWithContent בתמונה שמציגה אינדיקציה של עיגול, עם BlendMode.Clear וללא CompositingStrategy
איור 13: Modifier.drawWithContent בתמונה שמוצג בה עיגול, עם BlendMode.Clear וללא CompositingStrategy

כדי להבין את זה קצת יותר טוב: אם לאפליקציה יש רקע חלון שקוף, ולא השתמשתם ב-CompositingStrategy.Offscreen, הפעולה BlendMode תתבצע על כל האפליקציה. היא תנקה את כל הפיקסלים כדי להציג את האפליקציה או את הטפט שמתחתיה, כמו בדוגמה הזו:

לא מוגדר CompositingStrategy ונעשה שימוש ב-BlendMode.Clear באפליקציה עם רקע חלון שקוף. הטפט הוורוד מוצג באזור שסביב עיגול הסטטוס האדום.
איור 14: לא הוגדרה CompositingStrategy ונעשה שימוש ב-BlendMode.Clear באפליקציה עם רקע חלון שקוף למחצה. שימו לב איך הטפט הוורוד מופיע באזור שמסביב לעיגול הסטטוס האדום.

חשוב לציין שכאשר משתמשים ב-CompositingStrategy.Offscreen, נוצרת טקסטורה מחוץ למסך בגודל של אזור השרטוט, והיא מעובדת בחזרה על המסך. כל פקודות הציור שמתבצעות באמצעות האסטרטגיה הזו, נחתכות כברירת מחדל לאזור הזה. קטע הקוד הבא ממחיש את ההבדלים כשעוברים לשימוש בטקסטורות מחוץ למסך:

@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 לעומת CompositingStrategy.Offscreen – offscreen מבצע חיתוך לאזור, כש-auto לא מבצע חיתוך
איור 15: CompositingStrategy.Auto לעומת CompositingStrategy.Offscreen – קליפים מחוץ למסך לאזור, שבו auto לא פועל
ModulateAlpha

אסטרטגיית הקומפוזיציה הזו משנה את ערך האלפא של כל אחת מהוראות הציור שמתועדות ב-graphicsLayer. היא לא יוצרת מאגר מחוץ למסך עבור ערך אלפא שקטן מ-1.0f, אלא אם מוגדר RenderEffect, ולכן היא יכולה להיות יעילה יותר עבור עיבוד אלפא. עם זאת, היא יכולה לספק תוצאות שונות עבור תוכן חופף. במקרים שבהם ידוע מראש שהתוכן לא חופף, היא יכולה לספק ביצועים טובים יותר מאשר CompositingStrategy.Auto עם ערכי אלפא שקטנים מ-1.

בדוגמה הבאה מוצגות אסטרטגיות שונות של קומפוזיציה – החלת ערכי אלפא שונים על חלקים שונים של רכיבי ה-Composable, והחלת אסטרטגיית 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 מחילה את האלפא שמוגדר על כל פקודת ציור בנפרד
איור 16: הפונקציה ModulateAlpha מחילה את האלפא שהוגדר על כל פקודת ציור בנפרד

כתיבת התוכן של רכיב שאפשר להרכיב ל-bitmap

תרחיש נפוץ לשימוש הוא יצירת Bitmap מקומפוזבילי. כדי להעתיק את התוכן של הקומפוזבילי ל-Bitmap, יוצרים GraphicsLayer באמצעות rememberGraphicsLayer().

מפנים את פקודות הציור לשכבה החדשה באמצעות drawWithContent() ו-graphicsLayer.record{}. לאחר מכן מציירים את השכבה בקנבס הגלוי באמצעות 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)
}

אפשר לשמור את מפת הביטים בדיסק ולשתף אותה. לפרטים נוספים, אפשר לעיין בדוגמה מלאה של קטע קוד. חשוב לבדוק את ההרשאות במכשיר לפני שמנסים לשמור בכונן.

משנה ציור בהתאמה אישית

כדי ליצור משנה משלכם, צריך להטמיע את הממשק DrawModifier. כך מקבלים גישה ל-ContentDrawScope, שזהה למה שמוצג כשמשתמשים ב-Modifier.drawWithContent(). אחר כך אפשר לחלץ פעולות ציור נפוצות למשני ציור מותאמים אישית כדי לנקות את הקוד ולספק עטיפות נוחות. לדוגמה, Modifier.background() היא עטיפה נוחה של DrawModifier.

לדוגמה, אם רוצים להטמיע Modifier שמבצע היפוך אנכי של התוכן, אפשר ליצור אותו כך:

class FlippedModifier : DrawModifier {
    override fun ContentDrawScope.draw() {
        scale(1f, -1f) {
            this@draw.drawContent()
        }
    }
}

fun Modifier.flipped() = this.then(FlippedModifier())

אחר כך משתמשים במקש הצירוף ההפוך שמוחל על Text:

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

שינוי כיוון הטקסט באמצעות משנה מותאם אישית
איור 17: משנה מותאם אישית הפוך בטקסט

מקורות מידע נוספים

דוגמאות נוספות לשימוש ב-graphicsLayer ובציור בהתאמה אישית זמינות במקורות המידע הבאים: