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

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

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

  • مصنع للعناصر المعدِّلة
    • هذه دالة تمديد في 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)

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

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

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

تنفيذ مُعدِّل مخصّص باستخدام 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، ما يسمح له بالتجاوز عن طريقة الرسم.

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

العقدة

الاستخدام

رابط نموذجي

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 إلّا إذا كانت مقارنة التساوي مع العنصر السابق تعرِض قيمة خاطئة.

يستخدم المثال أعلاه فئة بيانات لتحقيق ذلك. تُستخدَم هذه الطرق للتحقّق مما إذا كانت إحدى العقد بحاجة إلى التحديث أم لا. إذا كان العنصر يتضمّن سمات لا تساهم في تحديد ما إذا كان يجب تعديل عقدة معيّنة أو لا، أو إذا كنت تريد تجنُّب استخدام فئات data لأسباب تتعلّق بالتوافق مع الثنائيات، يمكنك تنفيذ 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 الضبط التغييرات تلقائيًا في CompositionLocal، مثل عناصر حالة الإنشاء. تتمثل ميزة عوامل تعديل Modifier.Node على عوامل تعديل التي تم إنشاؤها للتو باستخدام مصنع تركيبي في أنّها يمكنها قراءة قيمة التركيبة المحلية من مكان استخدام المُعدِّل في شجرة currentValueOf UI، وليس من مكان تخصيص المُعدِّل.

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

يرصد هذا المثال قيمة 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 Animatable APIs. على سبيل المثال، يُعدِّل هذا المقتطف الرمز CircleNode أعلاه لتلاشيه وتظهريه بشكل متكرّر:

class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode {
    private val alpha = Animatable(1f)

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

    override fun onAttach() {
        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)
        }
    }
}