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

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

מודפי ציור

כל פקודות הציור מתבצעות באמצעות שינוי של ציור ב-Compose. יש שלושה מודификаторים עיקריים לציור ב-Compose:

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

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

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

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

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: שמירת אובייקט המברשת במטמון באמצעות drawWithCache

גורמי שינוי גרפיים

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

Modifier.graphicsLayer הוא מודификатор שממיר את התוכן של רכיב התצוגה המצוירת ליצירה לשכבת ציור. שכבה מספקת כמה פונקציות שונות, כמו:

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

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

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

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

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

Scale – הגדלת הגודל

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
תרגום

אפשר לשנות את 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 ב-Image באמצעות Modifier.graphicsLayer
מקור

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

אם משנים את המקור באמצעות טרנספורמציה 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

קליפ וצורה

Shape מציין את קווי המתאר של התוכן שנחתכים כשהערך של clip = true הוא 1. בדוגמה הזו, מגדירים שני תיבות עם שני קליפים שונים – אחד באמצעות משתנה הקליפ 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 composable
איור 8: קליפ שהוחל על רכיב Box composable

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

קליפ שהוחל עם translationY, ומסגרת אדומה לקו המתאר
איור 9: קליפ שהוחל עם translationY, וגבול אדום לקו המתאר

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

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

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 – קליפים מחוץ למסך לאזור, בעוד שהאפשרות 'אוטומטי' לא עושה זאת
איור 15: CompositingStrategy.Auto לעומת CompositingStrategy.Offscreen – קליפים מחוץ למסך מועברים לאזור, בעוד שהאפשרות 'אוטומטי' לא עושה זאת
ModulateAlpha

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

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

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

פונקציית שינוי מותאם אישית לציור

כדי ליצור משתנה מותאם אישית משלכם, מטמיעים את הממשק 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 ובציור בהתאמה אישית זמינות במקורות המידע הבאים: