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

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

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

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

המשנה הבסיסי לשרטוט הוא drawWithContent, שבו אפשר להגדיר את סדר השרטוט של רכיב ה-Composable ואת פקודות השרטוט שמוגדרות בתוך המשנה. ‫drawBehind הוא wrapper נוח ל-drawWithContent, שסדר הציור שלו מוגדר מאחורי התוכן של ה-composable. ‫drawWithCache calls either onDrawBehind or onDrawWithContent inside of it - and provides a mechanism for caching the objects created in them.

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, תראו שזה רק wrapper נוח לשימוש סביב 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 הוא משנה שגורם לתוכן של הפונקציה הניתנת להרכבה להיכלל בשכבת ציור. שכבה מספקת כמה פונקציות שונות, כמו:

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

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

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

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

אפשר להחיל את השינויים הבאים באמצעות התוסף הזה:

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

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') נחתך לצורת העיגול:

הקליפ הוחל על רכיב Box
איור 8: קליפ שמוחל על רכיב Box

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

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

כדי לחתוך את הקומפוזיציה לאזור שבו היא מצוירת, אפשר להוסיף עוד 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 שהוגדר.

מחוץ למסך

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

דוגמה לשימוש ב-CompositingStrategy.Offscreen היא עם BlendModes. בדוגמה שלמטה, נניח שרוצים להסיר חלקים מ-Image שאפשר להרכיב באמצעות פקודת ציור שמשתמשת ב-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 ב-Image שמציג אינדיקציה של עיגול, עם 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 יכול לספק ביצועים טובים יותר מאשר שימוש ב-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 מ-composable. כדי להעתיק את התוכן של ה-composable ל-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 ובציור בהתאמה אישית זמינות במקורות המידע הבאים: