انتقال به 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() جدید در کتابخانه‌های Material ارائه شده است.

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

RippleTheme

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

  • از میاناهای برنامه‌سازی کاربردی کتابخانه Material 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 از این مقادیر استفاده نخواهند کرد.

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

انتقال از 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 فقط برای سفارشی‌سازی هر مؤلفه درنظر گرفته شده‌اند. سفارشی‌سازی سراسری/در سطح زمینه با این میاناهای برنامه‌سازی کاربردی پشتیبانی نمی‌شود؛ برای اطلاعات بیشتر درباره این مورد استفاده، استفاده از 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 ارائه می‌شود مطابقت داشته باشند. این تغییر همچنین به‌طور صریح‌تر مشخص می‌کند که موجک از کدام زمینه/مشخصات پیروی می‌کند، زیرا این خود میانای برنامه‌سازی کاربردی موجک است که این قرارداد را تعریف می‌کند، نه اینکه به‌طور ضمنی از زمینه مشتق شود.

برای راهنمایی، به پیاده‌سازی میانای برنامه‌سازی کاربردی موج در کتابخانه‌های 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 انتقال دهید. سطح میانای برنامه‌سازی کاربردی برای DrawModifierNode بسیار شبیه به IndicationInstance است: این میانا تابع 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. ScaleIndication را برای پیاده‌سازی IndicationNodeFactory انتقال دهید. Because the collection logic is now moved into the node, this is a very simple factory object whose only responsibility is to create a node instance.

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

    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 استفاده خواهد کرد.