Compose מספקת הרבה משנים להתנהגויות נפוצות, אבל אפשר גם ליצור משנים מותאמים אישית.
למקשי הצירופים יש כמה חלקים:
- מפעל לשינוי
- זוהי פונקציית הרחבה ב-
Modifier, שמספקת API אידיומטי לשינוי ומאפשרת לשרשר שינויים. ה-modifier factory יוצר את רכיבי ה-modifier שמשמשים את Compose כדי לשנות את ממשק המשתמש.
- זוהי פונקציית הרחבה ב-
- רכיב modifier
- כאן אתם יכולים להגדיר את אופן הפעולה של משנה ההתנהגות.
יש כמה דרכים להטמיע משנה מותאם אישית, בהתאם לפונקציונליות הנדרשת. לרוב, הדרך הכי פשוטה להטמיע משנה מותאם אישית היא להטמיע מפעל משנים מותאם אישית שמשלב מפעלים אחרים של משנים שכבר הוגדרו. אם אתם צריכים התנהגות מותאמת אישית יותר, אתם יכולים להטמיע את רכיב השינוי באמצעות ממשקי ה-API של Modifier.Node, שהם ברמה נמוכה יותר אבל מספקים יותר גמישות.
שרשור של משני מיקום קיימים
לעתים קרובות אפשר ליצור משנים מותאמים אישית באמצעות משנים קיימים. לדוגמה, Modifier.clip() מיושם באמצעות ה-modifier graphicsLayer. השיטה הזו משתמשת ברכיבי שינוי קיימים, ואתם מספקים מפעל שינוי מותאם אישית משלכם.
לפני שמטמיעים משנה מותאם אישית משלכם, כדאי לבדוק אם אפשר להשתמש באותה אסטרטגיה.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
לחלופין, אם אתם מוצאים שאתם חוזרים על אותה קבוצה של משנים לעיתים קרובות, אתם יכולים להגדיר אותם כמשנה משלכם:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
יצירת משנה בהתאמה אישית באמצעות מפעל משנים שניתן להרכבה
אפשר גם ליצור משנה מותאם אישית באמצעות פונקציה קומפוזבילית כדי להעביר ערכים למשנה קיים. הפעולה הזו נקראת פקטורי (factory) של מעבדים קומפוזביליים.
שימוש בפקטורי (factory) של עיבודים שניתנים להרכבה כדי ליצור עיבוד מאפשר גם להשתמש בממשקי API של Compose ברמה גבוהה יותר, כמו animate*AsState וממשקי API אחרים של Compose לאנימציה שמגובה על ידי ערך דינמי. לדוגמה, בקטע הקוד הבא מוצג משנה שמפעיל אנימציה של שינוי אלפא כשמפעילים או משביתים אותו:
@Composable fun Modifier.fade(enable: Boolean): Modifier { val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f) return this then Modifier.graphicsLayer { this.alpha = alpha } }
אם מדובר בשיטת נוחות שמאפשרת לספק ערכי ברירת מחדל מ-CompositionLocal, הדרך הקלה ביותר להטמיע את המאפיין הזה היא להשתמש במפעל מאפיינים קומפוזביליים:
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
יש כמה מגבלות לגישה הזו, שמפורטות בקטעים הבאים.
הערכים של CompositionLocal נפתרים באתר הקריאה של מפעל השינויים
כשיוצרים משנה בהתאמה אישית באמצעות מפעל של משנים קומפוזביליים, משתנים מקומיים של קומפוזיציה מקבלים את הערך מעץ הקומפוזיציה שבו הם נוצרו, ולא מזה שבו הם נמצאים בשימוש. זה עלול להוביל לתוצאות לא צפויות. לדוגמה, נסתכל על הדוגמה של שינוי מקומי של קומפוזיציה שהוזכרה קודם, שמיושמת בצורה קצת שונה באמצעות פונקציה קומפוזבילית:
@Composable fun Modifier.myBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) } @Composable fun MyScreen() { CompositionLocalProvider(LocalContentColor provides Color.Green) { // Background modifier created with green background val backgroundModifier = Modifier.myBackground() // LocalContentColor updated to red CompositionLocalProvider(LocalContentColor provides Color.Red) { // Box will have green background, not red as expected. Box(modifier = backgroundModifier) } } }
אם אתם רוצים שהמשנה יפעל בצורה אחרת, כדאי להשתמש במקום זאת ב-Modifier.Node מותאם אישית, כי המערכת תפתור את הבעיה של לוקאלים של קומפוזיציה באתר השימוש ותוכל להעביר אותם בבטחה.
השינויים בפונקציות קומפוזביליות אף פעם לא נפסחים
אי אפשר לדלג על משני מפעלים שאפשר להרכיב כי אי אפשר לדלג על פונקציות שאפשר להרכיב שמחזירות ערכים. כלומר, המערכת תקרא לפונקציית השינוי בכל קומפוזיציה מחדש, וזה עלול להיות יקר אם הקומפוזיציה מחדש מתבצעת לעיתים קרובות.
חובה לקרוא למשנים של פונקציות קומפוזביליות בתוך פונקציה קומפוזבילית
בדומה לכל הפונקציות הניתנות להגדרה, צריך לקרוא למשנה של מפעל הניתן להגדרה מתוך קומפוזיציה. ההגבלה הזו קובעת לאן אפשר להעלות את המאפיין, כי אי אפשר להעלות אותו מחוץ לקומפוזיציה. לעומת זאת, אפשר להוציא פונקציות ליצירת משנים שאינן קומפוזביליות מפונקציות קומפוזביליות כדי לאפשר שימוש חוזר קל יותר ולשפר את הביצועים:
val extractedModifier = Modifier.background(Color.Red) // Hoisted to save allocations @Composable fun Modifier.composableModifier(): Modifier { val color = LocalContentColor.current.copy(alpha = 0.5f) return this then Modifier.background(color) } @Composable fun MyComposable() { val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher }
הטמעה של התנהגות מותאמת אישית של מקשי שינוי באמצעות Modifier.Node
Modifier.Node הוא API ברמה נמוכה יותר ליצירת משנים ב-Compose. זה אותו API שבו Compose מטמיע את השינויים שלו, והוא הדרך הכי יעילה ליצור שינויים בהתאמה אישית.
הטמעה של משנה מותאם אישית באמצעות Modifier.Node
הטמעה של משנה מותאם אישית באמצעות Modifier.Node מורכבת משלושה חלקים:
- הטמעה של
Modifier.Nodeשמכילה את הלוגיקה ואת המצב של משנה המחירים. -
ModifierNodeElementשיוצרת ומעדכנת מופעים של צומת modifier. - מפעל אופציונלי של משנים, כפי שפורט קודם.
מחלקות ModifierNodeElement הן בלי שמירת מצב, ומוקצים מופעים חדשים בכל רה-קומפוזיציה, בעוד שמחלקות Modifier.Node יכולות להיות עם שמירת מצב ולהמשיך להתקיים בכמה רה-קומפוזיציות, ואפילו לעשות בהן שימוש חוזר.
בקטע הבא מוסבר על כל חלק ומוצגת דוגמה ליצירת משנה מאפיינים בהתאמה אישית לציור עיגול.
Modifier.Node
ההטמעה של Modifier.Node (בדוגמה הזו, CircleNode) מטמיעה את הפונקציונליות של שינוי המחיר המותאם אישית.
// Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
בדוגמה הזו, הפונקציה מציירת את העיגול בצבע שמועבר לפונקציית השינוי.
צומת מטמיע את Modifier.Node וגם אפס או יותר סוגי צמתים. יש סוגים שונים של צמתים, בהתאם לפונקציונליות שנדרשת לשינוי. בדוגמה הקודמת, צריך להיות אפשר לצייר, ולכן היא מטמיעה את DrawModifierNode, שמאפשר לה לדרוס את ה-method draw.
אלה הסוגים הזמינים:
Node |
שימוש |
קישור לדוגמה |
|
||
|
||
הטמעה של הממשק הזה מאפשרת ל- |
||
|
||
|
||
|
||
|
||
|
||
|
||
האפשרות הזו שימושית אם רוצים לשלב כמה הטמעות של צמתים בהטמעה אחת. |
||
מאפשר לשיעורים של |
הצמתים נפסלים אוטומטית כשקוראים לעדכון ברכיב המתאים שלהם. בדוגמה שלנו מדובר ב-DrawModifierNode, ולכן בכל פעם שמתבצע עדכון ברכיב, הצומת מפעיל ציור מחדש והצבע שלו מתעדכן בצורה נכונה. אפשר לבטל את ההסכמה לביטול אוטומטי של תוקף הצמתים, כמו שמתואר בקטע ביטול ההסכמה לביטול אוטומטי של תוקף הצמתים.
ModifierNodeElement
ModifierNodeElement הוא מחלקה שלא ניתן לשנות, שמכילה את הנתונים ליצירה או לעדכון של משנה בהתאמה אישית:
// ModifierNodeElement private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() { override fun create() = CircleNode(color) override fun update(node: CircleNode) { node.color = color } }
הטמעות של ModifierNodeElement צריכות לבטל את השיטות הבאות:
-
create: זו הפונקציה שיוצרת את מופע של צומת ה-modifier. הפונקציה הזו נקראת כדי ליצור את הצומת כשמחילים את שינוי המאפיינים בפעם הראשונה. בדרך כלל, הפעולה הזו כוללת בנייה של הצומת והגדרת הפרמטרים שהועברו למפעל של שינוי המאפיינים. -
update: הפונקציה הזו מופעלת בכל פעם שהמשנה הזה מסופק באותו המקום שבו הצומת הזה כבר קיים, אבל מאפיין השתנה. הערך הזה נקבע על ידי המתודהequalsשל המחלקה. צומת המשנה שנוצר קודם נשלח כפרמטר לקריאהupdate. בשלב הזה, צריך לעדכן את מאפייני הצמתים כך שיתאימו לפרמטרים המעודכנים. היכולת לעשות שימוש חוזר בצמתים בצורה הזו היא המפתח לשיפור הביצועים ש-Modifier.Nodeמביא איתו. לכן, צריך לעדכן את הצומת הקיים במקום ליצור צומת חדש בשיטהupdate. בדוגמה של העיגול, הצבע של הצומת מתעדכן.
בנוסף, בהטמעות של ModifierNodeElement צריך להטמיע גם את equals ואת hashCode. הפונקציה update תיקרא רק אם השוואה של שוויון עם הרכיב הקודם תחזיר false.
בדוגמה שלמעלה נעשה שימוש בסיווג נתונים כדי להשיג את המטרה הזו. השיטות האלה משמשות כדי לבדוק אם צריך לעדכן צומת או לא. אם לאלמנט יש מאפיינים שלא משפיעים על הצורך לעדכן צומת, או אם אתם רוצים להימנע ממחלקות נתונים מסיבות של תאימות בינארית, אתם יכולים להטמיע באופן ידני את equals ו-hashCode, למשל, את האלמנט של משנה הריווח.
פקטורי (factory) של עיבודים
זהו ממשק ה-API הציבורי של ה-modifier. ברוב ההטמעות נוצר רכיב modifier והוא מתווסף לשרשרת modifier:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
דוגמה מלאה
שלושת החלקים האלה משולבים יחד כדי ליצור את שינוי ההתאמה האישית לצורך שרטוט עיגול באמצעות ממשקי ה-API של Modifier.Node:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color) // ModifierNodeElement private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() { override fun create() = CircleNode(color) override fun update(node: CircleNode) { node.color = color } } // Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
מצבים נפוצים שבהם משתמשים במאפיין Modifier.Node
כשיוצרים משנים בהתאמה אישית באמצעות Modifier.Node, יכולות להיות כמה סיטואציות נפוצות.
אפס פרמטרים
אם למחרוזת לשינוי אין פרמטרים, אין צורך לעדכן אותה, וגם אין צורך שהיא תהיה מחלקת נתונים. הדוגמה הבאה היא הטמעה של משנה שמוסיף ריווח קבוע לרכיב Composable:
fun Modifier.fixedPadding() = this then FixedPaddingElement data object FixedPaddingElement : ModifierNodeElement<FixedPaddingNode>() { override fun create() = FixedPaddingNode() override fun update(node: FixedPaddingNode) {} } class FixedPaddingNode : LayoutModifierNode, Modifier.Node() { private val PADDING = 16.dp override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints ): MeasureResult { val paddingPx = PADDING.roundToPx() val horizontal = paddingPx * 2 val vertical = paddingPx * 2 val placeable = measurable.measure(constraints.offset(-horizontal, -vertical)) val width = constraints.constrainWidth(placeable.width + horizontal) val height = constraints.constrainHeight(placeable.height + vertical) return layout(width, height) { placeable.place(paddingPx, paddingPx) } } }
הפניה למשתנים מקומיים של רכיב
המשנים Modifier.Node לא עוקבים אוטומטית אחרי שינויים באובייקטים של מצב Compose, כמו CompositionLocal. היתרון של משני Modifier.Node על פני משנים שנוצרים רק באמצעות מפעל קומפוזבילי הוא שהם יכולים לקרוא את הערך של המקומי בהרכב מהמקום שבו נעשה שימוש במשנה בעץ ממשק המשתמש, ולא מהמקום שבו המשנה מוקצה, באמצעות currentValueOf.
עם זאת, מופעים של צומת משנה לא עוקבים אוטומטית אחרי שינויים במצב. כדי להגיב אוטומטית לשינוי מקומי של יצירה מוזיקלית, אפשר לקרוא את הערך הנוכחי שלה בתוך היקף:
DrawModifierNode:ContentDrawScope-
LayoutModifierNode:MeasureScopeוIntrinsicMeasureScope SemanticsModifierNode:SemanticsPropertyReceiver
בדוגמה הזו, הערך של LocalContentColor נבדק כדי לצייר רקע על סמך הצבע שלו. ContentDrawScope עוקב אחרי שינויים בתמונת מצב, ולכן הוא מצייר מחדש באופן אוטומטי כשערך LocalContentColor משתנה:
class BackgroundColorConsumerNode : Modifier.Node(), DrawModifierNode, CompositionLocalConsumerModifierNode { override fun ContentDrawScope.draw() { val currentColor = currentValueOf(LocalContentColor) drawRect(color = currentColor) drawContent() } }
כדי להגיב לשינויים במצב מחוץ להיקף ולעדכן אוטומטית את ה-modifier, צריך להשתמש ב-ObserverModifierNode.
לדוגמה, Modifier.scrollable משתמש בטכניקה הזו כדי לעקוב אחרי שינויים ב-LocalDensity. דוגמה פשוטה:
class ScrollableNode : Modifier.Node(), ObserverModifierNode, CompositionLocalConsumerModifierNode { // Place holder fling behavior, we'll initialize it when the density is available. val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity)) override fun onAttach() { updateDefaultFlingBehavior() observeReads { currentValueOf(LocalDensity) } // monitor change in Density } override fun onObservedReadsChanged() { // if density changes, update the default fling behavior. updateDefaultFlingBehavior() } private fun updateDefaultFlingBehavior() { val density = currentValueOf(LocalDensity) defaultFlingBehavior.flingDecay = splineBasedDecay(density) } }
יצירת אנימציה של שינוי
להטמעות של Modifier.Node יש גישה אל coroutineScope. כך אפשר להשתמש בממשקי ה-API של Compose Animatable. לדוגמה, קטע הקוד הזה משנה את CircleNode שמופיע למעלה כך שהוא יופיע וייעלם שוב ושוב:
class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode { private lateinit var alpha: Animatable<Float, AnimationVector1D> override fun ContentDrawScope.draw() { drawCircle(color = color, alpha = alpha.value) drawContent() } override fun onAttach() { alpha = Animatable(1f) coroutineScope.launch { alpha.animateTo( 0f, infiniteRepeatable(tween(1000), RepeatMode.Reverse) ) { } } } }
שיתוף מצב בין משנים באמצעות העברה
אפשר להעביר הרשאות לצמתים אחרים באמצעות משני Modifier.Node. יש הרבה תרחישי שימוש לזה, כמו חילוץ הטמעות נפוצות בין משני תכונות שונות, אבל אפשר להשתמש בזה גם כדי לשתף מצב נפוץ בין משני תכונות.
לדוגמה, הטמעה בסיסית של צומת משנה שניתן ללחוץ עליו ומשתף נתוני אינטראקציה:
class ClickableNode : DelegatingNode() { val interactionData = InteractionData() val focusableNode = delegate( FocusableNode(interactionData) ) val indicationNode = delegate( IndicationNode(interactionData) ) }
ביטול ההסכמה לביטול אוטומטי של תוקף הצומת
צמתי Modifier.Node מתבטלים אוטומטית כשהעדכון של הקריאות התואמות שלהם מתבצע.ModifierNodeElement אם אתם משתמשים במגדירי שינוי מורכבים, יכול להיות שתרצו להשבית את ההתנהגות הזו כדי לקבל שליטה מדויקת יותר על המקרים שבהם מגדיר השינוי מבטל שלבים.
האפשרות הזו שימושית במיוחד אם המגדיר המותאם אישית משנה גם את הפריסה וגם את הציור. אם משביתים את האפשרות להוספה אוטומטית לרשימת הפסילה, המערכת תפסול את הציור רק אם יש שינוי במאפיינים שקשורים לציור, כמו color. כך תוכלו למנוע פסילה של פריסת המודעה ולשפר את הביצועים של שינוי המודעה.
דוגמה היפותטית לכך מוצגת בדוגמה הבאה עם משנה שיש לו למבדא color, size ו-onClick כמאפיינים. המשנה הזה מבטל רק את מה שנדרש, ומדלג על ביטולים לא נחוצים:
class SampleInvalidatingNode( var color: Color, var size: IntSize, var onClick: () -> Unit ) : DelegatingNode(), LayoutModifierNode, DrawModifierNode { override val shouldAutoInvalidate: Boolean get() = false private val clickableNode = delegate( ClickablePointerInputNode(onClick) ) fun update(color: Color, size: IntSize, onClick: () -> Unit) { if (this.color != color) { this.color = color // Only invalidate draw when color changes invalidateDraw() } if (this.size != size) { this.size = size // Only invalidate layout when size changes invalidateMeasurement() } // If only onClick changes, we don't need to invalidate anything clickableNode.update(onClick) } override fun ContentDrawScope.draw() { drawRect(color) } override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints ): MeasureResult { val size = constraints.constrain(size) val placeable = measurable.measure(constraints) return layout(size.width, size.height) { placeable.place(0, 0) } } }