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

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

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

הוצא משימוש

החלפה

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

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

הערה: בהקשר הזה, "ספריות 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

אם אתם משתמשים בספריית Material, החליפו ישירות את rememberRipple() בקריאה ל-ripple() מהספרייה המתאימה. ה-API הזה יוצר אפקט אדווה באמצעות ערכים שנגזרים מממשקי ה-API של התאמת העיצוב של Google Material Design. לאחר מכן, מעבירים את האובייקט שהוחזר אל 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 כדי להגדיר את ההתנהגות של האפקט ברמת העיצוב. זו הייתה בעצם נקודת שילוב בין הרכיבים המקומיים של מערכת העיצוב בהתאמה אישית לבין האפקט. במקום לחשוף פרימיטיב כללי של עיצוב, material-ripple חושף עכשיו פונקציה של createRippleModifierNode(). הפונקציה הזו מאפשרת לספריות של מערכת העיצוב ליצור הטמעה של wrapper בסדר גבוה יותר, ששולחת שאילתות לגבי ערכי העיצוב שלהן ואז מקצה את הטמעת האפקט לצומת שנוצר על ידי הפונקציה הזו.

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

לקבלת הדרכה, עיינו בהטמעה של Ripple API בספריות Material, והחליפו את הקריאות ל-CompositionLocal של Material לפי הצורך במערכת העיצוב שלכם.

העברה מ-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. מכיוון שלוגיקת האיסוף הועברה לצומת, זהו אובייקט פשוט מאוד של מפעל, שהאחריות היחידה שלו היא ליצור מופע של צומת.

    לדוגמה, קטע הקוד הבא משתמש בממשקי ה-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.