העברה לממשקי 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 של ספריית החומרים 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

אם אתם משתמשים בספריית 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 שיצאו משימוש:

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.