انتقال به Indication و Ripple APIs

برای بهبود عملکرد ترکیب‌بندی عناصر تعاملی که از Modifier.clickable استفاده می‌کنند، میاناهای برنامه‌سازی کاربردی جدیدی معرفی کرده‌ایم. این «میاناهای برنامه‌سازی کاربردی» امکان پیاده‌سازی‌های کارآمدتر Indication، مانند موج‌ها را فراهم می‌کنند.

‫androidx.compose.foundation:foundation:1.7.0+ و androidx.compose.material:material-ripple:1.7.0+ شامل تغییرات زیر در واسط برنامه‌سازی کاربردی می‌شود:

منسوخ

جایگزین

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

به‌جای آن، ripple() API جدید در کتابخانه‌های Material ارائه شده است.

توجه: در این زمینه، «کتابخانه‌های مواد» به androidx.compose.material:material، androidx.compose.material3:material3، androidx.wear.compose:compose-material، و androidx.wear.compose:compose-material3. اشاره دارد

RippleTheme

یکی از موارد زیر را انجام دهید:

  • از میاناهای برنامه‌سازی کاربردی RippleConfiguration «کتابخانه عناصر» استفاده کنید، یا
  • پیاده‌سازی موجی سیستم طراحی خودتان را بسازید

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

تغییر رفتار

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

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

این نسخه‌های کتابخانه‌های Material دیگر از rememberRipple() استفاده نمی‌کنند؛ درعوض، از میاناهای برنامه‌سازی کاربردی جدید موج استفاده می‌کنند. درنتیجه، آن‌ها LocalRippleTheme را پُرسمان نمی‌کنند. بنابراین، اگر LocalRippleTheme را در برنامه‌تان تنظیم کنید، عناصر Material از این مقادیر استفاده نخواهند کرد.

بخش‌های زیر نحوه انتقال به APIهای جدید را شرح می‌دهد.

انتقال از rememberRipple به ripple

استفاده از کتابخانه Material

اگر از کتابخانه Material استفاده می‌کنید، rememberRipple() را مستقیماً با فراخوانی ripple() از کتابخانه مربوطه جایگزین کنید. این API موجی ایجاد می‌کند که از مقادیر برگرفته از APIهای موضوع Material استفاده می‌کند. سپس، شیء برگشتی را به Modifier.clickable و/یا دیگر عناصر ارسال کنید.

برای مثال، تکه‌کد زیر از میاناهای برنامه‌سازی کاربردی منسوخ استفاده می‌کند:

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 سفارشی برای پیکربندی موج استفاده می‌کردید، باید به‌جای آن میانای برنامه‌سازی کاربردی موج خودتان را ارائه دهید که به میاناهای برنامه‌سازی کاربردی گره موج نمایان‌شده در material-ripple واگذار می‌کند. سپس، عناصر شما می‌توانند از موج خودشان استفاده کنند که مستقیماً مقادیر زمینه‌تان را مصرف می‌کند. برای اطلاعات بیشتر، انتقال ازRippleTheme را ببینید.

انتقال از RippleTheme

استفاده از RippleTheme برای غیرفعال کردن موج برای یک عنصر معین

کتابخانه‌های material و material3 RippleConfiguration و LocalRippleConfiguration را آشکار می‌کنند که به شما امکان می‌دهد ظاهر موج‌ها را در یک زیردرخت پیکربندی کنید. توجه داشته باشید که RippleConfiguration و LocalRippleConfiguration فقط برای سفارشی‌سازی هر عنصر درنظر گرفته شده‌اند. شخصی‌سازی سراسری/موضوعی با این APIها پشتیبانی نمی‌شود؛ برای اطلاعات بیشتر درباره این مورد استفاده، استفاده از RippleTheme برای تغییر سراسری همه موج‌ها در برنامه را ببینید.

برای مثال، تکه‌کد زیر از میاناهای برنامه‌سازی کاربردی منسوخ استفاده می‌کند:

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 فقط برای سفارشی‌سازی براساس هر عنصر درنظر گرفته شده‌اند.

برای مثال، تکه‌کد زیر از میاناهای برنامه‌سازی کاربردی منسوخ استفاده می‌کند:

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 موج تعریف می‌کند، نه اینکه به‌طور ضمنی از زمینه مشتق شود.

برای راهنمایی، پیاده‌سازی API موج را در کتابخانه‌های Material ببینید و فراخوانی‌های محلی‌های ترکیب 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 است: این API یک تابع ContentDrawScope#draw() را آشکار می‌کند که ازنظر عملکردی معادل IndicationInstance#drawContent() است. باید آن تابع را تغییر دهید، و سپس منطق collectLatest را مستقیماً در گره پیاده‌سازی کنید، نه در Indication.

    برای مثال، تکه‌کد زیر از میاناهای برنامه‌سازی کاربردی منسوخ استفاده می‌کند:

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

    برای مثال، تکه‌کد زیر از میاناهای برنامه‌سازی کاربردی منسوخ استفاده می‌کند:

    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 برای یک عنصر استفاده کنید. بااین‌حال، در موارد نادری که به‌صورت دستی بااستفاده از rememberUpdatedInstance IndicationInstance ایجاد می‌کنید، باید پیاده‌سازی‌تان را به‌روز کنید تا بررسی کند Indication IndicationNodeFactory است یا نه تا بتوانید از پیاده‌سازی سبک‌تری استفاده کنید. برای مثال، Modifier.indication اگر IndicationNodeFactory باشد، به‌صورت داخلی به گره ایجادشده واگذار می‌کند. اگر ندارید، از Modifier.composed برای تماس با rememberUpdatedInstance استفاده خواهد شد.