توفر 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، ما يسمح له بتجاوز طريقة الرسم.
في ما يلي الأنواع المتاحة:
العقدة |
الاستخدام |
رابط المثال |
|
||
|
||
يسمح تنفيذ هذه الواجهة لـ |
||
|
||
|
||
|
||
|
||
|
||
يمكن أن توفّر |
||
يمكن أن يكون ذلك مفيدًا لدمج عمليات تنفيذ عُقد متعددة في عملية تنفيذ واحدة. |
||
يسمح لفئات |
يتم إبطال العُقد تلقائيًا عند استدعاء `update` على العنصر المقابل. بما أنّ مثالنا هو 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 إلى تجاوز الطرق التالية:
create: هذه هي الدالة التي تنشئ عُقدة المعدِّل. يتم استدعاء هذه الدالة لإنشاء العُقدة عند تطبيق المعدِّل لأول مرة. عادةً ما يكون ذلك عبارة عن إنشاء العُقدة وإعدادها باستخدام المَعلمات التي تم تمريرها إلى مصنع المعدِّلات.update: يتم استدعاء هذه الدالة كلما تم توفير هذا المعدِّل في الموضع نفسه الذي توجد فيه هذه العُقدة حاليًا، ولكن تم تغيير إحدى الخصائص. يتم تحديد ذلك من خلال طريقةequalsللفئة. يتم إرسال عُقدة المعدِّل التي تم إنشاؤها سابقًا كمَعلمة إلى استدعاءupdate. في هذه المرحلة، يجب تعديل خصائص العُقد لتتطابق مع المَعلمات المعدَّلة. إنّ إمكانية إعادة استخدام العُقد بهذه الطريقة هي مفتاح التحسينات في الأداء التي يوفّرهاModifier.Node، لذا يجب تعديل العُقدة الحالية بدلاً من إنشاء عُقدة جديدة في طريقةupdate. في مثال الدائرة، يتم تعديل لون العُقدة.
بالإضافة إلى ذلك، تحتاج عمليات تنفيذ ModifierNodeElement أيضًا إلى تنفيذ equals وhashCode. لن يتم استدعاء update إلا إذا كانت نتيجة مقارنة `equals` مع العنصر السابق هي "خطأ".
يستخدم المثال السابق فئة بيانات لتحقيق ذلك. تُستخدم هاتان الطريقتان للتحقّق مما إذا كانت العُقدة بحاجة إلى التعديل أم لا. إذا كان العنصر يتضمّن خصائص لا تساهم في تحديد ما إذا كان يجب تعديل العُقدة أم لا، أو إذا كنت تريد تجنُّب فئات البيانات لأسباب تتعلق بالتوافق الثنائي، يمكنك تنفيذ 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 تلقائيًا التغييرات التي تطرأ على عناصر حالة Compose، مثل CompositionLocal. تتمثّل ميزة معدِّلات Modifier.Node مقارنةً بالمعدِّلات التي يتم إنشاؤها باستخدام مصنع الدوال المركّبة في أنّه يمكنها قراءة قيمة الموقع المحلي للتركيب من المكان الذي يتم فيه استخدام المعدِّل في شجرة واجهة المستخدم، وليس من المكان الذي يتم فيه تخصيص المعدِّل، وذلك باستخدام currentValueOf.
ومع ذلك، لا تراقب نُسخ عُقد المعدِّلات تلقائيًا التغييرات في الحالة. للتفاعل تلقائيًا مع تغيير الموقع المحلي للتركيب، يمكنك قراءة قيمته الحالية ضِمن نطاق:
DrawModifierNode:ContentDrawScopeLayoutModifierNode:MeasureScope&IntrinsicMeasureScopeSemanticsModifierNode: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. يسمح ذلك
باستخدام واجهات برمجة التطبيقات `Animatable` في Compose. على سبيل المثال، يعدّل هذا المقتطف 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 المقابلة `update`. بالنسبة إلى المعدِّلات المعقّدة، قد تحتاج إلى إيقاف هذا السلوك للتحكّم بشكل أكثر دقة في وقت إبطال المعدِّل للمراحل.
يكون ذلك مفيدًا بشكل خاص إذا كان المعدِّل المخصّص يعدّل كلاً من التنسيق والرسم. يسمح لك إيقاف الإبطال التلقائي بإبطال الرسم فقط عندما تتغيّر الخصائص المتعلّقة بالرسم فقط، مثل color. يؤدي ذلك إلى تجنُّب إبطال التنسيق ويمكن أن يحسّن أداء المعدِّل.
يعرض المثال التالي مثالاً افتراضيًا على ذلك باستخدام معدِّل يتضمّن 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) } } }