Özel değiştiriciler oluşturma

Compose, genel davranışlar için kullanıma hazır birçok değiştirici sunar. Bununla birlikte, kendi özel değiştiricilerinizi de oluşturabilirsiniz.

Değiştiricilerin birden çok bölümü vardır:

  • Değiştirici fabrika
    • Bu, Modifier üzerinde bulunan ve değiştiriciniz için deyimsel bir API sağlayan ve değiştiricilerin kolayca birbirine bağlanmasını sağlayan bir uzantı işlevidir. Değiştirici fabrika, Compose tarafından kullanıcı arayüzünü değiştirmek için kullanılan değiştirici öğeleri üretir.
  • Değiştirici öğe
    • Değiştiricinizin davranışını burada uygulayabilirsiniz.

Gereken işlevlere bağlı olarak özel değiştirici uygulamanın birden çok yolu vardır. Genellikle, özel değiştiriciyi uygulamanın en kolay yolu, önceden tanımlanmış diğer değiştirici fabrikaları birleştiren bir özel değiştirici fabrikası uygulamaktır. Daha fazla özel davranışa ihtiyacınız varsa değiştirici öğeyi, daha düşük düzeyde olan ancak daha fazla esneklik sunan Modifier.Node API'lerini kullanarak uygulayın.

Mevcut değiştiricileri birbirine bağlayın

Özel değiştiriciler oluşturmak genellikle yalnızca mevcut değiştiricileri kullanarak mümkündür. Örneğin, Modifier.clip(), graphicsLayer değiştiricisi kullanılarak uygulanır. Bu stratejide mevcut değiştirici öğeler kullanılıyor ve siz kendi özel değiştirici fabrikanızı sunuyorsunuz.

Kendi özel değiştiricinizi uygulamadan önce, aynı stratejiyi kullanıp kullanamayacağınıza bakın.

fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)

Aynı değiştirici grubu sık sık tekrarlandığını fark ederseniz bunları kendi değiştiricinize alabilirsiniz:

fun Modifier.myBackground(color: Color) = padding(16.dp)
    .clip(RoundedCornerShape(8.dp))
    .background(color)

Fabrika ayarı kullanarak özel değiştirici oluşturun

Değerleri mevcut bir değiştiriciye aktarmak için composable işlev kullanarak da özel değiştirici oluşturabilirsiniz. Bu, composable değiştirici fabrikası olarak bilinir.

Değiştirici oluşturmak için composable değiştirici fabrikasının kullanılması, animate*AsState ve diğer Compose durumu destekli animasyon API'leri gibi daha üst düzey yazma API'lerinin kullanılmasına da olanak tanır. Örneğin, aşağıdaki snippet etkinleştirildiğinde/devre dışı bırakıldığında alfa değişimini canlandıran bir değiştirici gösterir:

@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 }
}

Özel değiştiriciniz, bir CompositionLocal öğesinden varsayılan değerleri sağlamak için kullanışlı bir yöntem sunuyorsa bunu uygulamanın en kolay yolu bir composable değiştirici fabrikası kullanmaktır:

@Composable
fun Modifier.fadedBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

Bu yaklaşımda, aşağıda ayrıntıları verilen bazı uyarılar bulunmaktadır.

CompositionLocal değerleri, değiştirici fabrikanın çağrı sitesinde çözümlenir

Kompozit değiştirici fabrikası kullanarak özel değiştirici oluştururken beste yerelleri, değeri kullanılmadığında, oluşturuldukları bileşim ağacından alırlar. Bu durum beklenmedik sonuçlara yol açabilir. Örneğin, yukarıdaki bileşim yerel değiştirici örneğini ele alalım. Bu örnek, composable bir işlev kullanılarak biraz farklı şekilde uygulanmaktadır:

@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)
        }
    }
}

Değiştiricinizin bu şekilde çalışmasını beklemiyorsanız bunun yerine özel bir Modifier.Node kullanın. Çünkü beste yerelleri, kullanım alanında doğru şekilde çözümlenir ve güvenli bir şekilde kaldırılabilir.

Özelleştirilebilir işlev değiştiricileri hiçbir zaman atlanmaz

Döndürülen değerleri olan composable işlevler atlanamayacağından, oluşturulabilir fabrika değiştiricileri hiçbir zaman atlanmaz. Bu, değiştirici işlevinizin her yeniden oluşturmada çağrılacağı anlamına gelir. Bu, sık sık yeniden düzenleme yapılması durumunda pahalı olabilir.

Özelleştirilebilir işlev değiştiricileri, composable işlev içinde çağrılmalıdır

Tüm composable işlevler gibi bir composable fabrika değiştiricisi de kompozisyondan çağrılmalıdır. Bu, hiçbir zaman bileşimden çıkarılamayacağından düzenleyicinin kaldırılabileceği yeri sınırlar. Buna karşılık, daha kolay yeniden kullanım ve performansı artırmak için composable işlevlerden çıkarılabilir.

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 kullanarak özel değiştirici davranışı uygulayın

Modifier.Node, Compose'da değiştirici oluşturmak için kullanılan alt düzey bir API'dir. Bu, Compose'un kendi değiştiricilerini uyguladığı API'dir ve özel değiştiriciler oluşturmanın en etkili yoludur.

Modifier.Node kullanarak özel değiştirici uygulayın

Değiştirici.Düğüm'ü kullanarak özel değiştirici uygulamanın üç bölümü vardır:

  • Değiştiricinizin mantığını ve durumunu barındıran bir Modifier.Node uygulaması.
  • Değiştirici düğüm örnekleri oluşturan ve güncelleyen bir ModifierNodeElement.
  • Yukarıda açıklandığı şekilde isteğe bağlı bir değiştirici fabrikası.

ModifierNodeElement sınıfları durum bilgisizdir ve her yeniden oluşturma için yeni örnekler ayrılır. Modifier.Node sınıfları ise durum bilgili olabilir, birden fazla yeniden oluşturmada dayanabilir ve hatta yeniden kullanılabilir.

Aşağıdaki bölümde her bir bölüm açıklanmakta ve daire çizmek için özel değiştirici oluşturma örneği gösterilmektedir.

Modifier.Node

Modifier.Node uygulaması (bu örnekte CircleNode), özel değiştiricinizin işlevini uygular.

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

Bu örnekte, değiştirici işlevine iletilen rengi içeren daireyi çiziyor.

Bir düğüm, Modifier.Node özelliğinin yanı sıra sıfır veya daha fazla düğüm türünü de uygular. Değiştiricinizin gerektirdiği işlevlere bağlı olarak farklı düğüm türleri bulunur. Yukarıdaki örneğin çizim yapabilmesi gerekir. Bu nedenle, çizim yöntemini geçersiz kılmasına olanak tanıyan DrawModifierNode öğesini uygular.

Kullanılabilir türler şunlardır:

Düğüm

Kullanım

Örnek Bağlantı

LayoutModifierNode

Sarmalanmış içeriğin ölçülme ve gösterilme şeklini değiştiren bir Modifier.Node.

Örnek

DrawModifierNode

Düzenin alanını kullanan bir Modifier.Node.

Örnek

CompositionLocalConsumerModifierNode

Bu arayüzün uygulanması, Modifier.Node cihazınızın kompozisyon yerellerini okumasına olanak tanır.

Örnek

SemanticsModifierNode

Test, erişilebilirlik ve benzer kullanım alanlarında kullanım için anlamsal anahtar/değer çifti ekleyen Modifier.Node.

Örnek

PointerInputModifierNode

PointerInputChanges alan bir Modifier.Node.

Örnek

ParentDataModifierNode

Üst düzene veri sağlayan bir Modifier.Node.

Örnek

LayoutAwareModifierNode

onMeasured ve onPlaced geri çağırma alan bir Modifier.Node.

Örnek

GlobalPositionAwareModifierNode

İçeriğin genel konumu değişmiş olduğunda düzenin son LayoutCoordinates ile birlikte onGloballyPositioned geri çağırması alan Modifier.Node.

Örnek

ObserverModifierNode

ObserverNode uygulayan Modifier.Node'lar, observeReads blokunda okunan anlık görüntü nesnelerinde yapılan değişikliklere yanıt olarak çağrılacak kendi onObservedReadsChanged uygulamasını sağlayabilir.

Örnek

DelegatingNode

Diğer Modifier.Node örneklerine iş yetkisi verebilen bir Modifier.Node.

Bu, birden fazla düğüm uygulamasının tek bir uygulamada oluşturulması yararlı olabilir.

Örnek

TraversableNode

Modifier.Node sınıflarının, aynı tür sınıflar veya belirli bir anahtar için düğüm ağacında yukarı/aşağı hareket etmesine izin verir.

Örnek

Karşılık gelen öğelerinde güncelleme çağrıldığında düğümler otomatik olarak geçersiz kılınır. Örneğimiz DrawModifierNode olduğundan, öğe üzerinde her güncelleme çağrıldığında düğüm bir yeniden çizimi tetikler ve rengi doğru şekilde güncellenir. Otomatik geçersiz kılmayı aşağıda açıklandığı şekilde devre dışı bırakabilirsiniz.

ModifierNodeElement

ModifierNodeElement, özel değiştiricinizi oluşturmak veya güncellemek için verileri tutan sabit bir sınıftır:

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

ModifierNodeElement uygulamalarının aşağıdaki yöntemleri geçersiz kılması gerekir:

  1. create: Bu, değiştirici düğümünüzü örneklendiren işlevdir. Bu işlem, değiştiriciniz ilk uygulandığında düğümü oluşturmak için çağrılır. Genellikle bu, düğümü ve değiştirici fabrikasına aktarılan parametrelerle yapılandırmakla ilgilidir.
  2. update: Bu işlev, bu düğümün zaten bulunduğu aynı noktada bu değiştirici sağlandığında ancak bir özellik değiştiğinde çağrılır. Bu değer, sınıfın equals yöntemi tarafından belirlenir. Daha önce oluşturulan değiştirici düğüm, update çağrısına parametre olarak gönderilir. Bu noktada, düğümlerin özelliklerini güncellenmiş parametrelere uygun olacak şekilde güncellemeniz gerekir. Düğümlerin bu şekilde yeniden kullanılabilmesi, Modifier.Node'nin sağladığı performans kazanımları için çok önemlidir. Bu nedenle, update yönteminde yeni bir düğüm oluşturmak yerine mevcut düğümü güncellemeniz gerekir. Daire örneğimizde, düğümün rengi güncellenir.

Ayrıca, ModifierNodeElement uygulamalarının equals ve hashCode uygulamalarını da uygulaması gerekir. update, yalnızca önceki öğeyle eşittir karşılaştırması "false" döndürürse çağrılır.

Yukarıdaki örnekte, bunu başarmak için bir veri sınıfı kullanılmaktadır. Bu yöntemler, bir düğümün güncellenmesi gerekip gerekmediğini kontrol etmek için kullanılır. Öğeniz, bir düğümün güncellenmesine ihtiyaç duyulmasına katkıda bulunmayan özellikler içeriyorsa veya ikili program uyumluluğu nedeniyle veri sınıflarından kaçınmak istiyorsanız equals ve hashCode öğelerini (ör. dolgu değiştirici öğesini) manuel olarak uygulayabilirsiniz.

Değiştirici fabrikası

Bu, değiştiricinizin herkese açık API yüzeyidir. Çoğu uygulama, değiştirici öğeyi oluşturur ve değiştirici zincirine ekler:

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

Tam örnek

Bu üç parça, Modifier.Node API'lerini kullanarak bir daire çizmek için özel değiştiriciyi oluşturmak üzere bir araya getirilir:

// 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 kullanımıyla ilgili yaygın durumlar

Modifier.Node ile özel değiştiriciler oluştururken karşılaşabileceğiniz bazı yaygın durumları aşağıda bulabilirsiniz.

Sıfır parametre

Değiştiricinizde parametre yoksa hiçbir zaman güncellenmesi gerekmez. Ayrıca, veri sınıfı olması da gerekmez. Aşağıda, bir composable'a sabit miktarda dolgu uygulayan bir değiştiricinin örnek uygulamasını görebilirsiniz:

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)
        }
    }
}

Kompozisyon yerellerine referans verme

Modifier.Node değiştiricileri, CompositionLocal gibi Compose durum nesnelerindeki değişiklikleri otomatik olarak gözlemlemez. Modifier.Node değiştiricilerinin, yalnızca composable fabrika ile oluşturulan değiştiricilere kıyasla avantajı, değiştiricinin ayrıldığı yere değil, kullanıcı arayüzü ağacınızda kullanıldığı yerdeki bestenin yerel değerini currentValueOf kullanarak okuyabilmeleridir.

Bununla birlikte, değiştirici düğüm örnekleri durum değişikliklerini otomatik olarak gözlemlemez. Bir bestenin yerel değişimine otomatik olarak tepki vermek için bir kapsamdaki mevcut değerini okuyabilirsiniz:

Bu örnekte, rengine göre arka plan çizmek için LocalContentColor değeri gösterilmektedir. ContentDrawScope, anlık görüntü değişikliklerini gözlemlediğinden, LocalContentColor değeri değiştiğinde bu işlem otomatik olarak yeniden çizim yapar:

class BackgroundColorConsumerNode :
    Modifier.Node(),
    DrawModifierNode,
    CompositionLocalConsumerModifierNode {
    override fun ContentDrawScope.draw() {
        val currentColor = currentValueOf(LocalContentColor)
        drawRect(color = currentColor)
        drawContent()
    }
}

Kapsam dışındaki durum değişikliklerine tepki vermek ve değiştiricinizi otomatik olarak güncellemek için ObserverModifierNode kullanın.

Örneğin Modifier.scrollable, LocalDensity hizmetindeki değişiklikleri gözlemlemek için bu tekniği kullanır. Aşağıda basitleştirilmiş bir örnek gösterilmektedir:

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)
    }
}

Animasyon değiştirici

Modifier.Node uygulamalarının coroutineScope öğesine erişimi var. Bu, Compose Animatable API'lerinin kullanılmasına olanak tanır. Örneğin, bu snippet yukarıdan CircleNode değerini üste koyulacak ve sürekli olarak artacak ve artacak şekilde değiştirir:

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)
            ) {
            }
        }
    }
}

Yetkiyi kullanarak değiştiriciler arasında paylaşım durumu

Modifier.Node değiştiricileri başka düğümlere yetki verebilir. Bunun, farklı değiştiricilerdeki yaygın uygulamaları ayıklama gibi pek çok kullanım alanı vardır ancak aynı zamanda değiştiriciler arasında ortak bir durumu paylaşmak için de kullanılabilir.

Örneğin, etkileşim verilerini paylaşan tıklanabilir bir değiştirici düğümün temel bir uygulaması:

class ClickableNode : DelegatingNode() {
    val interactionData = InteractionData()
    val focusableNode = delegate(
        FocusableNode(interactionData)
    )
    val indicationNode = delegate(
        IndicationNode(interactionData)
    )
}

Düğümlerin otomatik olarak geçersiz kılınmasının kapsamı dışında kalma

Karşılık gelen ModifierNodeElement çağrıları güncellendiğinde Modifier.Node düğüm otomatik olarak geçersiz olur. Bazen daha karmaşık bir değiştiricide, değiştiricinizin aşamaları ne zaman geçersiz kıldığı konusunda daha ayrıntılı kontrol sahibi olmak için bu davranışı devre dışı bırakabilirsiniz.

Bu, özellikle özel değiştiriciniz hem düzeni hem de çizimi değiştiriyorsa yararlı olabilir. Otomatik geçersiz kılmayı devre dışı bırakmak, çizimi yalnızca color gibi çizimle ilgili özelliklerde (ör. değişiklik yapıldığında) geçersiz kılmanıza ve düzeni geçersiz kılmanıza olanak tanır. Bu, değiştiricinizin performansını artırabilir.

Aşağıda, color, size ve onClick lambda özelliklerine sahip bir değiştiriciyle bunun varsayımsal bir örneği gösterilmektedir. Bu değiştirici yalnızca gerekli olan öğeleri geçersiz kılar ve gerekli olmayan geçersiz kılmaları atlar:

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)
        }
    }
}