إنشاء تعديلات مخصّصة

توفّر Compose العديد من المعدِّلات للسلوكيات الشائعة الجاهزة للاستخدام، ولكن يمكنك أيضًا إنشاء معدِّلات مخصّصة.

تتضمّن المعدِّلات أجزاءً متعددة:

  • مصنع معدِّلات
    • هذه دالة إضافية في Modifier، توفّر واجهة برمجة تطبيقات اصطلاحية للمعدِّل وتسمح بربط المعدِّلات معًا. ينتج مصنع المعدِّلات عناصر المعدِّلات التي يستخدمها Compose لتعديل واجهة المستخدم.
  • عنصر معدِّل
    • هذا هو المكان الذي يمكنك فيه تنفيذ سلوك المعدِّل.

تتوفّر عدة طرق لتنفيذ أداة تعديل مخصّصة حسب الوظيفة المطلوبة. في كثير من الأحيان، تكون أبسط طريقة لتنفيذ معدِّل مخصّص هي تنفيذ مصنع معدِّل مخصّص يجمع بين مصانع معدِّلات أخرى سبق تحديدها. إذا كنت بحاجة إلى سلوك مخصّص أكثر، يمكنك تنفيذ العنصر المعدِّل باستخدام واجهات برمجة التطبيقات Modifier.Node، وهي ذات مستوى أدنى ولكنها توفّر مرونة أكبر.

ربط مفاتيح التعديل الحالية معًا

من الممكن غالبًا إنشاء معدِّلات مخصّصة باستخدام المعدِّلات الحالية. على سبيل المثال، يتم تنفيذ Modifier.clip() باستخدام المعدِّل graphicsLayer. تستخدِم هذه الاستراتيجية عناصر معدِّلات حالية، وتوفّر أنت مصنع المعدِّلات المخصّص الخاص بك.

قبل تنفيذ معدِّل مخصّص خاص بك، تحقّق مما إذا كان بإمكانك استخدام الاستراتيجية نفسها.

fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)

أو إذا تكررت مجموعة معدّلات البحث نفسها كثيرًا، يمكنك تضمينها في معدّل بحث خاص بك:

fun Modifier.myBackground(color: Color) = padding(16.dp)
    .clip(RoundedCornerShape(8.dp))
    .background(color)

إنشاء معدِّل مخصّص باستخدام مصنع معدِّلات قابل للإنشاء

يمكنك أيضًا إنشاء معدِّل مخصّص باستخدام دالة مركّبة لتمرير القيم إلى معدِّل حالي. يُعرف ذلك باسم مصنع المعدِّلات القابلة للإنشاء.

يتيح لك استخدام مصنع معدِّلات قابل للإنشاء لإنشاء معدِّل أيضًا استخدام واجهات برمجة تطبيقات Compose ذات المستوى الأعلى، مثل animate*AsState وغيرها من واجهات برمجة التطبيقات للرسوم المتحركة المستندة إلى حالة Compose. على سبيل المثال، يعرض المقتطف التالي معدِّلاً يحرّك تغييرًا في قيمة ألفا عند تفعيله أو إيقافه:

@Composable
fun Modifier.fade(enable: Boolean): Modifier {
    val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f)
    return this then Modifier.graphicsLayer { this.alpha = alpha }
}

إذا كان المعدِّل المخصّص عبارة عن طريقة سهلة لتوفير القيم التلقائية من CompositionLocal، فإنّ أسهل طريقة لتنفيذ ذلك هي استخدام مصنع معدِّلات قابل للإنشاء:

@Composable
fun Modifier.fadedBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

يتضمّن هذا الأسلوب بعض التحذيرات التي تم توضيحها في الأقسام التالية.

يتم تحديد قيم CompositionLocal في موقع استدعاء دالة إنشاء المعدِّل

عند إنشاء معدِّل مخصّص باستخدام مصنع معدِّلات قابل للإنشاء، تأخذ المتغيرات المحلية الخاصة بالتجميع القيمة من شجرة التجميع التي تم إنشاؤها فيها، وليس من المكان الذي تم استخدامها فيه. يمكن أن يؤدي ذلك إلى نتائج غير متوقعة. على سبيل المثال، لنأخذ مثال معدِّل التركيبة المحلية المذكور سابقًا، والذي تم تنفيذه بشكل مختلف قليلاً باستخدام دالة قابلة للإنشاء:

@Composable
fun Modifier.myBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

@Composable
fun MyScreen() {
    CompositionLocalProvider(LocalContentColor provides Color.Green) {
        // Background modifier created with green background
        val backgroundModifier = Modifier.myBackground()

        // LocalContentColor updated to red
        CompositionLocalProvider(LocalContentColor provides Color.Red) {

            // Box will have green background, not red as expected.
            Box(modifier = backgroundModifier)
        }
    }
}

إذا لم يكن هذا هو السلوك المتوقّع للمعدِّل، استخدِم معدِّلاً مخصّصًا Modifier.Node بدلاً من ذلك، لأنّه سيتم حلّ المتغيرات المحلية للتركيب بشكل صحيح في موقع الاستخدام ويمكن نقلها بأمان.

لا يتم أبدًا تخطّي معدِّلات الدوال القابلة للإنشاء

لا يتم تخطّي معدِّلات المصنع القابلة للإنشاء أبدًا لأنّه لا يمكن تخطّي الدوال القابلة للإنشاء التي تتضمّن قيمًا معروضة. وهذا يعني أنّه سيتم استدعاء دالة المعدِّل في كل عملية إعادة إنشاء، ما قد يكون مكلفًا إذا تم إعادة الإنشاء بشكل متكرّر.

يجب استدعاء معدِّلات الدوال المركّبة داخل دالة مركّبة

وكما هو الحال مع جميع الدوال القابلة للإنشاء، يجب استدعاء معدِّل مصنع قابل للإنشاء من داخل عملية الإنشاء. يحدّد هذا الإعداد المكان الذي يمكن فيه نقل المعدِّل، إذ لا يمكن نقله خارج التركيب. بالمقارنة، يمكن نقل مصانع المعدِّلات غير المركّبة خارج الدوال المركّبة لتسهيل إعادة استخدامها وتحسين الأداء:

val extractedModifier = Modifier.background(Color.Red) // Hoisted to save allocations

@Composable
fun Modifier.composableModifier(): Modifier {
    val color = LocalContentColor.current.copy(alpha = 0.5f)
    return this then Modifier.background(color)
}

@Composable
fun MyComposable() {
    val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher
}

تنفيذ سلوك معدِّل مخصّص باستخدام Modifier.Node

‫Modifier.Node هي واجهة برمجة تطبيقات ذات مستوى منخفض لإنشاء أدوات تعديل في Compose. وهي واجهة برمجة التطبيقات نفسها التي تنفّذ فيها Compose أدوات التعديل الخاصة بها، وهي الطريقة الأفضل أداءً لإنشاء أدوات تعديل مخصّصة.

تنفيذ أداة تعديل مخصّصة باستخدام Modifier.Node

هناك ثلاثة أجزاء لتنفيذ أداة تعديل مخصّصة باستخدام Modifier.Node:

  • تنفيذ Modifier.Node يحتوي على منطق المعدِّل وحالته.
  • ModifierNodeElement تنشئ مثيلات عقدة المعدِّل وتعدّلها.
  • مصنع معدِّل اختياري، كما هو موضّح سابقًا

ففئات ModifierNodeElement لا تحتفظ بأي حالة ويتم تخصيص مثيلات جديدة لها في كل عملية إعادة التكوين، بينما يمكن أن تحتفظ فئات Modifier.Node بحالة وتستمر في العمل خلال عمليات إعادة التكوين المتعددة، ويمكن حتى إعادة استخدامها.

يوضّح القسم التالي كل جزء ويعرض مثالاً على إنشاء أداة تعديل مخصّصة لرسم دائرة.

Modifier.Node

ينفّذ رمز Modifier.Node (في هذا المثال، CircleNode) وظيفة المعدِّل المخصّص.

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

في هذا المثال، يتم رسم الدائرة باللون الذي تم تمريره إلى دالة المعدِّل.

تنفّذ العُقدة Modifier.Node بالإضافة إلى صفر أو أكثر من أنواع العُقد. تتوفّر أنواع مختلفة من العُقد استنادًا إلى الوظيفة التي يتطلّبها المعدِّل. يحتاج المثال السابق إلى إمكانية الرسم، لذا فهو ينفّذ DrawModifierNode، ما يتيح له إلغاء طريقة الرسم.

في ما يلي الأنواع المتاحة:

Node

الاستخدام

رابط العيّنة

LayoutModifierNode

Modifier.Node يغيّر طريقة قياس المحتوى المضمّن وتنسيقه.

عيّنة

DrawModifierNode

Modifier.Node يرسم في مساحة التصميم.

عيّنة

CompositionLocalConsumerModifierNode

يتيح تنفيذ هذه الواجهة لتطبيق Modifier.Node قراءة البيانات المحلية الخاصة بالتكوين.

عيّنة

SemanticsModifierNode

Modifier.Node يضيف مفتاحًا/قيمة دلالية لاستخدامها في الاختبار وإمكانية الوصول وحالات الاستخدام المشابهة.

عيّنة

PointerInputModifierNode

Modifier.Node يتلقّى PointerInputChanges

عيّنة

ParentDataModifierNode

Modifier.Node يوفّر البيانات لتصميم العنصر الرئيسي.

عيّنة

LayoutAwareModifierNode

Modifier.Node يتلقّى عمليات ردّ الاتصال onMeasured وonPlaced.

عيّنة

GlobalPositionAwareModifierNode

Modifier.Node يتلقّى عملية ردّ الاتصال onGloballyPositioned مع LayoutCoordinates النهائي للتنسيق عندما يكون الموضع العام للمحتوى قد تغيّر.

عيّنة

ObserverModifierNode

يمكن Modifier.Node التي تنفّذ ObserverNode توفير تنفيذ خاص بها لـ onObservedReadsChanged سيتم استدعاؤه استجابةً للتغييرات التي تطرأ على عناصر اللقطة التي تتم قراءتها ضمن كتلة observeReads.

عيّنة

DelegatingNode

Modifier.Node يمكنه تفويض العمل إلى مثيلات Modifier.Node أخرى.

يمكن أن يكون هذا مفيدًا لدمج عمليات تنفيذ عقد متعددة في عملية تنفيذ واحدة.

عيّنة

TraversableNode

تسمح لفئات Modifier.Node بالانتقال للأعلى أو للأسفل في شجرة العُقد لفئات من النوع نفسه أو لمفتاح معيّن.

عيّنة

يتم إبطال صحة العُقد تلقائيًا عند طلب التعديل على العنصر المرتبط بها. بما أنّ مثالنا هو DrawModifierNode، كلما تم استدعاء عملية تعديل الوقت على العنصر، سيؤدي ذلك إلى إعادة رسم العقدة وتعديل لونها بشكل صحيح. يمكنك إيقاف الإبطال التلقائي، كما هو موضّح بالتفصيل في قسم إيقاف الإبطال التلقائي للعُقد.

ModifierNodeElement

ModifierNodeElement هو فئة غير قابلة للتغيير تحتوي على البيانات اللازمة لإنشاء أو تعديل أداة التعديل المخصّصة:

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

يجب أن تتجاوز عمليات تنفيذ ModifierNodeElement الطرق التالية:

  1. ‫create: هذه هي الدالة التي تنشئ عقدة المعدِّل. يتم استدعاء هذه الدالة لإنشاء العقدة عند تطبيق المعدِّل لأول مرة. عادةً، يتم ذلك من خلال إنشاء العقدة وإعدادها باستخدام المَعلمات التي تم تمريرها إلى مصنع المعدِّل.
  2. ‫update: يتم استدعاء هذه الدالة كلما تم توفير المعدِّل في الموضع نفسه الذي تتوفّر فيه هذه العقدة، ولكن تم تغيير إحدى السمات. يتم تحديد ذلك من خلال طريقة equals للفئة. يتم إرسال عقدة المعدِّل التي تم إنشاؤها سابقًا كمَعلمة إلى طلب update. في هذه المرحلة، عليك تعديل خصائص العُقد لتتوافق مع المَعلمات المعدَّلة. إنّ إمكانية إعادة استخدام العُقد بهذه الطريقة هي أساس التحسينات في الأداء التي توفّرها Modifier.Node، لذا عليك تعديل العقدة الحالية بدلاً من إنشاء عقدة جديدة في طريقة update. في مثال الدائرة، يتم تعديل لون العقدة.

بالإضافة إلى ذلك، يجب أن تتضمّن عمليات تنفيذ ModifierNodeElement أيضًا equals وhashCode. لن يتم استدعاء update إلا إذا كانت مقارنة المساواة مع العنصر السابق تعرض القيمة false.

يستخدم المثال السابق فئة بيانات لتحقيق ذلك. تُستخدَم هذه الطرق للتحقّق ممّا إذا كانت إحدى العُقد بحاجة إلى تعديل أم لا. إذا كان العنصر يتضمّن سمات لا تساهم في تحديد ما إذا كان يجب تعديل العقدة، أو إذا كنت تريد تجنُّب فئات البيانات لأسباب تتعلّق بالتوافق الثنائي، يمكنك تنفيذ equals وhashCode يدويًا، مثل عنصر معدِّل المساحة المتروكة.

مصنع أدوات التعديل

هذه هي مساحة واجهة برمجة التطبيقات العامة للمعدِّل. تنشئ معظم عمليات التنفيذ عنصر المعدِّل وتضيفه إلى سلسلة المعدِّلات:

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

مثال كامل

تتكامل هذه الأجزاء الثلاثة لإنشاء أداة التعديل المخصّصة لرسم دائرة باستخدام واجهات برمجة التطبيقات Modifier.Node:

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

الحالات الشائعة التي تستخدم Modifier.Node

عند إنشاء معدّلات مخصّصة باستخدام Modifier.Node، إليك بعض الحالات الشائعة التي قد تواجهها.

بدون مَعلمات

إذا لم يكن للمعدِّل أي مَعلمات، لن يحتاج إلى تعديل أبدًا، ولن يحتاج أيضًا إلى أن يكون فئة بيانات. في ما يلي مثال على تنفيذ معدِّل يضيف مقدارًا ثابتًا من المساحة المتروكة إلى عنصر قابل للإنشاء:

fun Modifier.fixedPadding() = this then FixedPaddingElement

data object FixedPaddingElement : ModifierNodeElement<FixedPaddingNode>() {
    override fun create() = FixedPaddingNode()
    override fun update(node: FixedPaddingNode) {}
}

class FixedPaddingNode : LayoutModifierNode, Modifier.Node() {
    private val PADDING = 16.dp

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val paddingPx = PADDING.roundToPx()
        val horizontal = paddingPx * 2
        val vertical = paddingPx * 2

        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))

        val width = constraints.constrainWidth(placeable.width + horizontal)
        val height = constraints.constrainHeight(placeable.height + vertical)
        return layout(width, height) {
            placeable.place(paddingPx, paddingPx)
        }
    }
}

المراجع المحلية للمكوّن

لا تراقب معدِّلات Modifier.Node تلقائيًا التغييرات التي تطرأ على عناصر حالة Compose، مثل CompositionLocal. تتميّز عناصر تعديل Modifier.Node عن عناصر التعديل التي يتم إنشاؤها باستخدام مصنع دالة مركّبة بأنّها يمكنها قراءة قيمة التكوين المحلية من المكان الذي يتم فيه استخدام عنصر التعديل في شجرة واجهة المستخدم، وليس من المكان الذي يتم فيه تخصيص عنصر التعديل، وذلك باستخدام currentValueOf.

ومع ذلك، لا تلاحظ مثيلات عقدة المعدِّل تلقائيًا تغييرات الحالة. للتفاعل تلقائيًا مع تغيير في قيمة CompositionLocal، يمكنك قراءة قيمتها الحالية داخل نطاق:

يراقب هذا المثال قيمة LocalContentColor لرسم خلفية استنادًا إلى لونها. بما أنّ ContentDrawScope يراقب التغييرات في اللقطة، تتم إعادة الرسم تلقائيًا عند تغيُّر قيمة LocalContentColor:

class BackgroundColorConsumerNode :
    Modifier.Node(),
    DrawModifierNode,
    CompositionLocalConsumerModifierNode {
    override fun ContentDrawScope.draw() {
        val currentColor = currentValueOf(LocalContentColor)
        drawRect(color = currentColor)
        drawContent()
    }
}

للتفاعل مع تغييرات الحالة خارج النطاق وتعديل المعدِّل تلقائيًا، استخدِم ObserverModifierNode.

على سبيل المثال، تستخدم Modifier.scrollable هذا الأسلوب لمراقبة التغييرات في LocalDensity. في ما يلي مثال مبسط:

class ScrollableNode :
    Modifier.Node(),
    ObserverModifierNode,
    CompositionLocalConsumerModifierNode {

    // Place holder fling behavior, we'll initialize it when the density is available.
    val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity))

    override fun onAttach() {
        updateDefaultFlingBehavior()
        observeReads { currentValueOf(LocalDensity) } // monitor change in Density
    }

    override fun onObservedReadsChanged() {
        // if density changes, update the default fling behavior.
        updateDefaultFlingBehavior()
    }

    private fun updateDefaultFlingBehavior() {
        val density = currentValueOf(LocalDensity)
        defaultFlingBehavior.flingDecay = splineBasedDecay(density)
    }
}

تحريك معدِّل

يمكن لعمليات تنفيذ Modifier.Node الوصول إلى coroutineScope. يتيح ذلك استخدام واجهات برمجة التطبيقات القابلة للتحريك في Compose. على سبيل المثال، يعدّل هذا المقتطف قيمة CircleNode المعروضة سابقًا لتظهر وتختفي بشكل متكرر:

class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode {
    private lateinit var alpha: Animatable<Float, AnimationVector1D>

    override fun ContentDrawScope.draw() {
        drawCircle(color = color, alpha = alpha.value)
        drawContent()
    }

    override fun onAttach() {
        alpha = Animatable(1f)
        coroutineScope.launch {
            alpha.animateTo(
                0f,
                infiniteRepeatable(tween(1000), RepeatMode.Reverse)
            ) {
            }
        }
    }
}

مشاركة الحالة بين المعدِّلات باستخدام التفويض

يمكن لمعدّلات Modifier.Node تفويض المهام إلى عُقد أخرى. هناك العديد من حالات الاستخدام لهذه الميزة، مثل استخراج عمليات التنفيذ الشائعة من خلال أدوات التعديل المختلفة، ولكن يمكن استخدامها أيضًا لمشاركة الحالة الشائعة بين أدوات التعديل.

على سبيل المثال، عملية تنفيذ أساسية لعقدة معدِّل قابلة للنقر تشارك بيانات التفاعل:

class ClickableNode : DelegatingNode() {
    val interactionData = InteractionData()
    val focusableNode = delegate(
        FocusableNode(interactionData)
    )
    val indicationNode = delegate(
        IndicationNode(interactionData)
    )
}

إيقاف ميزة "إبطال صحة العُقد تلقائيًا"

يتم إبطال صحة عقد Modifier.Node تلقائيًا عند تعديل عمليات استدعاء ModifierNodeElement المقابلة. بالنسبة إلى المعدِّلات المعقّدة، قد تحتاج إلى إيقاف هذا السلوك من أجل التحكّم بشكل أدق في الحالات التي يبطل فيها المعدِّل المراحل.

يكون ذلك مفيدًا بشكل خاص إذا كان المعدِّل المخصّص يعدّل كلاً من التنسيق والرسم. يسمح لك إيقاف ميزة الإبطال التلقائي بإبطال عملية الرسم فقط عندما تتغيّر الخصائص ذات الصلة بالرسم، مثل color. يؤدي ذلك إلى تجنُّب إبطال صحة التنسيق ويمكن أن يحسّن أداء المعدِّل.

يتم عرض مثال افتراضي على ذلك في المثال التالي مع معدِّل يتضمّن دوال lambda color وsize وonClick كسمات. لا يبطل هذا المعدِّل سوى ما هو مطلوب، مع تخطّي أي إبطال غير ضروري:

class SampleInvalidatingNode(
    var color: Color,
    var size: IntSize,
    var onClick: () -> Unit
) : DelegatingNode(), LayoutModifierNode, DrawModifierNode {
    override val shouldAutoInvalidate: Boolean
        get() = false

    private val clickableNode = delegate(
        ClickablePointerInputNode(onClick)
    )

    fun update(color: Color, size: IntSize, onClick: () -> Unit) {
        if (this.color != color) {
            this.color = color
            // Only invalidate draw when color changes
            invalidateDraw()
        }

        if (this.size != size) {
            this.size = size
            // Only invalidate layout when size changes
            invalidateMeasurement()
        }

        // If only onClick changes, we don't need to invalidate anything
        clickableNode.update(onClick)
    }

    override fun ContentDrawScope.draw() {
        drawRect(color)
    }

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val size = constraints.constrain(size)
        val placeable = measurable.measure(constraints)
        return layout(size.width, size.height) {
            placeable.place(0, 0)
        }
    }
}