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

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