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

توفّر أداة 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.Nodes التي تنفّذ ObserverNode توفير تنفيذها الخاص لـ onObservedReadsChanged الذي سيتم استدعاؤه استجابةً للتغييرات في عناصر اللقطات التي تتم قراءتها ضمن كتلة observeReads.

عيّنة

DelegatingNode

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

يمكن أن يكون ذلك مفيدًا لإنشاء عمليات استخدام متعددة للعُقد في عملية واحدة.

عيّنة

TraversableNode

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

عيّنة

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

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. At this point, you should update the nodes' properties to correspond with the updated parameters. إنّ إمكانية إعادة استخدام العُقد بهذه الطريقة هي أساس تحسينات الأداء التي يوفّرها Modifier.Node، لذا عليك تعديل العقدة الحالية بدلاً من إنشاء عقدة جديدة في طريقة update. في مثال الدائرة، يتم تعديل لون العقدة.

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

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

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

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