ساختن اصلاح‌کننده‌های سفارشی

Compose اصلاح‌کننده‌های زیادی را برای رفتارهای رایج به‌صورت آماده ارائه می‌دهد، اما می‌توانید اصلاح‌کننده‌های سفارشی خودتان را نیز ایجاد کنید.

اصلاح‌گرها چند بخش دارند:

  • کارخانه اصلاح‌گر
    • این یک تابع افزونه در Modifier است که یک API اصطلاحی برای اصلاح‌کننده شما ارائه می‌دهد و اجازه می‌دهد اصلاح‌کننده‌ها به یکدیگر زنجیر شوند. کارخانه اصلاح‌کننده عناصر اصلاح‌کننده‌ای را تولید می‌کند که 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 در سایت فراخوانی کارخانه اصلاح‌کننده حل می‌شوند

When creating a custom modifier using a composable modifier factory, composition locals take the value from the composition tree where they are created, not used. این کار می‌تواند به نتایج غیرمنتظره منجر شود. برای مثال، نمونه اصلاح‌کننده محلی ترکیب‌بندی را که قبلاً ذکر شد درنظر بگیرید که بااستفاده از تابع ترکیب‌پذیر کمی متفاوت پیاده‌سازی شده است:

@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

استفاده

پیوند نمونه

LayoutModifierNode

‫Modifier.Node که نحوه اندازه‌گیری و چیدمان محتوای پیچیده‌شده خود را تغییر می‌دهد.

نمونه

DrawModifierNode

‫Modifier.Node که در فضای چیدمان ترسیم می‌شود.

نمونه

CompositionLocalConsumerModifierNode

با پیاده‌سازی این میانجی، Modifier.Node شما می‌تواند ترکیب‌بندی‌های محلی را بخواند.

نمونه

SemanticsModifierNode

Modifier.Node که جفت کلید/مقدار معنایی را برای استفاده در آزمایش، دسترس‌پذیری، و موارد استفاده مشابه اضافه می‌کند.

نمونه

PointerInputModifierNode

‫Modifier.Node که PointerInputChanges را دریافت می‌کند.

نمونه

ParentDataModifierNode

Modifier.Node که داده‌ها را به چیدمان ولی ارائه می‌دهد.

نمونه

LayoutAwareModifierNode

‫Modifier.Node که تماس‌های برگشتی onMeasured و onPlaced را دریافت می‌کند.

نمونه

GlobalPositionAwareModifierNode

Modifier.Node که وقتی موقعیت سراسری محتوا تغییر می‌کند، با LayoutCoordinates نهایی طرح‌بندی، یک فراخوانی برگشتی onGloballyPositioned دریافت می‌کند.

نمونه

ObserverModifierNode

‫Modifier.Nodeهایی که ObserverNode را پیاده‌سازی می‌کنند می‌توانند پیاده‌سازی خودشان را از onObservedReadsChanged ارائه دهند که در پاسخ به تغییرات در اشیای عکس آنی خوانده‌شده در بلوک observeReads فراخوانی خواهد شد.

نمونه

DelegatingNode

Modifier.Node که می‌تواند کار را به نمونه‌های دیگر Modifier.Node واگذار کند.

این می‌تواند برای ترکیب کردن چندین پیاده‌سازی گره در یک گره مفید باشد.

نمونه

TraversableNode

به کلاس‌های Modifier.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 باید روش‌های زیر را لغو کنند:

  1. ‫create: این تابعی است که گره اصلاح‌کننده شما را نمونه‌سازی می‌کند. این برای ایجاد گره زمانی که اصلاح‌کننده شما برای اولین‌بار اعمال می‌شود فراخوانی می‌شود. معمولاً، این کار شامل ساختن گره و پیکربندی آن با پارامترهایی است که به کارخانه اصلاح‌کننده ارسال شده‌اند.
  2. update: این تابع هروقت این اصلاح‌کننده در همان نقطه‌ای که این گره ازقبل وجود دارد ارائه شود، اما ویژگی‌ای تغییر کرده باشد، فراخوانده می‌شود. این مورد با روش equals کلاس تعیین می‌شود. گره اصلاح‌کننده‌ای که قبلاً ایجاد شده است به‌عنوان پارامتر به فراخوانی update ارسال می‌شود. در این مرحله، باید ویژگی‌های گره‌ها را به‌روز کنید تا با پارامترهای به‌روزشده مطابقت داشته باشند. امکان استفاده مجدد از گره‌ها به این روش، کلید بهبود عملکردی است که Modifier.Node به ارمغان می‌آورد؛ بنابراین، باید گره موجود را به‌روز کنید، نه اینکه گره جدیدی در روش update ایجاد کنید. در مثال دایره ما، رنگ گره به‌روز می‌شود.

علاوه‌براین، پیاده‌سازی‌های ModifierNodeElement باید equals و hashCode را نیز پیاده‌سازی کنند. update will only get called if an equals comparison with the previous element returns false.

مثال قبلی از کلاس داده برای دستیابی به این هدف استفاده می‌کند. از این روش‌ها برای بررسی اینکه آیا گره نیاز به به‌روزرسانی دارد یا نه استفاده می‌شود. اگر عنصر شما ویژگی‌هایی دارد که در به‌روزرسانی شدن یا نشدن گره نقشی ندارند، یا می‌خواهید به‌دلایل سازگاری دودویی از کلاس‌های داده استفاده نکنید، می‌توانید به‌صورت دستی equals و hashCode را پیاده‌سازی کنید، برای مثال، عنصر اصلاح‌کننده حاشیه داخلی.

کارخانه اصلاح‌گر

این سطح API عمومی اصلاح‌کننده شما است. بیشتر پیاده‌سازی‌ها عنصر اصلاح‌کننده را ایجاد می‌کنند و آن را به زنجیره اصلاح‌کننده اضافه می‌کنند:

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

نمونه کامل

این سه بخش باهم ترکیب می‌شوند تا اصلاح‌کننده سفارشی را برای کشیدن دایره بااستفاده از Modifier.Node API ایجاد کنند:

// 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، در اینجا برخی‌از موقعیت‌های رایجی که ممکن است با آن‌ها مواجه شوید آورده شده است.

پارامترهای صفر

اگر اصلاح‌کننده شما پارامتری نداشته باشد، هرگز نیازی به به‌روزرسانی ندارد و، علاوه‌براین، نیازی نیست کلاس داده باشد. در زیر نمونه‌ای از پیاده‌سازی اصلاح‌کننده‌ای آورده شده است که مقدار ثابتی از فاصله‌گذاری را به یک 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 نسبت به اصلاح‌کننده‌هایی که فقط با کارخانه قابل‌ترکیب ایجاد می‌شوند این است که می‌توانند مقدار ترکیب‌بندی محلی را از جایی که اصلاح‌کننده در درخت واسط کاربر شما استفاده می‌شود، نه جایی که اصلاح‌کننده اختصاص داده شده است، بااستفاده از currentValueOf بخوانند.

اما نمونه‌های گره اصلاح‌کننده به‌طور خودکار تغییرات وضعیت را مشاهده نمی‌کنند. برای واکنش خودکار به تغییر CompositionLocal، می‌توانید مقدار فعلی آن را در یک محدوده بخوانید:

این مثال مقدار LocalContentColor را برای ترسیم پس‌زمینه براساس رنگ آن مشاهده می‌کند. As ContentDrawScope does observe snapshot changes, this automatically redraws when the value of LocalContentColor changes:

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 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، تغییر می‌کنند، ترسیم را باطل کنید. این کار از نامعتبر شدن چیدمان جلوگیری می‌کند و می‌تواند عملکرد اصلاح‌کننده را بهبود بخشد.

نمونه فرضی این مورد در مثال زیر با اصلاح‌کننده‌ای نشان داده شده است که دارای ویژگی‌های لامبدا 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)
        }
    }
}