ב-Compose יש הרבה משנים להתנהגויות נפוצות, אבל אתם יכולים גם ליצור משנים מותאמים אישית משלכם.
למשנים יש כמה חלקים:
- מפעל לשינוי מאפיינים
- זו פונקציית הרחבה ב-
Modifier
, שמספקת API אידיומטי לשינוי ומאפשרת לשרשר בקלות שינויים. ה-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)
יצירת משנה בהתאמה אישית באמצעות מפעל משנים שניתן להרכבה
אפשר גם ליצור משנה מותאם אישית באמצעות פונקציה שאפשר להרכיב כדי להעביר ערכים למשנה קיים. הפעולה הזו נקראת יצירת משנה (modifier) שאפשר להרכיב.
שימוש במפעל של משנים שניתנים להרכבה כדי ליצור משנה מאפשר גם שימוש בממשקי 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
מותאם אישית, כי משתנים מקומיים של קומפוזיציה ייפתרו בצורה נכונה באתר השימוש וניתן להעלות אותם בבטחה.
אף פעם לא מדלגים על משנים של פונקציות הניתנות להגדרה
לעולם לא מדלגים על משנים של פונקציות factory שאפשר להרכיב, כי אי אפשר לדלג על פונקציות שאפשר להרכיב שמחזירות ערכים. כלומר, הפונקציה של שינוי המאפיינים תיקרא בכל קומפוזיציה מחדש, וזה עלול להיות יקר אם הקומפוזיציה מחדש מתבצעת לעיתים קרובות.
חובה לקרוא למשנים של פונקציות הניתנות להגדרה בתוך פונקציה הניתנת להגדרה
בדומה לכל הפונקציות הניתנות להרכבה, צריך לקרוא למשנה של מפעל שניתן להרכבה מתוך קומפוזיציה. כך מגבילים את המיקום שאליו אפשר להעלות את המודיפייר, כי אי אפשר להעלות אותו מחוץ לקומפוזיציה. לעומת זאת, אפשר להוציא פונקציות ליצירת משנים שאי אפשר להרכיב מתוך פונקציות שאפשר להרכיב כדי לאפשר שימוש חוזר קל יותר ולשפר את הביצועים:
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
הן חסרות מצב, ומקצים מופעים חדשים בכל הרכבה מחדש. לעומת זאת, מחלקות 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
, שמאפשר לדרוס את שיטת הציור.
אלה הסוגים הזמינים:
צומת |
שימוש |
קישור לדוגמה |
תג |
||
|
||
הטמעה של הממשק הזה מאפשרת ל- |
||
|
||
|
||
|
||
|
||
|
||
|
||
האפשרות הזו יכולה להיות שימושית אם רוצים לשלב כמה הטמעות של צמתים להטמעה אחת. |
||
מאפשר ל- |
הצמתים נפסלים אוטומטית כשמפעילים עדכון ברכיב המתאים שלהם. מכיוון שהדוגמה שלנו היא 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
: זו הפונקציה שיוצרת מופע של צומת השינוי. הפונקציה הזו נקראת כדי ליצור את הצומת כשהמשנה מוחל בפעם הראשונה. בדרך כלל, הפעולה הזו כוללת בנייה של הצומת והגדרת הפרמטרים שהועברו למפעל של שינוי המאפיינים. -
update
: הפונקציה הזו מופעלת בכל פעם שהמשנה הזה מסופק באותו המקום שבו הצומת הזה כבר קיים, אבל מאפיין השתנה. הוא נקבע על ידי השיטהequals
של המחלקה. צומת השינוי שנוצר קודם נשלח כפרמטר לקריאהupdate
. בשלב הזה, צריך לעדכן את המאפיינים של הצמתים כך שיתאימו לפרמטרים המעודכנים. היכולת לעשות שימוש חוזר בצמתים בדרך הזו היא המפתח לשיפור הביצועים ש-Modifier.Node
מביא איתו. לכן, צריך לעדכן את הצומת הקיים ולא ליצור צומת חדש בשיטהupdate
. בדוגמה של העיגול, הצבע של הצומת מתעדכן.
בנוסף, בהטמעות של ModifierNodeElement
צריך להטמיע גם את equals
וגם את hashCode
. הפונקציה update
תופעל רק אם השוואה של שוויון עם הרכיב הקודם תחזיר false.
בדוגמה שלמעלה נעשה שימוש במחלקת נתונים כדי להשיג את זה. השיטות האלה משמשות כדי לבדוק אם צריך לעדכן צומת או לא. אם לרכיב יש מאפיינים שלא משפיעים על הצורך לעדכן צומת, או אם אתם רוצים להימנע ממחלקות נתונים מסיבות של תאימות בינארית, אתם יכולים להטמיע באופן ידני את equals
ואת hashCode
, למשל את רכיב שינוי הריווח.
Modifier factory
זהו ממשק ה-API הציבורי של התוסף. ברוב ההטמעות פשוט יוצרים את רכיב ה-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
, יכולות להיות כמה סיטואציות נפוצות:
אפס פרמטרים
אם למשנה אין פרמטרים, הוא לא צריך להתעדכן אף פעם, וגם לא צריך להיות מחלקת נתונים. דוגמה להטמעה של משנה שמוסיף ריווח קבוע לרכיב שאפשר להרכיב:
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) } } }
הפניה ללוקאלים של קומפוזיציה
השינויים באובייקטים של מצב Compose, כמו CompositionLocal
, לא משתקפים אוטומטית במגדירי Modifier.Node
. היתרון של Modifier.Node
modifiers על פני modifiers שנוצרו רק באמצעות composable factory הוא שהם יכולים לקרוא את הערך של composition local מהמקום שבו נעשה שימוש ב-modifier בעץ ממשק המשתמש, ולא מהמקום שבו הוקצה ה-modifier, באמצעות 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 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
, ולא תבטל את התוקף של הפריסה.
כך אפשר לשפר את הביצועים של משנה המחיר.
בדוגמה ההיפותטית הבאה מוצג משנה עם מאפייני 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) } } }