Özel değiştiriciler oluşturma

Oluşturma, yaygın davranışlar için kullanıma hazır birçok değişken sağlar ancak kendi özel değiştiricilerinizi de oluşturabilirsiniz.

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

  • Değiştirici fabrikası
    • Bu, Modifier üzerinde bir uzantı işlevidir. Değiştiriciniz için idiomatik bir API sağlar ve değiştiricilerin kolayca birbirine bağlanmasına olanak tanır. Değiştirici fabrikası, kullanıcı arayüzünüzü değiştirmek için Compose tarafından kullanılan değiştirici öğelerini üretir.
  • Değiştirici öğe
    • Burada, değiştiricinizin davranışını uygulayabilirsiniz.

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

Mevcut değiştiricileri birbirine bağlama

Genellikle, yalnızca mevcut değiştiricileri kullanarak özel değiştiriciler oluşturabilirsiniz. Örneğin, Modifier.clip(), graphicsLayer değiştiricisi kullanılarak uygulanır. Bu stratejide mevcut değiştirici öğeleri kullanılır ve kendi özel değiştirici fabrikanızı sağlarsınız.

Kendi özel değiştiricinizi uygulamadan önce aynı stratejiyi kullanıp kullanamayacağınızı kontrol edin.

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

Aynı değiştirici grubunu sık sık tekrarladığınızı fark ederseniz bunları kendi değiştiricinize dahil edebilirsiniz:

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

Bir birleştirilebilir değiştirici fabrikası kullanarak özel değiştirici oluşturma

Mevcut bir değiştiriciye değer aktarmak için birleştirilebilir bir işlev kullanarak özel değiştirici de oluşturabilirsiniz. Buna birleştirilebilir değiştirici fabrikası denir.

Bir değiştirici oluşturmak için birleştirilebilir değiştirici fabrikası kullanmak, animate*AsState ve diğer Compose durumu destekli animasyon API'leri gibi daha üst düzey Compose API'lerinin kullanılmasına da olanak tanır. Örneğin, aşağıdaki snippet'te etkinleştirildiğinde/devre dışı bırakıldığında alfa değişikliğini animasyonlu olarak gösteren bir değiştirici gösterilmektedir:

@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, CompositionLocal öğesinden varsayılan değerler sağlamak için kullanılan bir kolaylık yöntemiyse bunu uygulamanın en kolay yolu, birleştirilebilir bir 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şımın aşağıda ayrıntılı olarak açıklanan bazı sakıncaları vardır.

CompositionLocal değerleri, değiştirici fabrikasının çağrı yerinde çözülür

Bir birleştirilebilir değiştirici fabrikası kullanılarak özel değiştirici oluşturulurken, yerel beste değerleri, kullanıldıkları kompozisyon ağacından değil, oluşturuldukları kompozisyon ağacından alınır. Bu durum beklenmedik sonuçlara yol açabilir. Örneğin, yukarıdaki kompozisyon yerel değiştirici örneğini ele alalım. Bu örnek, birleştirilebilir bir işlev kullanılarak biraz farklı şekilde uygulanmıştı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 yerel derlemeler kullanım sitesinde doğru şekilde çözüleceği ve güvenli bir şekilde kaldırılabileceği için bunun yerine özel bir Modifier.Node kullanın.

Kompozit işlev değiştiriciler hiçbir zaman atlanmaz

Döndürülen değeri olan birleştirilebilir işlevler atlanamayabileceğinden, birleştirilebilir fabrika değiştiricileri hiçbir zaman atlanmaz. Bu, değiştirici işlevinizin her yeniden oluşturma işleminde çağrılacağı anlamına gelir. Bu, sık sık yeniden oluşturma işlemi yapılması durumunda pahalı olabilir.

Composable işlev değiştiriciler, composable işlev içinde çağrılmalıdır

Tüm birleştirilebilir işlevler gibi, birleştirilebilir fabrika değiştirici de kompozisyon içinden çağrılmalıdır. Bu, değiştiricinin hiçbir zaman kompozisyonun dışına çıkarılamaması nedeniyle değiştiricinin kaldırılabileceği yeri sınırlar. Buna karşılık, birleştirilebilir olmayan değiştirici fabrikaları, daha kolay yeniden kullanıma izin vermek ve performansı artırmak için birleştirilebilir işlevlerden kaldırı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ışı uygulama

Modifier.Node, Oluştur'da değiştirici oluşturmak için kullanılan daha düşük düzeyli bir API'dir. Bu API, Compose'un kendi değiştiricilerini uyguladığı API'dir ve özel değiştirici oluşturmanın en performanslı yoludur.

Modifier.Node kullanarak özel değiştirici uygulama

Modifier.Node kullanarak özel bir 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 örneklerini oluşturan ve güncelleyen bir ModifierNodeElement.
  • Yukarıda ayrıntılı olarak açıklanan isteğe bağlı bir değiştirici fabrikası.

ModifierNodeElement sınıfları durum bilgisizdir ve her yeniden derleme işleminde yeni örnekler ayrılır. Modifier.Node sınıfları ise durum bilgisine sahip olabilir, birden fazla yeniden derleme işleminde hayatta kalır ve hatta yeniden kullanılabilir.

Aşağıdaki bölümde her bir bölüm açıklanmakta ve daire çizmek için özel bir 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 renkle daireyi çizer.

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

Kullanılabilir türler şunlardır:

Düğüm

Kullanım

Örnek Bağlantı

LayoutModifierNode

Sarmalanmış içeriğinin nasıl ölçüleceğini ve düzenleneceğini değiştiren bir Modifier.Node.

Örnek

DrawModifierNode

Sayfa düzeninin alanına çizilen bir Modifier.Node.

Örnek

CompositionLocalConsumerModifierNode

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

Örnek

SemanticsModifierNode

Test, erişilebilirlik ve benzer kullanım alanlarında kullanılmak üzere anlamsal anahtar/değer ekleyen bir Modifier.Node.

Örnek

PointerInputModifierNode

PointerInputChanges alıcı bir Modifier.Node.

Örnek

ParentDataModifierNode

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

Örnek

LayoutAwareModifierNode

onMeasured ve onPlaced geri çağırmalarının alındığı bir Modifier.Node.

Örnek

GlobalPositionAwareModifierNode

İçeriğin genel konumu değişmiş olabileceği durumlarda, düzenin son LayoutCoordinates değerini içeren bir onGloballyPositioned geri çağırma işlevi alan bir Modifier.Node.

Örnek

ObserverModifierNode

ObserverNode'u uygulayan Modifier.Node'ler, bir observeReads bloğunda okunan anlık görüntü nesnelerindeki değişikliklere yanıt olarak çağrılacak kendi onObservedReadsChanged uygulamalarını sağlayabilir.

Örnek

DelegatingNode

İşleri diğer Modifier.Node örneklerine delege edebilen bir Modifier.Node.

Bu, birden fazla düğüm uygulamasını tek bir uygulamada derlemek için yararlı olabilir.

Örnek

TraversableNode

Modifier.Node sınıflarının, aynı türdeki sınıflar veya belirli bir anahtar için düğüm ağacında yukarı/aşağı gezinmesine olanak tanır.

Örnek

İlgili öğelerinde update çağrısı yapıldığında düğümler otomatik olarak geçersiz kılınır. Örneğimiz bir DrawModifierNode olduğundan, öğede update çağrısı yapıldığında düğüm yeniden çizmeyi tetikler ve rengi doğru şekilde güncellenir. Otomatik geçersiz kılma özelliğini aşağıda açıklandığı şekilde devre dışı bırakabilirsiniz.

ModifierNodeElement

ModifierNodeElement, özel değiştiricinizi oluşturmak veya güncellemek için gerekli verileri barındıran değişmez 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ü örnekleyen işlevdir. Bu işlev, değiştiriciniz ilk kez uygulandığında düğümü oluşturmak için çağrılır. Bu genellikle düğümü oluşturmak ve değiştirici fabrikasına iletilen parametrelerle yapılandırmak anlamına gelir.
  2. update: Bu işlev, bu değiştiricinin düğümün zaten bulunduğu aynı yerde sağlandığı ancak bir özelliğin değiştiği her zaman çağrılır. Bu, 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üncellenen parametrelere uygun olacak şekilde güncellemeniz gerekir. Nodların bu şekilde yeniden kullanılabilmesi, Modifier.Node'ün sağladığı performans kazançlarının anahtarıdır. Bu nedenle, update yönteminde yeni bir tane 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'yi de uygulaması gerekir. update yalnızca önceki öğeyle yapılan eşitlik karşılaştırması yanlış döndürürse çağrılır.

Yukarıdaki örnekte, bunu yapmak için bir veri sınıfı kullanılmıştır. Bu yöntemler, bir düğümün güncellenmesi gerekip gerekmediğini kontrol etmek için kullanılır. Öğenizde, bir düğümün güncellenmesi gerekip gerekmediğine katkıda bulunmayan özellikler varsa veya ikili uyumluluk nedeniyle veri sınıflarından kaçınmak istiyorsanız equals ve hashCode'i (ör. dolgu değiştirici öğesi) manuel olarak uygulayabilirsiniz.

Değiştirici fabrikası

Bu, değiştiricinizin herkese açık API yüzeyidir. Çoğu uygulamada, değiştirici öğesi oluşturulur ve değiştirici zincirine eklenir:

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

Tam örnek

Bu üç parça bir araya gelerek Modifier.Node API'lerini kullanarak daire çizecek özel değiştiriciyi oluşturur:

// 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 kullanan yaygın durumlar

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

Sıfır parametre

Değiştiricinizin parametresi yoksa hiçbir zaman güncellenmesi gerekmez ve ayrıca veri sınıfı olması gerekmez. Bir bileşiğe sabit miktarda dolgu uygulayan bir değiştiricinin örnek uygulaması aşağıda verilmiştir:

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

Yerel kompozisyonlara referans verme

Modifier.Node değiştiriciler, CompositionLocal gibi Oluşturma durumu nesnelerindeki değişiklikleri otomatik olarak gözlemlemez. Modifier.Node değiştiricilerinin, yalnızca bir birleştirilebilir fabrikayla oluşturulan değiştiricilere kıyasla avantajı, currentValueOf kullanarak bileşimin değerini değiştiricinin ayrıldığı yerden değil, kullanıcı arayüzü ağacınızda değiştiricinin kullanıldığı yerden yerel olarak okuyabilmesidir.

Ancak değiştirici düğüm örnekleri, durum değişikliklerini otomatik olarak gözlemlemez. Bir bileşiğin yerel olarak değişmesine otomatik olarak tepki vermek için bileşiğin geçerli değerini bir kapsam içinde okuyabilirsiniz:

Bu örnekte, LocalContentColor değerine göre arka plan çizmek için renginin değeri gözlemlenir. ContentDrawScope, anlık görüntü değişikliklerini gözlemlediğinden LocalContentColor değeri değiştiğinde otomatik olarak yeniden çizilir:

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

Bir kapsamın 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'daki değişiklikleri gözlemlemek için bu tekniği kullanır. Aşağıda basitleştirilmiş bir örnek verilmiştir:

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'a erişimi var. Bu sayede Compose Animatable APIs'in kullanılmasına olanak tanınmaktadır. Örneğin, bu snippet yukarıdaki CircleNode öğesini tekrar tekrar görünür ve görünmez hale gelecek ş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)
            ) {
            }
        }
    }
}

Yetki verme özelliğini kullanarak değiştiriciler arasında durum paylaşımı

Modifier.Node değiştiriciler diğer düğümlere yetki verebilir. Bunun için farklı değiştiricilerde ortak uygulamaları ayıklamak gibi birçok kullanım alanı vardır ancak değiştiriciler arasında ortak durumu paylaşmak için de kullanılabilir.

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

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

Düğüm otomatik geçersiz kılma özelliğini devre dışı bırakma

Modifier.Node düğümleri, ilgili ModifierNodeElement çağrıları güncellendiğinde otomatik olarak geçersiz kılınır. Bazen, daha karmaşık bir değiştiricide, değiştiricinizin aşamaları ne zaman geçersiz kılacağı konusunda daha ayrıntılı kontrol sahibi olmak için bu davranışı devre dışı bırakmak isteyebilirsiniz.

Bu, özellikle özel değiştiriciniz hem düzeni hem de çizimi değiştiriyorsa yararlı olabilir. Otomatik geçersiz kılma özelliğini devre dışı bıraktığınızda, yalnızca çizimle ilgili özellikler (ör. color) değiştiğinde çizimi geçersiz kılarsınız ve düzeni geçersiz kılmazsınız. Bu, değiştiricinizin performansını artırabilir.

Bunun varsayıma dayalı bir örneği aşağıda verilmiştir. Bu örnekte, color, size ve onClick lambda'sı olan bir değiştirici gösterilmektedir. Bu değiştirici yalnızca gerekli olanları 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)
        }
    }
}