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