בנוסף לרכיב הקומפוזבילי 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 }
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.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()) ) } } )
שינויים בגרפיקה
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 } )
תרגום
אפשר לשנות את 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() } )
סיבוב
מגדירים את 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 } )
מקור
אפשר לציין 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 } )
חיתוך ושינוי צורה
הצורה מציינת את המתאר שהתוכן נחתך לפי כשהאפשרות 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') נחתך לצורה של העיגול:
אם לאחר מכן מוסיפים 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)) ) }
אלפא
אפשר להשתמש ב-Modifier.graphicsLayer כדי להגדיר alpha (אטימות) לשכבה כולה. הצבע 1.0f אטום לחלוטין והצבע 0.0f לא נראה.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "clock", modifier = Modifier .graphicsLayer { this.alpha = 0.5f } )
אסטרטגיית קומפוזיציה
העבודה עם אלפא ושקיפות לא תמיד פשוטה כמו שינוי ערך אלפא יחיד. בנוסף לשינוי של ערך אלפא, אפשר גם להגדיר CompositingStrategy בgraphicsLayer. CompositingStrategy קובע איך התוכן של הרכיב הניתן להרכבה מורכב (משולב) עם התוכן האחר שכבר מוצג על המסך.
אלה האסטרטגיות השונות:
אוטומטי (ברירת מחדל)
אסטרטגיית הקומפוזיציה נקבעת על ידי שאר הפרמטרים של graphicsLayer. השכבה מעובדת לתוך מאגר זמני מחוץ למסך אם ערך האלפא קטן מ-1.0f או אם מוגדר RenderEffect. בכל פעם שערך האלפא קטן מ-1f, נוצרת באופן אוטומטי שכבת קומפוזיציה כדי לעבד את התוכן, ואז המאגר הזה מחוץ למסך מצויר ליעד עם ערך האלפא המתאים. הגדרת RenderEffect או גלילה מעבר לקצה תמיד מעבדת את התוכן במאגר זמני מחוץ למסך, ללא קשר לערך CompositingStrategy שהוגדר.
מחוץ למסך
התוכן של הרכיב הניתן להרכבה תמיד עובר רסטריזציה לטקסטורה או למפת סיביות מחוץ למסך לפני העיבוד ליעד. האפשרות הזו שימושית להחלת פעולות של 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 חלה רק על התוכן של הרכיב הניתן להרכבה הזה). הוא מעבד את התוכן מעל מה שכבר עבר עיבוד במסך, בלי להשפיע על התוכן שכבר צויר.
אם לא השתמשתם ב-CompositingStrategy.Offscreen, התוצאות של החלת BlendMode.Clear הן מחיקה של כל הפיקסלים ביעד, בלי קשר למה שכבר הוגדר – כך שמאגר העיבוד של החלון (שחור) נשאר גלוי. הרבה מהפקודות BlendModes שכוללות אלפא לא יפעלו כמו שצריך בלי מאגר זמני מחוץ למסך. שימו לב לטבעת השחורה סביב האינדיקטור של העיגול האדום:
כדי להבין את זה קצת יותר טוב: אם לאפליקציה יש רקע חלון שקוף, ולא השתמשתם ב-CompositingStrategy.Offscreen, הפעולה BlendMode תתבצע על כל האפליקציה. היא תנקה את כל הפיקסלים כדי להציג את האפליקציה או את הטפט שמתחתיה, כמו בדוגמה הזו:
חשוב לציין שכאשר משתמשים ב-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())) } } }
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)
כתיבת התוכן של רכיב שאפשר להרכיב ל-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() )
מקורות מידע נוספים
דוגמאות נוספות לשימוש ב-graphicsLayer ובציור בהתאמה אישית זמינות במקורות המידע הבאים:
מומלץ בשבילכם
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- גרפיקה בפיתוח נייטיב
- התאמה אישית של תמונה {:#customize-image}
- Kotlin ל-Jetpack Compose