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

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

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

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

يسجِّل هذا المثال قيمة 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 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 وonClick lambda. لا يلغي هذا المعدِّل سوى ما هو مطلوب، ويتخطى أي إلغاء غير صحيح:

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