העברה לממשקי API של Indication ו-Ripple

כדי לשפר את ביצועי הקומפוזיציה של רכיבים אינטראקטיביים שמשתמשים ב-Modifier.clickable, הוספנו ממשקי API חדשים. ממשקי ה-API האלה מאפשרים הטמעות יעילות יותר של Indication, כמו ripples.

androidx.compose.foundation:foundation:1.7.0+ ו-androidx.compose.material:material-ripple:1.7.0+ כוללים את השינויים הבאים ב-API:

הוצא משימוש

החלפה

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

במקום זאת, ממשקי API חדשים של ripple() מסופקים בספריות Material.

הערה: בהקשר הזה, 'ספריות חומרים' מתייחסות אל androidx.compose.material:material, androidx.compose.material3:material3, androidx.wear.compose:compose-material ו-androidx.wear.compose:compose-material3.

RippleTheme

למשל:

  • משתמשים בממשקי ה-API של ספריית Material‏ RippleConfiguration, או
  • יצירת הטמעה של מערכת עיצוב משלכם

בדף הזה מתואר השינוי בהתנהגות, ומופיעות הוראות להעברה לממשקי ה-API החדשים.

שינוי בהתנהגות

גרסאות הספריות הבאות כוללות שינוי בהתנהגות של אפקט הגל:

  • androidx.compose.material:material:1.7.0+
  • androidx.compose.material3:material3:1.3.0+
  • androidx.wear.compose:compose-material:1.4.0+

בגרסאות האלה של ספריות Material כבר לא נעשה שימוש ב-rememberRipple(), אלא בממשקי ה-API החדשים של אפקט האדווה. כתוצאה מכך, הם לא שולחים שאילתות אל LocalRippleTheme. לכן, אם תגדירו את LocalRippleTheme באפליקציה, רכיבי Material לא ישתמשו בערכים האלה.

בקטעים הבאים מוסבר איך לבצע מיגרציה לממשקי ה-API החדשים.

העברה מ-rememberRipple אל ripple

שימוש בספריית חומרים

אם אתם משתמשים בספריית Material, מחליפים את rememberRipple() ישירות בקריאה ל-ripple() מהספרייה המתאימה. ה-API הזה יוצר אפקט אדווה באמצעות ערכים שנגזרים מממשקי ה-API של ערכת העיצוב Material. לאחר מכן, מעבירים את האובייקט שמוחזר אל Modifier.clickable או אל רכיבים אחרים.

לדוגמה, בקטע הקוד הבא נעשה שימוש בממשקי API שיצאו משימוש:

Box(
    Modifier.clickable(
        onClick = {},
        interactionSource = remember { MutableInteractionSource() },
        indication = rememberRipple()
    )
) {
    // ...
}

צריך לשנות את קטע הקוד שלמעלה כך שייראה כך:

@Composable
private fun RippleExample() {
    Box(
        Modifier.clickable(
            onClick = {},
            interactionSource = remember { MutableInteractionSource() },
            indication = ripple()
        )
    ) {
        // ...
    }
}

שימו לב: ripple() היא כבר לא פונקציה שאפשר להרכיב, ואין צורך לזכור אותה. אפשר גם להשתמש בו מחדש בכמה רכיבים, בדומה לשימוש במאפייני שינוי. לכן, כדאי להעביר את יצירת האפקט של האדווה לערך ברמה העליונה כדי לחסוך בהקצאות.

הטמעה של מערכת עיצוב בהתאמה אישית

אם אתם מטמיעים מערכת עיצוב משלכם, ובעבר השתמשתם ב-rememberRipple() יחד עם RippleTheme מותאם אישית כדי להגדיר את האפקט של הגל, במקום זאת אתם צריכים לספק API משלכם לאפקט הגל, שמקצה ל-API של צומת הגל את הפונקציות שמוצגות ב-material-ripple. לאחר מכן, הרכיבים יכולים להשתמש באפקט האדווה שלכם, שמשתמש ישירות בערכי העיצוב. מידע נוסף זמין במאמר בנושא מעבר מ-RippleTheme.

העברה מ-RippleTheme

שימוש ב-RippleTheme כדי להשבית את אפקט הגל ברכיב מסוים

הספריות material ו-material3 חושפות את RippleConfiguration ו-LocalRippleConfiguration, שמאפשרות להגדיר את המראה של האדוות בתוך עץ משנה. שימו לב שהתכונות RippleConfiguration ו-LocalRippleConfiguration הן ניסיוניות ומיועדות להתאמה אישית של כל רכיב בנפרד. התאמה אישית גלובלית או התאמה אישית של כל הנושא לא נתמכות בממשקי ה-API האלה. מידע נוסף על תרחיש השימוש הזה זמין במאמר שימוש ב-RippleTheme כדי לשנות באופן גלובלי את כל האפקטים של הגלים באפליקציה.

לדוגמה, בקטע הקוד הבא נעשה שימוש בממשקי API שיצאו משימוש:

private object DisabledRippleTheme : RippleTheme {

    @Composable
    override fun defaultColor(): Color = Color.Transparent

    @Composable
    override fun rippleAlpha(): RippleAlpha = RippleAlpha(0f, 0f, 0f, 0f)
}

// ...
    CompositionLocalProvider(LocalRippleTheme provides DisabledRippleTheme) {
        Button {
            // ...
        }
    }

צריך לשנות את קטע הקוד שלמעלה כך שייראה כך:

CompositionLocalProvider(LocalRippleConfiguration provides null) {
    Button {
        // ...
    }
}

שימוש ב-RippleTheme כדי לשנות את הצבע או את ערך האלפא של אפקט האדווה ברכיב נתון

כפי שמתואר בסעיף הקודם, RippleConfiguration ו-LocalRippleConfiguration הם ממשקי API ניסיוניים שמיועדים רק להתאמה אישית של כל רכיב בנפרד.

לדוגמה, בקטע הקוד הבא נעשה שימוש בממשקי API שיצאו משימוש:

private object DisabledRippleThemeColorAndAlpha : RippleTheme {

    @Composable
    override fun defaultColor(): Color = Color.Red

    @Composable
    override fun rippleAlpha(): RippleAlpha = MyRippleAlpha
}

// ...
    CompositionLocalProvider(LocalRippleTheme provides DisabledRippleThemeColorAndAlpha) {
        Button {
            // ...
        }
    }

צריך לשנות את קטע הקוד שלמעלה כך שייראה כך:

@OptIn(ExperimentalMaterialApi::class)
private val MyRippleConfiguration =
    RippleConfiguration(color = Color.Red, rippleAlpha = MyRippleAlpha)

// ...
    CompositionLocalProvider(LocalRippleConfiguration provides MyRippleConfiguration) {
        Button {
            // ...
        }
    }

שימוש ב-RippleTheme כדי לשנות באופן גלובלי את כל האדוות באפליקציה

בעבר, יכולתם להשתמש ב-LocalRippleTheme כדי להגדיר את התנהגות האפקט ברמה של כל הנושא. הנקודה הזו הייתה למעשה נקודת שילוב בין קבצים מקומיים של מערכת עיצוב מותאמת אישית לבין Ripple. במקום להציג פרימיטיב כללי של עיצוב, material-ripple מציג עכשיו פונקציה של createRippleModifierNode(). הפונקציה הזו מאפשרת לספריות של מערכת העיצוב ליצור הטמעה של wrapper מסדר גבוה יותר, שמבצעת שאילתה על ערכי הנושא שלה ואז מעבירה את ההטמעה של אפקט האדווה לצומת שנוצר על ידי הפונקציה הזו.

כך מערכות עיצוב יכולות לשלוח שאילתות ישירות לגבי מה שהן צריכות, ולחשוף את כל שכבות העיצוב הנדרשות שניתנות להגדרה על ידי המשתמשים, בלי להתאים את עצמן למה שמופיע בשכבת material-ripple. בנוסף, השינוי הזה הופך את ההתאמה של האפקט לערכת העיצוב או למפרט לברורה יותר, כי ה-API של האפקט הוא זה שמגדיר את החוזה, ולא נגזר באופן מרומז מערכת העיצוב.

הנחיות אפשר למצוא בהטמעה של ripple API בספריות Material. צריך להחליף את הקריאות ל-Material composition locals לפי הצורך במערכת העיצוב שלכם.

העברה מ-Indication אל IndicationNodeFactory

עוברים ליד Indication

אם אתם רק יוצרים Indication כדי להעביר אותו, למשל כדי ליצור אפקט של גל שמעבירים ל-Modifier.clickable או ל-Modifier.indication, אתם לא צריכים לבצע שינויים. ‫IndicationNodeFactory עובר בירושה מ-Indication, כך שהכול ימשיך להתקמפל ולפעול.

יצירת Indication

אם אתם יוצרים הטמעה משלכם של Indication, ברוב המקרים ההעברה אמורה להיות פשוטה. לדוגמה, נניח שיש Indication שמחיל אפקט של שינוי גודל בלחיצה:

object ScaleIndication : Indication {
    @Composable
    override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
        // key the remember against interactionSource, so if it changes we create a new instance
        val instance = remember(interactionSource) { ScaleIndicationInstance() }

        LaunchedEffect(interactionSource) {
            interactionSource.interactions.collectLatest { interaction ->
                when (interaction) {
                    is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition)
                    is PressInteraction.Release -> instance.animateToResting()
                    is PressInteraction.Cancel -> instance.animateToResting()
                }
            }
        }

        return instance
    }
}

private class ScaleIndicationInstance : IndicationInstance {
    var currentPressPosition: Offset = Offset.Zero
    val animatedScalePercent = Animatable(1f)

    suspend fun animateToPressed(pressPosition: Offset) {
        currentPressPosition = pressPosition
        animatedScalePercent.animateTo(0.9f, spring())
    }

    suspend fun animateToResting() {
        animatedScalePercent.animateTo(1f, spring())
    }

    override fun ContentDrawScope.drawIndication() {
        scale(
            scale = animatedScalePercent.value,
            pivot = currentPressPosition
        ) {
            this@drawIndication.drawContent()
        }
    }
}

אפשר להעביר את זה בשני שלבים:

  1. העברה של ScaleIndicationInstance כך שיהיה DrawModifierNode. ממשק ה-API של DrawModifierNode דומה מאוד לזה של IndicationInstance: הוא חושף פונקציה ContentDrawScope#draw() ששווה מבחינת הפונקציונליות לפונקציה IndicationInstance#drawContent(). צריך לשנות את הפונקציה הזו, ואז להטמיע את הלוגיקה של collectLatest ישירות בתוך הצומת, במקום ב-Indication.

    לדוגמה, בקטע הקוד הבא נעשה שימוש בממשקי API שיצאו משימוש:

    private class ScaleIndicationInstance : IndicationInstance {
        var currentPressPosition: Offset = Offset.Zero
        val animatedScalePercent = Animatable(1f)
    
        suspend fun animateToPressed(pressPosition: Offset) {
            currentPressPosition = pressPosition
            animatedScalePercent.animateTo(0.9f, spring())
        }
    
        suspend fun animateToResting() {
            animatedScalePercent.animateTo(1f, spring())
        }
    
        override fun ContentDrawScope.drawIndication() {
            scale(
                scale = animatedScalePercent.value,
                pivot = currentPressPosition
            ) {
                this@drawIndication.drawContent()
            }
        }
    }

    צריך לשנות את קטע הקוד שלמעלה כך שייראה כך:

    private class ScaleIndicationNode(
        private val interactionSource: InteractionSource
    ) : Modifier.Node(), DrawModifierNode {
        var currentPressPosition: Offset = Offset.Zero
        val animatedScalePercent = Animatable(1f)
    
        private suspend fun animateToPressed(pressPosition: Offset) {
            currentPressPosition = pressPosition
            animatedScalePercent.animateTo(0.9f, spring())
        }
    
        private suspend fun animateToResting() {
            animatedScalePercent.animateTo(1f, spring())
        }
    
        override fun onAttach() {
            coroutineScope.launch {
                interactionSource.interactions.collectLatest { interaction ->
                    when (interaction) {
                        is PressInteraction.Press -> animateToPressed(interaction.pressPosition)
                        is PressInteraction.Release -> animateToResting()
                        is PressInteraction.Cancel -> animateToResting()
                    }
                }
            }
        }
    
        override fun ContentDrawScope.draw() {
            scale(
                scale = animatedScalePercent.value,
                pivot = currentPressPosition
            ) {
                this@draw.drawContent()
            }
        }
    }

  2. העברה של ScaleIndication כדי להטמיע את IndicationNodeFactory. הלוגיקה של האוסף הועברה עכשיו לצומת, ולכן מדובר באובייקט פשוט מאוד של factory, שהאחריות היחידה שלו היא ליצור מופע של צומת.

    לדוגמה, בקטע הקוד הבא נעשה שימוש בממשקי API שיצאו משימוש:

    object ScaleIndication : Indication {
        @Composable
        override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
            // key the remember against interactionSource, so if it changes we create a new instance
            val instance = remember(interactionSource) { ScaleIndicationInstance() }
    
            LaunchedEffect(interactionSource) {
                interactionSource.interactions.collectLatest { interaction ->
                    when (interaction) {
                        is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition)
                        is PressInteraction.Release -> instance.animateToResting()
                        is PressInteraction.Cancel -> instance.animateToResting()
                    }
                }
            }
    
            return instance
        }
    }

    צריך לשנות את קטע הקוד שלמעלה כך שייראה כך:

    object ScaleIndicationNodeFactory : IndicationNodeFactory {
        override fun create(interactionSource: InteractionSource): DelegatableNode {
            return ScaleIndicationNode(interactionSource)
        }
    
        override fun hashCode(): Int = -1
    
        override fun equals(other: Any?) = other === this
    }

שימוש ב-Indication ליצירת IndicationInstance

ברוב המקרים, כדאי להשתמש ב-Modifier.indication כדי להציג את Indication עבור רכיב. עם זאת, במקרים נדירים שבהם אתם יוצרים באופן ידני IndicationInstance באמצעות rememberUpdatedInstance, אתם צריכים לעדכן את ההטמעה כדי לבדוק אם Indication הוא IndicationNodeFactory, וכך להשתמש בהטמעה קלה יותר. לדוגמה, Modifier.indication יבצע הקצאה פנימית לצומת שנוצר אם הוא IndicationNodeFactory. אם לא, המערכת תשתמש ב-Modifier.composed כדי להתקשר אל rememberUpdatedInstance.