ב-Compose יש הרבה תכונות שינוי להתנהגויות נפוצות, אבל אתם יכולים גם ליצור תכונות שינוי בהתאמה אישית.
למגדירים יש כמה חלקים:
- מפעל לשינוי
- זוהי פונקציית תוסף ב-
Modifier, שמספקת API אידיומטי ל-modifier ומאפשרת לשרשר modifiers. ה-modifier factory מייצר את רכיבי ה-modifier שבהם Compose משתמש כדי לשנות את ממשק המשתמש.
- זוהי פונקציית תוסף ב-
- רכיב לשינוי
- כאן אתם יכולים להטמיע את ההתנהגות של המגדיר.
יש כמה דרכים להטמיע משתנה מותאם אישית, בהתאם לפונקציונליות הנדרשת. בדרך כלל, הדרך הכי פשוטה להטמיע משתנה מותאם אישית היא להטמיע מפעל של משתנים מותאמים אישית שמשלב מפעלים אחרים של משתנים שכבר הוגדרו. אם אתם צריכים התנהגות מותאמת אישית יותר, אתם יכולים להטמיע את הרכיב modifier באמצעות ממשקי ה-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)
יצירת עיבוד בהתאמה אישית באמצעות מפעל עיבודים קומפוזבילי
אפשר גם ליצור מגדיר מותאם אישית באמצעות פונקציה קומפוזבילית כדי להעביר ערכים למגדיר קיים. זה נקרא composable modifier factory.
שימוש ב-composable modifier factory כדי ליצור מגדיל מאפשר גם להשתמש בממשקי API ברמה גבוהה יותר של פיתוח נייטיב, כמו animate*AsState וממשקי API אחרים של אנימציה שמגובים במצב פיתוח נייטיב. לדוגמה, קטע הקוד הבא מציג משתנה שיוצר אנימציה של שינוי אלפא כשהוא מופעל או מושבת:
@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 modifier factory:
@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 בהתאמה אישית במקום זאת, כי המשתנים המקומיים של הקומפוזיציה יפורשו בצורה נכונה באתר השימוש וניתן להעביר אותם בבטחה.
אף פעם לא מדלגים על פונקציות קומפוזביליות עם משתני שינוי
המערכת אף פעם לא מדלגת על פונקציות פקטורי קומפוזביליות של מגדירי שינוי, כי אי אפשר לדלג על פונקציות קומפוזביליות שיש להן ערכי החזרה. זה אומר שפונקציית ה-modifier תופעל בכל הרכבה מחדש, וזה יכול להיות יקר אם היא מורכבת מחדש לעיתים קרובות.
צריך לקרוא לשינויים של פונקציה קומפוזבילית בתוך פונקציה קומפוזבילית
כמו כל הפונקציות הקומפוזביליות, צריך לקרוא ל-composable factory modifier מתוך הקומפוזיציה. זה מגביל את המקום שאליו אפשר להעביר את ה-modifier, כי אי אפשר להעביר אותו מחוץ לקומפוזיציה. לעומת זאת, אפשר להעביר פונקציות מפעל של משתני שינוי לא קומפוזביליים אל מחוץ לפונקציות קומפוזביליות כדי לאפשר שימוש חוזר קל יותר ולשפר את הביצועים:
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 המותאם אישית.
// Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
בדוגמה הזו, הפונקציה מציירת את העיגול בצבע שהועבר לפונקציית ה-modifier.
צומת מטמיע את Modifier.Node וגם אפס או יותר סוגי צמתים. יש סוגי צמתים שונים, בהתאם לפונקציונליות שהמשתנה דורש. בדוגמה הקודמת, צריך להיות אפשר לצייר, ולכן היא מטמיעה את DrawModifierNode, שמאפשר לה לשנות את ה-method של הציור.
אלה סוגי המזהים הזמינים:
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. הפונקציה הזו נקראת כדי ליצור את הצומת כשמחילים את המשתנה בפעם הראשונה. בדרך כלל, הפעולה הזו כוללת את בניית הצומת והגדרת הפרמטרים שהועברו ל-modifier factory. -
update: הפונקציה הזו נקראת בכל פעם שהמשתנה הזה מסופק באותו מקום שבו הצומת הזה כבר קיים, אבל מאפיין השתנה. המידע הזה נקבע לפי ה-methodequalsשל המחלקה. צומת ה-modifier שנוצר קודם נשלח כפרמטר לקריאה ל-update. בשלב הזה, צריך לעדכן את המאפיינים של הצמתים כך שיתאימו לפרמטרים המעודכנים. היכולת לעשות שימוש חוזר בצמתים בדרך הזו היא המפתח לשיפור הביצועים ש-Modifier.Nodeמביא איתו. לכן, צריך לעדכן את הצומת הקיים במקום ליצור צומת חדש ב-methodupdate. בדוגמה של העיגול, הצבע של הצומת מתעדכן.
בנוסף, בהטמעות של ModifierNodeElement צריך להטמיע גם את equals ואת hashCode. הפונקציה update תיקרא רק אם השוואה שווה עם הרכיב הקודם תחזיר false.
בדוגמה הקודמת השתמשנו במחלקת נתונים כדי לעשות את זה. השיטות האלה משמשות כדי לבדוק אם צריך לעדכן צומת או לא. אם לאלמנט יש מאפיינים שלא משפיעים על הצורך לעדכן צומת, או אם אתם רוצים להימנע משימוש במחלקות נתונים מסיבות של תאימות בינארית, אתם יכולים להטמיע באופן ידני את equals ו-hashCode, לדוגמה, אלמנט ה-modifier של המרווח הפנימי.
מפעל ההרחבות
זהו ממשק ה-API הציבורי של המשתנה שלכם. ברוב ההטמעות, יוצרים את רכיב ה-modifier ומוסיפים אותו לשרשרת ה-modifier:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
דוגמה פתורה
שלושת החלקים האלה משתלבים כדי ליצור את ה-modifier בהתאמה אישית לציור עיגול באמצעות ממשקי ה-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, אלה כמה מצבים נפוצים שבהם אתם עשויים להיתקל.
אפס פרמטרים
אם ל-modifier אין פרמטרים, הוא לא צריך להתעדכן אף פעם, ובנוסף, הוא לא צריך להיות מחלקת נתונים. הדוגמה הבאה היא הטמעה של משתנה שמוסיף כמות קבועה של ריפוד לרכיב 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 מסוג Modifier.Node על פני משתני modifier שנוצרו באמצעות פונקציית factory קומפוזבילית הוא שהם יכולים לקרוא את הערך של המשתנה המקומי של הקומפוזיציה מהמקום שבו משתמשים במשתנה ה-modifier בעץ ממשק המשתמש, ולא מהמקום שבו משתנה ה-modifier מוקצה, באמצעות currentValueOf.
עם זאת, מופעים של צומתי שינוי לא צופים באופן אוטומטי בשינויי מצב. כדי להגיב באופן אוטומטי לשינוי ב-CompositionLocal, אפשר לקרוא את הערך הנוכחי שלו בתוך היקף:
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. כך אפשר להשתמש ב-Compose Animatable APIs. לדוגמה, קטע הקוד הזה משנה את 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 התואמות מתעדכנות. במקרה של משתנים מורכבים, יכול להיות שתרצו לבטל את ההתנהגות הזו כדי לקבל שליטה מדויקת יותר על המקרים שבהם המשתנה מבטל שלבים.
זה שימושי במיוחד אם משתמשים ב-modifier בהתאמה אישית כדי לשנות גם את הפריסה וגם את הציור. ביטול ההפעלה של ביטול התוקף האוטומטי מאפשר לבטל את התוקף של השרטוט רק כשמשתנים מאפיינים שקשורים לשרטוט, כמו color. כך אפשר למנוע ביטול של הפריסה ולשפר את הביצועים של ה-modifier.
בדוגמה הבאה מוצג תרחיש היפותטי עם משתנה שינוי שיש לו את המאפיינים color, size ו-onClick lambda. המשתנה הזה מבטל רק את מה שנדרש, ומדלג על כל ביטול מיותר:
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) } } }