העברה לממשקי 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.

הערה: בהקשר זה, האפשרות "ספריות חומרים" מתייחס אל 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 החדשים של Repple. כתוצאה מכך, הם לא שולחים שאילתה לגבי LocalRippleTheme. לכן, אם מגדירים את LocalRippleTheme באפליקציה, חומר רכיבים לא ישתמשו בערכים האלה.

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

שדרוג הגרסה של ספריית Material ללא העברה

כדי לבטל את החסימה של שדרוג גרסאות של ספריות, אפשר להשתמש API של LocalUseFallbackRippleImplementation CompositionLocal להגדרה רכיבי חומר כדי לחזור להתנהגות הישנה:

CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
    MaterialTheme {
        App()
    }
}

חשוב להקפיד לספק את התוכן הזה מחוץ ל-MaterialTheme כדי שההדים הישנים יוכלו מסופקים דרך LocalIndication.

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

העברה מ-RippleTheme

ביטול זמני של שינוי בהתנהגות

לספריות של חומרים יש ערך זמני מסוג CompositionLocal, LocalUseFallbackRippleImplementation, ואפשר להשתמש בו כדי להגדיר רכיבי החומר שצריך לחזור להשתמש בהם באמצעות rememberRipple. כך, rememberRipple ממשיך/ה לשאילתה של LocalRippleTheme.

קטע הקוד הבא מדגים איך להשתמש LocalUseFallbackRippleImplementation CompositionLocal API:

CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
    MaterialTheme {
        App()
    }
}

אם משתמשים בעיצוב אפליקציה מותאם אישית שמבוסס על Material, אפשר הוסיפו באופן בטוח את הקומפוזיציה המקומית כחלק מהנושא של האפליקציה:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyAppTheme(content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
        MaterialTheme(content = content)
    }
}

למידע נוסף, ראו שדרוג הספרייה 'חומרי לימוד' ללא העברה.

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

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

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

העברה מ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.