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