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

توفّر 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

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

// 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 Animatable. على سبيل المثال، يعدّل هذا المقتطف 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)
        }
    }
}