ב-Compose יש הרבה משתני אופן פעולה להתנהגויות נפוצות, אבל אפשר גם ליצור משתני אופן פעולה מותאמים אישית.
לשינויים יש כמה חלקים:
- מפעל של משתני ערך
- זוהי פונקציית תוסף ב-
Modifier
, שמספקת ממשק API שמותאם לשינוי, ומאפשרת לשרשר יחד קלילות שינויים. מפעל המשתנים מייצר את רכיבי המשתנים שבהם Compose משתמש כדי לשנות את ממשק המשתמש.
- זוהי פונקציית תוסף ב-
- רכיב שינוי
- כאן אפשר להטמיע את ההתנהגות של המשתנה המשתנה.
יש כמה דרכים להטמיע משתנה מותאם אישית, בהתאם לפונקציונליות הנדרשת. בדרך כלל, הדרך הקלה ביותר להטמיע פונקציית שינוי מותאמת אישית היא פשוט להטמיע מפעל של פונקציות שינוי בהתאמה אישית, שמכיל שילוב של מפעלים אחרים של פונקציות שינוי שכבר מוגדרים. אם אתם צריכים התנהגות מותאמת אישית יותר, תוכלו להטמיע את רכיב המשנה באמצעות ממשקי ה-API של Modifier.Node
, שהם ברמה נמוכה יותר אבל מספקים יותר גמישות.
קישור של מגבילים קיימים
בדרך כלל אפשר ליצור משתני התאמה אישית רק באמצעות משתני התאמה אישית קיימים. לדוגמה, Modifier.clip()
מוטמע באמצעות המשתנה 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)
יצירת מודификатор בהתאמה אישית באמצעות מפעל מודификаторים שאפשר לשלב
אפשר גם ליצור משתנה מותאם אישית באמצעות פונקציה שניתנת ליצירה כדי להעביר ערכים למשתנה קיים. המבנה הזה נקרא 'מפעל של מודификаторים שניתנים לשילוב'.
שימוש במפעל של מודификаторים שניתנים ליצירה כדי ליצור מודификатор מאפשר גם להשתמש בממשקי API ברמה גבוהה יותר, כמו 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
בהתאמה אישית, כי משתני ה-local של הקומפוזיציה ימוינו בצורה נכונה באתר השימוש וניתן יהיה להעביר אותם בבטחה.
אף פעם לא מדלגים על משתני פונקציה הניתנת להגדרה
מודификаторים של יצירת רכיבים לא דילוגים אף פעם, כי אי אפשר לדלג על פונקציות ניתנות ליצירה שיש להן ערכי החזרה. כלומר, פונקציית המשנה תופעל בכל פעם שמתבצעת יצירת קומפוזיציה מחדש, ויכול להיות שהפעולה הזו תהיה יקרה אם היא מתבצעת לעיתים קרובות.
חייבים להפעיל את המשתנים של פונקציות הניתנות להגדרה בתוך פונקציה הניתנת להגדרה
כמו כל הפונקציות הניתנות ליצירה, צריך להפעיל את המשתנה של המפעל הניתן ליצירה מתוך היצירה. כך אפשר להגביל את המיקום שאליו אפשר להעביר את המשתנה המשנה, כי אף פעם אי אפשר להעביר אותו מחוץ ליצירה. לעומת זאת, אפשר להוציא מתוך פונקציות מורכבות מפעלים של משתני אופן פעולה שלא ניתן לשלב, כדי לאפשר שימוש חוזר קל יותר ולשפר את הביצועים:
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
שיוצר ומעדכן מכונות של צמתים של משתני ערך.- מפעל אופציונלי של מודификаторים, כפי שמתואר למעלה.
כיתות ModifierNodeElement
הן ללא מצב (stateless), ומוקצים להן מופעים חדשים בכל יצירת קומפוזיציה מחדש. לעומת זאת, כיתות Modifier.Node
יכולות להיות עם מצב (stateful), והן ישרדו כמה פעולות של יצירת קומפוזיציה מחדש, ואפילו ניתן יהיה לעשות בהן שימוש חוזר.
בקטע הבא מוסבר על כל חלק ודוגמה ליצירת משתנה מותאם אישית לציור עיגול.
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
, שמאפשר לה לשנות את השיטה draw.
אלה סוגי הקבצים הזמינים:
Node |
שימוש |
קישור לדוגמה |
|
||
|
||
הטמעת הממשק הזה מאפשרת ל- |
||
|
||
|
||
|
||
|
||
|
||
|
||
אפשר להשתמש באפשרות הזו כדי ליצור קומפוזיציה של כמה הטמעות של צמתים ליצירת הטמעה אחת. |
||
מאפשר לכיתות |
הצמתים מתבטלים באופן אוטומטי כשמתבצעת קריאה ל-update על הרכיב התואם שלהם. מכיוון שהדוגמה שלנו היא DrawModifierNode
, בכל פעם שמפעילים את העדכון על הרכיב, הצומת מפעיל ציור מחדש והצבע שלו מתעדכן בצורה נכונה. אפשר לבטל את ההסכמה לביטול אוטומטי, כפי שמפורט בהמשך.
ModifierNodeElement
ModifierNodeElement
היא כיתת immutable שמכילה את הנתונים ליצירה או לעדכון של המשנה בהתאמה אישית:
// 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
: זוהי הפונקציה שיוצרת מופע של צומת המשתנה. הפונקציה הזו נקראת כדי ליצור את הצומת בפעם הראשונה שמחילים את המשתנה המשנה. בדרך כלל, הפעולה הזו כוללת יצירה של הצומת והגדרה שלו באמצעות הפרמטרים שהועברו למפעל המשתנים.update
: הפונקציה הזו נקראת בכל פעם שמספקים את המשתנה הזה באותו מקום שבו הצומת כבר קיים, אבל נכס השתנה. הוא נקבע לפי השיטהequals
של הכיתה. צומת המשתנה שנוצר קודם נשלח כפרמטר לקריאה ל-update
. בשלב הזה, צריך לעדכן את המאפיינים של הצמתים כך שיתואמים לפרמטרים המעודכנים. היכולת לעשות שימוש חוזר בצורה הזו בצמתים היא המפתח לשיפור הביצועים שמתקבל באמצעותModifier.Node
. לכן, צריך לעדכן את הצומת הקיים במקום ליצור צומת חדש בשיטהupdate
. בדוגמה שלנו של העיגול, הצבע של הצומת מתעדכן.
בנוסף, הטמעות של ModifierNodeElement
צריכות לכלול גם הטמעה של equals
ו-hashCode
. update
תופעל רק אם ההשוואה ל-equals עם הרכיב הקודם תחזיר false.
בדוגמה שלמעלה נעשה שימוש בכיתה של נתונים כדי להשיג זאת. השיטות האלה משמשות כדי לבדוק אם צריך לעדכן צומת או לא. אם לאלמנט יש מאפיינים שלא משפיעים על הצורך לעדכן צומת, או אם רוצים להימנע משימוש בקטגוריות נתונים מסיבות של תאימות בינארית, אפשר להטמיע את equals
ו-hashCode
באופן ידני, למשל אלמנט המשתנה של הפס הארגומנט.
מפעל של מקשי צירוף
זוהי פני השטח הציבוריים של ה-API של המשתנה. ברוב הטמעות ה-CSS, פשוט יוצרים את רכיב המשתנה ומוסיפים אותו לרשת המשתנים:
// 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
, אלה כמה מצבים נפוצים שעשויים להתרחש.
אפס פרמטרים
אם למשתנה המשנה אין פרמטרים, הוא אף פעם לא צריך לעדכן, וגם לא צריך להיות סיווג נתונים. זו דוגמה להטמעה של מודификатор שמחיל כמות קבועה של ריפוד על רכיב מורכב:
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() } }
כדי להגיב לשינויים במצב מחוץ להיקף ולעדכן את המשתנה באופן אוטומטי, משתמשים ב-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
. כך תוכלו להשתמש בCompose Animatable APIs. לדוגמה, קטע הקוד הזה משנה את הערך של CircleNode
שלמעלה כדי להציג אותו בהדרגה שוב ושוב:
class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode { private val alpha = Animatable(1f) override fun ContentDrawScope.draw() { drawCircle(color = color, alpha = alpha.value) drawContent() } override fun onAttach() { 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
מתעדכנות. לפעמים, במשתנה מורכב יותר, כדאי לבטל את ההסכמה להתנהגות הזו כדי לקבל שליטה מפורטת יותר לגבי הזמנים שבהם המשתנה יבטל שלבים.
האפשרות הזו שימושית במיוחד אם המשתנה המותאם אישית משנה גם את הפריסה וגם את תהליך הציור. אם תבטלו את ההסכמה לביטול התוקף האוטומטי, תוכלו לבטל את התוקף של ה-draw רק כשיש שינוי בנכסים שקשורים ל-draw, כמו color
, ולא לבטל את התוקף של הפריסה.
כך תוכלו לשפר את הביצועים של המשתנה המשנה.
דוגמה היפותטית לכך מוצגת בהמשך, עם שינוי שיש לו פונקציית lambda של 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) } } }