توفّر أداة 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 |
الاستخدام |
رابط نموذج |
|
||
|
||
يتيح تنفيذ هذه الواجهة لـ " |
||
|
||
|
||
|
||
|
||
|
||
يمكن لـ |
||
يمكن أن يكون ذلك مفيدًا لإنشاء عمليات استخدام متعددة للعُقد في عملية واحدة. |
||
تسمح لفئات |
يتم إبطال العُقد تلقائيًا عند استدعاء التعديل على العنصر المقابل لها. بما أنّ المثال الذي نستخدمه هو 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. At this point, you should update the nodes' properties to correspond with the updated parameters. إنّ إمكانية إعادة استخدام العُقد بهذه الطريقة هي أساس تحسينات الأداء التي يوفّرهاModifier.Node، لذا عليك تعديل العقدة الحالية بدلاً من إنشاء عقدة جديدة في طريقةupdate. في مثال الدائرة، يتم تعديل لون العقدة.
بالإضافة إلى ذلك، يجب أن تنفّذ عمليات تنفيذ ModifierNodeElement أيضًا
equals وhashCode. يتم استدعاء update فقط إذا كانت مقارنة المساواة
مع العنصر السابق تعرض القيمة "خطأ".
يستخدم المثال السابق فئة بيانات لتحقيق ذلك. تُستخدم هذه الطرق للتحقق مما إذا كانت
العقدة بحاجة إلى تحديث أم لا. إذا كان العنصر يحتوي على خصائص لا تساهم في تحديد ما إذا كانت العقدة بحاجة إلى التحديث، أو إذا كنت تريد تجنُّب فئات البيانات لأسباب تتعلق بالتوافق الثنائي، يمكنك تنفيذ 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.
ومع ذلك، لا ترصد مثيلات عُقد المُعدِّل تغييرات الحالة تلقائيًا. للتفاعل تلقائيًا مع تغيير محلي في التركيب، يمكنك قراءة قيمته الحالية داخل نطاق:
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. يتيح ذلك استخدام واجهات برمجة التطبيقات Compose Animatable API. على سبيل المثال، يعدّل هذا المقتطف
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) } } }