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

توفر ميزة 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)

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

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

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

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

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

نعرض أدناه مثالاً افتراضيًا على ذلك مع مُعدِّل يتضمن دالة واحدة (color) وsize وonClicklambda كسمات. لا يبطل هذا المُعدِّل سوى ما هو مطلوب، ويتخطّى أي إلغاء غير مطلوب:

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)
        }
    }
}