النقل إلى واجهات برمجة تطبيقات المؤشرات والتموج

لتحسين أداء إنشاء المكوّنات التفاعلية التي تستخدم Modifier.clickable، أطلقنا واجهات برمجة تطبيقات جديدة. تتيح واجهات برمجة التطبيقات هذه عمليات تنفيذ Indication أكثر كفاءة، مثل التموجات.

تتضمّن الإصدارات androidx.compose.foundation:foundation:1.7.0+ وandroidx.compose.material:material-ripple:1.7.0+ تغييرات واجهة برمجة التطبيقات التالية:

تم إيقافها نهائيًا

الاستبدال

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

يتم توفير واجهات برمجة التطبيقات الجديدة ripple() في مكتبات 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() من المكتبة المعنية. تنشئ واجهة برمجة التطبيقات هذه تأثير تموّج باستخدام قيم مشتقة من واجهات برمجة التطبيقات الخاصة بمظهر 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 لتحديد سلوك التموج على مستوى المظهر بأكمله. كانت هذه النقطة في الأساس نقطة دمج بين العناصر المحلية المخصّصة لتصميم النظام وRipple. بدلاً من عرض عنصر أساسي عام لتطبيق السمات، يعرض material-ripple الآن الدالة createRippleModifierNode(). تسمح هذه الدالة لمكتبات نظام التصميم بإنشاء تنفيذ wrapper ذي ترتيب أعلى، والذي يستعلم عن قيم السمة ثم يفوّض تنفيذ التموج إلى العقدة التي تم إنشاؤها بواسطة هذه الدالة.

يتيح ذلك لأنظمة التصميم الاستعلام مباشرةً عن ما تحتاجه، وعرض أي طبقات مطلوبة لتحديد المظهر يمكن للمستخدم ضبطها في الأعلى بدون الحاجة إلى الالتزام بما يتم توفيره في الطبقة material-ripple. يؤدي هذا التغيير أيضًا إلى توضيح المظهر أو المواصفات التي يتوافق معها التأثير المرئي، لأنّ واجهة برمجة التطبيقات الخاصة بالتأثير المرئي هي التي تحدّد هذا العقد، بدلاً من أن يتم استنتاجه ضمنيًا من المظهر.

للحصول على إرشادات، راجِع تنفيذ واجهة برمجة التطبيقات 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 بما أنّ منطق التجميع قد تم نقله إلى العقدة، فإنّ هذا العنصر هو عنصر مصنع بسيط جدًا، ومسؤوليته الوحيدة هي إنشاء مثيل عقدة.

    على سبيل المثال، يستخدم المقتطف التالي واجهات برمجة التطبيقات المتوقّفة نهائيًا:

    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.