בנוסף לרכיב ה-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 }
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.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
הוא מודификатор שממיר את התוכן של רכיב התצוגה המצוירת ליצירה לשכבת ציור. שכבה מספקת כמה פונקציות שונות, כמו:
- בידוד של הוראות הציור (בדומה ל-
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 } )
תרגום
אפשר לשנות את 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)
, הטרנספורמציות מתחילות מהפינה הימנית העליונה של הרכיב הניתן לקיבוץ.
אם משנים את המקור באמצעות טרנספורמציה 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 } )
קליפ וצורה
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') ייחתך לפי צורת העיגול:
אם מחילים לאחר מכן את הפונקציה translationY
על המעגל הוורוד העליון, רואים שהגבולות של ה-Composable עדיין זהים, אבל המעגל מצויר מתחת למעגל התחתון (ומחוץ לגבולות שלו).
כדי לחתוך את ה-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)) ) }
אלפא
אפשר להשתמש ב-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
קובע איך התוכן של ה-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 הזה). לאחר מכן, המערכת תיצור את התצוגה של המודעות מעל התצוגה שכבר קיימת במסך, בלי להשפיע על התוכן שכבר צויר.
אם לא משתמשים ב-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
אסטרטגיית ההרכבה הזו משתנה בהתאם ל-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)
כתיבת התוכן של רכיב מורכב ב-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() )
מקורות מידע נוספים
דוגמאות נוספות לשימוש ב-graphicsLayer
ובציור בהתאמה אישית זמינות במקורות המידע הבאים:
מומלץ עבורך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- גרפיקה ב-Compose
- התאמה אישית של תמונה {:#customize-image}
- Kotlin ל-Jetpack פיתוח נייטיב