בנוסף לרכיב ה-Canvas
שאפשר ליצור, ב-Compose יש כמה רכיבים גרפיים שימושיים Modifiers
שיעזרו לכם לצייר תוכן מותאם אישית. המשתנים האלה שימושיים כי אפשר להחיל אותם על כל רכיב.
מודפי ציור
כל פקודות הציור מתבצעות באמצעות שינוי של כלי ציור ב-Compose. בכלי 'כתיבה' יש שלושה מגבילי שרטוט עיקריים:
מגביל הבסיס לשרטוט הוא drawWithContent
, שם אפשר לקבוע את סדר השרטוט של פריט הקומפוזבילי ואת פקודות השרטוט שהונפקו בתוך מגביל השרטוט. drawBehind
הוא רכיב wrapper נוח סביב drawWithContent
שסדר השרטוט מוגדר מאחורי התוכן של התוכן הקומפוזבילי. drawWithCache
קורא ל-onDrawBehind
או ל-onDrawWithContent
בתוכו – ומספק מנגנון לשמירת אובייקטים שנוצרו בהם במטמון.
Modifier.drawWithContent
: בחירה של סדר השרטוט
Modifier.drawWithContent
מאפשר לבצע פעולות של DrawScope
לפני או אחרי התוכן של ה-composable. חשוב להפעיל את drawContent
כדי להציג את התוכן בפועל של ה-composable. בעזרת המשתנה הזה תוכלו לקבוע את סדר הפעולות, אם אתם רוצים שהתוכן יופיע לפני או אחרי פעולות הציור בהתאמה אישית.
לדוגמה, אם רוצים לעבד הדרגתי רדיאלי מעל התוכן כדי ליצור אפקט של חור מנעול של פנס בממשק המשתמש, ניתן לבצע את הפעולות הבאות:
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
, מחוץ ל-modifer. עם זאת, לא תמיד אפשר לעשות זאת כי לא תמיד יש לכם גישה ליצירה המוזיקלית. שימוש ב-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
.
אפשר להוסיף אנימציה או לשנות אותן בלי שיהיה צורך לבצע מחדש את ה-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)
, הטרנספורמציות מתחילות מהפינה הימנית העליונה של הרכיב הניתן לקיבוץ.
אם משנים את המקור באמצעות טרנספורמציה 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
.
מחוץ למסך
התוכן של התוכן הקומפוזבילי תמיד עובר רסטרציה למרקם או למפת סיביות (bitmap) שלא מופיעים במסך לפני העיבוד ליעד. האפשרות הזו שימושית להחלה של פעולות 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 הזה). לאחר מכן הוא מציג אותו מעל לתוכן שכבר מעובד במסך, בלי להשפיע על התוכן שכבר מוצג.
אם לא משתמשים ב-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()
. לאחר מכן אפשר לחלץ פעולות שרטוט נפוצות למגבילי שרטוט בהתאמה אישית, כדי לנקות את הקוד ולספק רכיבי wrapper נוחים, לדוגמה, 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 פיתוח נייטיב