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

توفر ميزة إنشاء الرسائل العديد من التعديلات للسلوكيات الشائعة بشكل فوري، ولكن يمكنك أيضًا إنشاء مفاتيح تعديل مخصّصة بنفسك

تشتمل المعدِّلات على أجزاء متعددة:

  • مصنع تعديل
    • هذه دالة إضافة في 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. في مثال الدائرة، يتم تحديث لون العقدة.

بالإضافة إلى ذلك، تحتاج عمليات تنفيذ ModifierNodeElement أيضًا إلى تنفيذ equals وhashCode. لن يتم استدعاء الدالة 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 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 و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)
        }
    }
}