اصلاح کننده های سفارشی ایجاد کنید

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

اصلاح کننده ها چندین بخش دارند:

  • یک کارخانه اصلاح کننده
    • این یک تابع افزونه در Modifier است که یک API اصطلاحی برای اصلاح کننده شما ارائه می دهد و به اصلاح کننده ها اجازه می دهد به راحتی به هم زنجیر شوند. کارخانه اصلاح‌کننده عناصر اصلاح‌کننده‌ای را که توسط Compose برای اصلاح رابط کاربری شما استفاده می‌شود، تولید می‌کند.
  • یک عنصر اصلاح کننده
    • اینجاست که می توانید رفتار اصلاح کننده خود را پیاده سازی کنید.

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

استفاده از کارخانه اصلاح‌کننده قابل ترکیب برای ایجاد یک اصلاح‌کننده همچنین امکان استفاده از APIهای آهنگسازی سطح بالاتر، مانند animate*AsState و سایر APIهای انیمیشن با پشتیبانی از حالت 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 یک API سطح پایین تر برای ایجاد اصلاح کننده در Compose است. این همان API است که 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 پیاده‌سازی می‌کند، که به آن اجازه می‌دهد روش ترسیم را لغو کند.

انواع موجود به شرح زیر است:

گره

استفاده

لینک نمونه

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 که زمانی که ممکن است موقعیت کلی محتوا تغییر کرده باشد، یک تماس پاسخ به onGloballyPositioned با LayoutCoordinates نهایی طرح دریافت می کند.

نمونه

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

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

این مثال مقدار 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 API استفاده کنید. به عنوان مثال، این قطعه 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)
        }
    }
}