Compose اصلاحکنندههای زیادی را برای رفتارهای رایج، از همان ابتدا ارائه میدهد، اما شما میتوانید اصلاحکنندههای سفارشی خودتان را نیز ایجاد کنید.
اصلاحکنندهها چندین بخش دارند:
- یک کارخانه اصلاح کننده
- این یک تابع افزونه (extension function) روی
Modifier
است که یک API اصطلاحی برای modifier شما فراهم میکند و امکان اتصال زنجیرهای modifierها به یکدیگر را فراهم میکند. modifier factory عناصر modifier مورد استفاده Compose برای تغییر رابط کاربری شما را تولید میکند.
- این یک تابع افزونه (extension function) روی
- یک عنصر اصلاحکننده
- اینجاست که میتوانید رفتار اصلاحکننده خود را پیادهسازی کنید.
بسته به عملکرد مورد نیاز، روشهای متعددی برای پیادهسازی یک اصلاحکننده سفارشی وجود دارد. اغلب، سادهترین راه برای پیادهسازی یک اصلاحکننده سفارشی، پیادهسازی یک کارخانه اصلاحکننده سفارشی است که سایر کارخانههای اصلاحکننده از قبل تعریفشده را ترکیب میکند. اگر به رفتار سفارشی بیشتری نیاز دارید، عنصر اصلاحکننده را با استفاده از APIهای 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)
با استفاده از یک کارخانه اصلاح کننده قابل ترکیب، یک اصلاح کننده سفارشی ایجاد کنید
شما همچنین میتوانید با استفاده از یک تابع composable یک اصلاحکننده سفارشی ایجاد کنید تا مقادیر را به یک اصلاحکننده موجود منتقل کند. این به عنوان یک کارخانه اصلاحکننده composable شناخته میشود.
استفاده از یک کارخانه اصلاحکننده قابل ترکیب برای ایجاد یک اصلاحکننده، به شما امکان میدهد از APIهای سطح بالاتر ترکیب، مانند animate*AsState
و سایر APIهای انیمیشن پشتیبانیشده با حالت ترکیب، استفاده کنید. برای مثال، قطعه کد زیر اصلاحکنندهای را نشان میدهد که هنگام فعال/غیرفعال کردن، تغییر آلفا را متحرک میکند:
@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
در محل فراخوانی کارخانه اصلاحکننده (modifier factory) حل میشوند.
هنگام ایجاد یک اصلاحکننده سفارشی با استفاده از یک کارخانه اصلاحکننده قابل ترکیب، توابع محلی ترکیب، مقدار را از درخت ترکیب که در آن ایجاد شدهاند میگیرند، نه اینکه استفاده شوند. این میتواند منجر به نتایج غیرمنتظرهای شود. به عنوان مثال، مثال اصلاحکننده محلی ترکیب که قبلاً ذکر شد را در نظر بگیرید که با استفاده از یک تابع قابل ترکیب، کمی متفاوت پیادهسازی شده است:
@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
یک API سطح پایینتر برای ایجاد اصلاحکنندهها در Compose است. این همان API است که Compose اصلاحکنندههای خود را در آن پیادهسازی میکند و کارآمدترین روش برای ایجاد اصلاحکنندههای سفارشی است.
پیادهسازی یک اصلاحکننده سفارشی با استفاده از Modifier.Node
سه بخش برای پیادهسازی یک اصلاحکننده سفارشی با استفاده از Modifier.Node وجود دارد:
- یک پیادهسازی
Modifier.Node
که منطق و وضعیت اصلاحکنندهی شما را در خود نگه میدارد. - یک
ModifierNodeElement
که نمونههای گره اصلاحکننده را ایجاد و بهروزرسانی میکند. - یک کارخانه اصلاحکننده اختیاری، همانطور که قبلاً توضیح داده شد.
کلاسهای ModifierNodeElement
بدون وضعیت (stateless) هستند و در هر تغییر ترکیب، نمونههای جدیدی به آنها اختصاص داده میشود، در حالی که کلاسهای Modifier.Node
میتوانند دارای وضعیت (stateful) باشند و در چندین تغییر ترکیب باقی بمانند و حتی میتوانند دوباره استفاده شوند.
بخش زیر هر بخش را شرح میدهد و مثالی از ساخت یک اصلاحکننده سفارشی برای رسم دایره را نشان میدهد.
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
ایجاد کنید. در مثال دایره ما، رنگ گره بهروزرسانی میشود.
علاوه بر این، پیادهسازیهای ModifierNodeElement
نیز باید equals
و hashCode
پیادهسازی کنند. update
فقط در صورتی فراخوانی میشود که مقایسه equals با عنصر قبلی، مقدار false را برگرداند.
مثال قبلی از یک کلاس داده برای دستیابی به این هدف استفاده میکند. این متدها برای بررسی اینکه آیا یک گره نیاز به بهروزرسانی دارد یا خیر، استفاده میشوند. اگر عنصر شما دارای ویژگیهایی است که در نیاز به بهروزرسانی یک گره نقشی ندارند، یا اگر میخواهید به دلایل سازگاری باینری از کلاسهای داده اجتناب کنید، میتوانید به صورت دستی equals
و hashCode
پیادهسازی کنید، به عنوان مثال، عنصر اصلاحکننده padding .
کارخانه اصلاح کننده
این سطح API عمومی اصلاحکننده شماست. اکثر پیادهسازیها عنصر اصلاحکننده را ایجاد کرده و آن را به زنجیره اصلاحکننده اضافه میکنند:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
مثال کامل
این سه بخش در کنار هم قرار میگیرند تا اصلاحکننده سفارشی برای رسم دایره با استفاده از APIهای 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
، در اینجا برخی از موقعیتهای رایجی که ممکن است با آنها مواجه شوید، آورده شده است.
پارامترهای صفر
اگر اصلاحکننده شما هیچ پارامتری نداشته باشد، هرگز نیازی به بهروزرسانی ندارد و علاوه بر این، نیازی به کلاس داده بودن هم ندارد. در ادامه، نمونهای از پیادهسازی یک اصلاحکننده که مقدار ثابتی از padding را به یک composable اعمال میکند، آورده شده است:
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
نسبت به اصلاحکنندههایی که فقط با یک کارخانه composable ایجاد میشوند این است که میتوانند مقدار ترکیب محلی را از جایی که اصلاحکننده در درخت رابط کاربری شما استفاده میشود، بخوانند، نه جایی که اصلاحکننده تخصیص داده شده است، با استفاده از currentValueOf
.
با این حال، نمونههای گره اصلاحکننده به طور خودکار تغییرات حالت را مشاهده نمیکنند. برای واکنش خودکار به تغییر محلی یک ترکیب، میتوانید مقدار فعلی آن را در داخل یک محدوده بخوانید:
-
DrawModifierNode
:ContentDrawScope
-
LayoutModifierNode
:MeasureScope
وIntrinsicMeasureScope
-
SemanticsModifierNode
:SemanticsPropertyReceiver
این مثال مقدار LocalContentColor
را برای رسم یک پسزمینه بر اساس رنگ آن مشاهده میکند. از آنجایی که ContentDrawScope
تغییرات snapshot را مشاهده میکند، با تغییر مقدار 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
دسترسی دارند. این امکان استفاده از APIهای 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
مربوط به آنها بهروزرسانی میشود، نامعتبر میشوند. برای اصلاحکنندههای پیچیده، ممکن است بخواهید این رفتار را غیرفعال کنید تا کنترل دقیقتری بر زمانی که اصلاحکننده شما فازها را نامعتبر میکند، داشته باشید.
این امر به ویژه در صورتی مفید است که اصلاحکننده سفارشی شما هم layout و هم draw را تغییر دهد. غیرفعال کردن اعتبارسنجی خودکار به شما این امکان را میدهد که وقتی فقط ویژگیهای مرتبط با draw، مانند color
، تغییر میکنند، draw را نامعتبر کنید. این کار از نامعتبر شدن layout جلوگیری میکند و میتواند عملکرد اصلاحکننده شما را بهبود بخشد.
یک مثال فرضی از این مورد در مثال زیر با یک اصلاحکننده که دارای ویژگیهای 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) } } }