توفر ميزة 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 معدّلاتها الخاصة بها، وهي الطريقة
الأكثر فعالية لإنشاء معدّلات مخصّصة.
تنفيذ مُعدِّل مخصّص باستخدام 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
. في مثال الدائرة، تم تعديل لون العقدة.
بالإضافة إلى ذلك، يجب تنفيذ equals
وhashCode
أيضًا في عمليات تنفيذ ModifierNodeElement
. لن يتم استدعاء الدالة update
إلا إذا عرضت مقارنة يساوي مع العنصر السابق خطأ.
يستخدم المثال أعلاه فئة بيانات لتحقيق ذلك. تُستخدَم هذه الطرق للتحقّق مما إذا كانت إحدى العقد بحاجة إلى التحديث أم لا. إذا كان العنصر يحتوي على خصائص لا تساهم في تحديد ما إذا كانت العقدة بحاجة إلى تحديث أو كنت تريد تجنُّب فئات البيانات لأسباب تتعلق بالتوافق الثنائي، يمكنك تنفيذ 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
، وعدم إبطال القيمة للتنسيق.
ويمكن أن يؤدي ذلك إلى تحسين أداء المُعدِّل.
نعرض أدناه مثالاً افتراضيًا على ذلك مع مُعدِّل يتضمن دالة واحدة (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) } } }