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

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

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

עיבוד גרפיקה

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

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

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

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

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

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 הוחלו על תמונה קומפוזבילית
תרגום

אפשר לשנות את 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 הוחלו על תמונה באמצעות 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, אפשר לראות שהפריט מסתובב סביב הפינה השמאלית העליונה של ה-composable:

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, והשנייה באמצעות ה-wrapper הנוח 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 על העיגול הוורוד העליון, תראו שהגבולות של הפונקציה הקומפוזבילית עדיין זהים, אבל העיגול מצויר מתחת לעיגול התחתון (ומחוץ לגבולות שלו).

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

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

אלה השיטות השונות:

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

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

@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 מחילה את ערך האלפא שמוגדר לכל פקודת ציור בנפרד
איור 16: ModulateAlpha מחיל את ערך האלפא שמוגדר לכל פקודת ציור בנפרד

הבלטות של שכבות

כשפונקציה קומפוזבילית משורטטת במאגר נתונים זמני (buffer) מחוץ למסך (לדוגמה, כש-alpha קטן מ-1.0f, מוגדר RenderEffect או כשמשתמשים ב-CompositingStrategy.Offscreen), הטקסטורה מחוץ למסך מוגדרת כברירת מחדל לגבולות הפריסה שנמדדו של הפונקציה הקומפוזבילית. כל ציור או צללית בתוך השכבה שחורגים מגבולות הפריסה של ה-composable (כמו זוהר חיצוני שצויר באמצעות Modifier.dropShadow) נחתכים באופן משתמע בגבול של מאגר הנתונים הזמני מחוץ למסך.

במקום להוסיף Modifier.padding() (שמשנה את מדידת הפריסה של רכיבי ממשק משתמש סמוכים), מעבירים את LayerOutsets לפרמטר outsets של Modifier.graphicsLayer. הפעולה הזו מרחיבה את הגבולות החזותיים של המאגר מחוץ למסך בכל צד, בלי לשנות את גודל הפריסה שנמדד של הרכיב הניתן להגדרה. חשוב לדעת שהפונקציה LayerOutsets לא מרחיבה את צורת החיתוך או את גבולות הצל של השכבה, שנשארים מבוססים על הגודל המקורי של ה-composable (לכן צריך להשאיר את clip = false ב-graphicsLayer כדי למנוע חיתוך לגבולות המקוריים):

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

השוואה בין הילה חיצונית של שכבה מחוץ למסך בלי LayerOutsets (למעלה, חתוכה לגבולות הפריסה המלבנית) לבין LayerOutsets(24.dp) (למטה, לא חתוכה)
איור 17: הילה חיצונית של שכבה מחוץ למסך בלי LayerOutsets (למעלה, חתוכה בגבולות הפריסה המלבנית של ה-composable) לעומת עם LayerOutsets(24.dp) (למטה, לא חתוכה)

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