به 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 کتابخانه Material استفاده کنید، یا
  • سیستم طراحی خودتان را بسازید و از پیاده‌سازی موجی آن لذت ببرید

این صفحه تأثیر تغییر رفتار و دستورالعمل‌های مهاجرت به APIهای جدید را شرح می‌دهد.

تغییر رفتار

نسخه‌های کتابخانه زیر شامل تغییر رفتار ripple هستند:

  • 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های جدید ripple استفاده می‌کنند. در نتیجه، 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() دیگر یک تابع قابل ترکیب نیست و نیازی به حفظ کردن ندارد. همچنین می‌تواند در چندین کامپوننت، مشابه اصلاح‌کننده‌ها، مورد استفاده مجدد قرار گیرد، بنابراین استخراج ripple ایجاد شده را به یک مقدار سطح بالا در نظر بگیرید تا در تخصیص‌ها صرفه‌جویی شود.

پیاده‌سازی سیستم طراحی سفارشی

اگر در حال پیاده‌سازی سیستم طراحی خودتان هستید و قبلاً از rememberRipple() به همراه یک RippleTheme سفارشی برای پیکربندی ripple استفاده می‌کردید، باید API ripple خودتان را ارائه دهید که به APIهای گره ripple که در material-ripple قرار دارند، واگذار شود. سپس، اجزای شما می‌توانند از ripple خودتان استفاده کنند که مستقیماً از مقادیر تم شما استفاده می‌کند. برای اطلاعات بیشتر، به Migrate from 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 در سطح کل قالب استفاده کنید. این اساساً یک نقطه ادغام بین ترکیب سیستم طراحی سفارشی محلی و ripple بود. به جای نمایش یک قالب‌بندی اولیه عمومی، material-ripple اکنون یک تابع createRippleModifierNode() را نمایش می‌دهد. این تابع به کتابخانه‌های سیستم طراحی اجازه می‌دهد تا پیاده‌سازی wrapper مرتبه بالاتر ایجاد کنند، که مقادیر قالب خود را پرس‌وجو می‌کند و سپس پیاده‌سازی ripple را به گره ایجاد شده توسط این تابع واگذار می‌کند.

این به سیستم‌های طراحی اجازه می‌دهد تا مستقیماً آنچه را که نیاز دارند جستجو کنند و هرگونه لایه قالب‌بندی مورد نیاز قابل تنظیم توسط کاربر را در بالا نمایش دهند، بدون اینکه مجبور باشند با آنچه در لایه material-ripple ارائه می‌شود، مطابقت داشته باشند. این تغییر همچنین باعث می‌شود که قالب/مشخصات ripple با صراحت بیشتری مطابقت داشته باشد، زیرا خود API ripple است که آن قرارداد را تعریف می‌کند، نه اینکه به طور ضمنی از قالب مشتق شده باشد.

برای راهنمایی، به پیاده‌سازی ripple API در کتابخانه‌های Material مراجعه کنید و فراخوانی‌های محلی Material composition را در صورت نیاز برای سیستم طراحی خود جایگزین کنید.

مهاجرت از 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 است: این API یک تابع 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. برای پیاده‌سازی IndicationNodeFactory ScaleIndication مهاجرت دهید. از آنجا که منطق مجموعه اکنون به گره منتقل شده است، این یک شیء 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 استفاده خواهد کرد.