התכונה 'כתיבה' מספקת הרבה גורמים לשינוי להתנהגויות נפוצות, כבר מההתחלה, אבל תוכלו גם ליצור תכונות שינוי בהתאמה אישית משלכם.
לגורמי ההתאמה יש כמה חלקים:
- מפעל לשינוי תנאי
- זוהי פונקציית תוסף ב-
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 של אנימציה עם גיבוי מצב. לדוגמה, קטע הקוד הבא מציג
רכיב שמצורף אליו אנימציה של שינוי אלפא כשהיא מופעלת/מושבתת:
@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 ברמה נמוכה יותר ליצירת מגבילים בכתיבה. הוא
הוא אותו API שבו Composes מטמיעים התאמות משלהם,
גבוהה מאוד ליצירת מגבילים מותאמים אישית.
הטמעה של מגביל מותאם אישית באמצעות 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
, לכן צריך לעדכן את צומת קיים, במקום ליצור צומת חדש ב-methodupdate
. ב של עיגול לדוגמה, הצבע של הצומת מתעדכן.
בנוסף, צריך להטמיע גם את equals
בהטמעות של ModifierNodeElement
ו-hashCode
. תתבצע שיחה אל update
רק אם יש השוואה לערך
הרכיב הקודם מחזיר את הערך False.
בדוגמה שלמעלה נעשה שימוש בסיווג נתונים כדי להשיג את המטרה הזו. השיטות האלה משמשות
לבדוק אם צריך לעדכן צומת או לא. אם לרכיב יש מאפיינים
לא מציין אם צריך לעדכן צומת, או אם רוצים להימנע מנתונים
מסיבות של תאימות בינארית, אפשר להטמיע את equals
באופן ידני
וגם hashCode
, למשל רכיב ההתאמה של המרווח הפנימי.
מפעל התאמות
זוהי פלטפורמת ה-API הציבורית של הצירוף שלך. רוב היישומים פשוט יוצרים את רכיב הצירוף ומוסיפים אותו לשרשרת הצירוף:
// 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
לא מזהים באופן אוטומטי שינויים במצב הכתיבה
אובייקטים, כמו 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
עודכנו. לפעמים, באופן מורכב יותר, אתם עשויים
אם אתם רוצים לוותר על התנהגות זו, כדי לקבל שליטה פרטנית יותר על
מקש הצירוף מבטל את השלבים.
זה יכול להיות שימושי במיוחד אם הצירוף המותאם אישית משנה גם את הפריסה וגם
לצייר. כשאתם בוחרים שלא להשתמש בביטול אוטומטי, אתם יכולים פשוט לבטל את התוקף של השרטוט
רק מאפיינים שקשורים לשרטוט, כמו color
, משנים ולא מבטלים את הפריסה.
כך ניתן לשפר את הביצועים של המגביל.
למטה מוצגת דוגמה היפותטית לכך עם מגביל שיש לו 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) } } }