توفّر أداة "الإنشاء" العديد من المُعدِّلات للسلوكيات الشائعة بشكلٍ تلقائي، ولكن يمكنك أيضًا إنشاء مُعدِّلات مخصّصة.
تتضمّن العوامل المُعدِّلة أجزاء متعدّدة:
- مصنع للعناصر المعدِّلة
- هذه دالة تمديد في
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 معدّلاتها الخاصة بها، وهي الطريقة
الأكثر فعالية لإنشاء معدّلات مخصّصة.
تنفيذ مُعدِّل مخصّص باستخدام 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
، ما يسمح له بالتجاوز عن طريقة الرسم.
في ما يلي الأنواع المتاحة:
العقدة |
الاستخدام |
رابط نموذجي |
|
||
|
||
يتيح تطبيق هذه الواجهة لتطبيق |
||
|
||
|
||
|
||
|
||
|
||
يمكن أن يوفّر |
||
يمكن أن يكون ذلك مفيدًا لإنشاء عمليات تنفيذ متعددة للعقد في عملية واحدة. |
||
تسمح فئات |
يتم إلغاء صلاحية العقد تلقائيًا عند طلب تعديل العنصر المرتبط بها. بما أنّ مثالنا هو 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
الطرق التالية:
create
: هذه هي الوظيفة التي تنشئ مثيلًا لعقدة المُعدِّل. يتمّ استدعاء هذا الإجراء لإنشاء العقدة عند تطبيق المُعدِّل لأول مرة. ويؤدي ذلك عادةً إلى إنشاء العقدة وضبطها باستخدام المَعلمات التي تم تمريرها إلى مصنع المُعدِّلات.update
: يتمّ استدعاء هذه الدالة عند تقديم هذا المُعدِّل في المكان نفسه الذي تتوفّر فيه هذه العقدة، ولكن تمّ تغيير إحدى السمات. يتم تحديد ذلك بواسطة طريقةequals
للفئة. يتم إرسال عقدة المُعدِّل التي تم إنشاؤها سابقًا كمَعلمة إلى طلبupdate
. في هذه المرحلة، يجب تعديل سمات العقد لتتوافق مع المَعلمات المعدَّلة. إنّ إمكانية إعادة استخدام العقد بهذه الطريقة هي عامل رئيسي في تحسين الأداء الذي تحقّقهModifier.Node
. لذلك، عليك تعديل العقدة الحالية بدلاً من إنشاء عقدة جديدة في طريقةupdate
. في مثال الدائرة، تم تعديل لون العقدة.
بالإضافة إلى ذلك، يجب أن تتضمّن عمليات تنفيذ ModifierNodeElement
أيضًا تنفيذ equals
وhashCode
. لن يتمّ استدعاء update
إلا إذا كانت مقارنة التساوي مع
العنصر السابق تعرِض قيمة خاطئة.
يستخدم المثال أعلاه فئة بيانات لتحقيق ذلك. تُستخدَم هذه الطرق للتحقّق مما إذا كانت إحدى العقد بحاجة إلى التحديث أم لا. إذا كان العنصر يتضمّن سمات
لا تساهم في تحديد ما إذا كان يجب تعديل عقدة معيّنة أو لا، أو إذا كنت تريد تجنُّب استخدام فئات data
لأسباب تتعلّق بالتوافق مع الثنائيات، يمكنك تنفيذ 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
UI، وليس من مكان تخصيص المُعدِّل.
ومع ذلك، لا ترصد نُسخ عقدة المُعدِّل التغييرات في الحالة تلقائيًا. للتفاعل تلقائيًا مع تغيير محلي في التكوين، يمكنك قراءة قيمته الحالية داخل نطاق:
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
&IntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
يرصد هذا المثال قيمة 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
، وعدم إبطال القيمة للتنسيق.
ويمكن أن يؤدي ذلك إلى تحسين أداء المُعدِّل.
في ما يلي مثال افتراضي على ذلك باستخدام مُعدِّل يحتوي على دالة 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) } } }