Kullanıcı etkileşimlerini yönetme

Kullanıcı arayüzü bileşenleri cihaz kullanıcısına ne kadar geri bildirim verildiğini yanıt verebilme olanağı sunar. Her bileşenin kendine özgü yanıt verme yöntemi vardır etkileşimleridir, bu da kullanıcının etkileşimlerinin neler yaptığını anlamasına yardımcı olur. Örneğin, Örneğin, kullanıcı bir cihazın dokunmatik ekranındaki bir düğmeye dokunursa düğme, bir şekilde değişmesi olasıdır. Bu değişiklik kullanıcının düğmeye dokunduğunu bilmesini sağlar. Kullanıcı önce parmaklarını düğmeden uzağa sürüklemelerini Aksi takdirde düğme etkinleşir.

Şekil 1. Düğmeler her zaman etkin, herhangi bir dalgalanma olmadan görünen düğmeler.
ziyaret edin.
Şekil 2. Etkinleştirme durumunu uygun şekilde yansıtan basın dalgaları içeren düğmeler.

Hareketler oluşturma dokümanı nasıl Oluşturma bileşenleri, işaretçi hareketlerini ve alt öğeleri gösteren alt düzey işaretçi etkinliklerini tıklama sayısı. Oluşturulan ilk ayar, bu düşük düzeyli etkinlikleri soyutlayarak daha üst düzey etkileşimler. Örneğin, bir dizi işaretçi etkinlik bir düğmeye basıp bırakın. Bu üst düzey soyutlamaları anlamak kullanıcı arayüzünüzün kullanıcıya nasıl tepki vereceğini özelleştirmenize yardımcı olur. Örneğin, ekip arkadaşlarınızın kullanıcı etkileşimde bulunduğunda bileşenin görünümünün ya da yalnızca bu kullanıcı işlemlerinin kaydını tutmak isteyebilirsiniz. Bu dokümanı, standart kullanıcı arayüzü öğelerini değiştirmek için gereken bilgileri sağlar. veya kendi şablonunuzu tasarlayın.

Etkileşimler

Birçok durumda, Oluşturma bileşeninizin yalnızca bu bileşenin nasıl Kullanıcı etkileşimlerini yorumlamak. Örneğin Button, Modifier.clickable kullanıcının düğmeyi tıklayıp tıklamadığını belirlemek için kullanılır. Standart bir uygulamanıza eklemek için düğmenin onClick kodunu tanımlayabilir ve Modifier.clickable, uygun olduğunda bu kodu çalıştırır. Yani bir projeyi kullanıcının ekrana dokunduğunu veya düğmeli bir düğmeyle mi klavye; Modifier.clickable, kullanıcının bir tıklama gerçekleştirdiğini anlar ve onClick kodunuzu çalıştırarak yanıt verir.

Ancak kullanıcı arayüzü bileşeninizin kullanıcı davranışına verdiği yanıtı özelleştirmek isterseniz işin mutfağını öğrenmeniz gerekebilir. Bu bölümde, bazılarını anlatacağım.

Bir kullanıcı, kullanıcı arayüzü bileşeniyle etkileşime geçtiğinde sistem bu bileşenin davranışını temsil eder bir dizi oluşturarak Interaction etkinlikler. Örneğin, kullanıcı bir düğmeye dokunduğunda, düğme PressInteraction.Press. Kullanıcı parmağını düğmenin içine sokarsa, bu işlem PressInteraction.Release düğme, tıklamanın sona erdiğini bilmesini sağlar. Diğer yandan, kullanıcı parmağını düğmenin dışına sürüklediğinde parmağını kaldırdığında oluşturur PressInteraction.Cancel değeri gösterilir.

Bu etkileşimler bildirimsizdir. Yani, bu alt düzey etkileşimler Kullanıcı işlemlerinin anlamını yorumlamayı amaçlamayan etkinlikler veya tıklayın. Ayrıca hangi kullanıcı işlemlerinin, öncelikli diğer işlemler.

Bu etkileşimler genellikle birer başlangıcı ve sonu olan ikili gruplar halinde gelir. İkinci etkileşiminde ilkine referans verilir. Örneğin, bir kullanıcı Kullanıcı bir düğmeye dokunduktan sonra parmağını kaldırırsa dokunduğunda PressInteraction.Press kullanıcı etkileşimi sağlar ve sürüm, PressInteraction.Release; Release, ilk adımı tanımlayan bir press özelliğine sahiptir PressInteraction.Press.

Belirli bir bileşene ilişkin etkileşimleri, söz konusu bileşene ait InteractionSource InteractionSource, Kotlin'in üzerine kurulmuştur akışlar sayesinde, etkileşimleri aynı şekilde toplayabilirsiniz. diğer akışlarla çalışırsınız. Bu tasarım kararı hakkında daha fazla bilgi için Etkileşimleri Aydınlatan blog yayınını inceleyin.

Etkileşim durumu

Bileşenlerinizin yerleşik işlevselliğini ayrıca şunları yaparak da genişletmek isteyebilirsiniz: kendiniz izlemelisiniz. Örneğin, bir düğmeyi tıklayarak renk değiştiriliyor. Etkileşimleri izlemenin en basit yolu uygun etkileşim durumunu gözlemleyin. InteractionSource bir numara sunuyor farklı etkileşim durumlarını ortaya koyan bir dizi yöntem içerir. Örneğin, basılı tutup tutmadığınızı görmek için, InteractionSource.collectIsPressedAsState() yöntem:

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()

Button(
    onClick = { /* do something */ },
    interactionSource = interactionSource
) {
    Text(if (isPressed) "Pressed!" else "Not pressed")
}

Compose, collectIsPressedAsState()'ın yanı sıra aşağıdakileri de sunar: collectIsFocusedAsState(), collectIsDraggedAsState() ve collectIsHoveredAsState(). Bu yöntemler aslında kolaylık alt düzey InteractionSource API'leri temel alınarak oluşturulur. Bazı durumlarda, alt düzey işlevleri doğrudan kullanmak istiyorsanız.

Örneğin, bir düğmeye basılıp basılmadığını bilmeniz gerektiğini ve aynı zamanda sürüklenip sürüklenmediğini de kontrol edebilirsiniz. collectIsPressedAsState() ürününün her ikisini de kullanıyorsanız ve collectIsDraggedAsState(), Compose'da pek çok yinelenen iş var ve Tüm etkileşimleri doğru sırayla alacağınızın garantisi yoktur. Örneğin, bu gibi durumlarda doğrudan paydaşlarla çalışmayı InteractionSource Etkileşimlerin izlenmesiyle ilgili daha fazla bilgi için daha fazla bilgi edinmek için InteractionSource ile çalışma başlıklı makaleye göz atın.InteractionSource

Aşağıdaki bölümde, Sırasıyla InteractionSource ve MutableInteractionSource.

Interaction tüket ve yay

InteractionSource, Interactions salt okunur akışını temsil eder; bu değil InteractionSource için Interaction yayımlanabilir. Yayınlanacak Interactions, bir MutableInteractionSource kullanmanız gerekir; InteractionSource.

Değiştiriciler ve bileşenler Interactions öğesini kullanabilir, yayabilir, tüketebilir ve yayabilir. Aşağıdaki bölümlerde, her ikisinden de etkileşimlerin nasıl tüketileceği ve değiştiricileri ve bileşenleri hakkında bilgi edindiniz.

Kullanma değiştiricisi örneği

Odaklanmış durum için kenarlık çizen bir değiştiricide, tek yapmanız gereken Interactions, böylece bir InteractionSource kabul edebilirsiniz:

fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier {
    // ...
}

İşlev imzasından bu değiştiricinin bir tüketici olduğu açıkça anlaşılıyor. Interaction öğelerini kullanabilir ancak yayınlayamaz.

Prodüksiyon değiştirici örneği

Modifier.hoverable gibi fareyle üzerine gelme etkinliklerini işleyen bir değiştirici için: Interactions özelliğini yayınlamalı ve MutableInteractionSource öğesini parametresini kullanabilirsiniz:

fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier {
    // ...
}

Bu değiştirici bir üreticidir; sağlanan Fareyle üzerine gelindiğinde HoverInteractions özelliğini yayınlamak için MutableInteractionSource veya elbette.

Kullanan ve üreten bileşenler

Malzeme Button gibi üst düzey bileşenler, hem üretici hem de yardımcı olur. Giriş ve odaklanma etkinliklerini işler, ayrıca görünümlerini değiştirirler (ör. dalga göstermek veya hareketlerini canlandırmak) rakım. Sonuç olarak, MutableInteractionSource hatasını doğrudan parametresini kullanabilirsiniz. Böylece, hatırlanan kendi örneğinizi sağlayabilirsiniz:

@Composable
fun Button(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,

    // exposes MutableInteractionSource as a parameter
    interactionSource: MutableInteractionSource? = null,

    elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(),
    shape: Shape = MaterialTheme.shapes.small,
    border: BorderStroke? = null,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    content: @Composable RowScope.() -> Unit
) { /* content() */ }

Bu sayede, özelliği kaldırabilir MutableInteractionSource bileşenin dışına çıkarıp tüm Bileşen tarafından üretilen Interaction'ler. Bunu kontrol etmek için veya kullanıcı arayüzündeki başka bir bileşenin görünümünü değiştirebilirsiniz.

Kendi etkileşimli üst düzey bileşenlerinizi oluşturuyorsanız MutableInteractionSource değerini bu şekilde sunabilirsiniz. Ayrıca Eskalasyonla ilgili en iyi uygulamaları izleyerek bu verileri okumayı ve Tıpkı diğer herhangi bir bileşenin görsel durumunu, durumu (etkin durum gibi) okunabilir ve kontrol edilebilir.

Compose, katmanlı bir mimari yaklaşımı izler. Böylece üst seviye Malzeme bileşenleri, temel binanın üzerine inşa edilir dalgaları ve diğer özellikleri kontrol etmek için ihtiyaç duydukları Interaction'leri üreten bloklar görsel efektleri kullanın. Temel kitaplığı, üst düzey etkileşim değiştiricileri sunar Örneğin Modifier.hoverable, Modifier.focusable ve Modifier.draggable.

Fareyle öğelerin üzerine gelerek etkinliklere yanıt veren bir bileşen oluşturmak için Modifier.hoverable ve parametre olarak bir MutableInteractionSource iletin. Bileşenin üzerine gelindiğinde HoverInteraction sesi yayılır ve bileşenin görünümünü değiştirmek için bunu kullanın.

// This InteractionSource will emit hover interactions
val interactionSource = remember { MutableInteractionSource() }

Box(
    Modifier
        .size(100.dp)
        .hoverable(interactionSource = interactionSource),
    contentAlignment = Alignment.Center
) {
    Text("Hello!")
}

Ayrıca bu bileşeni odaklanılabilir hale getirmek için Modifier.focusable ekleyip parametre ile aynı MutableInteractionSource. Şimdi hem HoverInteraction.Enter/Exit ve FocusInteraction.Focus/Unfocus yayınlandı aynı MutableInteractionSource ile biçimlendirebilir ve Aynı yerde her iki etkileşim türü için de görünüm:

// This InteractionSource will emit hover and focus interactions
val interactionSource = remember { MutableInteractionSource() }

Box(
    Modifier
        .size(100.dp)
        .hoverable(interactionSource = interactionSource)
        .focusable(interactionSource = interactionSource),
    contentAlignment = Alignment.Center
) {
    Text("Hello!")
}

Modifier.clickable daha da yüksek hoverable ve focusable'ye göre daha düşük düzeyli soyutlama (bir bileşen için dolaylı olarak üzerine gelinebilir ve tıklanabilen bileşenler, odaklanabillirsiniz. Aşağıdaki özelliklere sahip bir bileşen oluşturmak için Modifier.clickable kullanabilirsiniz: fareyle üzerine gelme, odaklanma ve basma gibi etkileşimlerde, daha düşük düzeyde daha fazla bilgi edineceksiniz. Bileşeninizi de tıklanabilir yapmak isterseniz hoverable ve focusable değerlerini clickable ile değiştirin:

// This InteractionSource will emit hover, focus, and press interactions
val interactionSource = remember { MutableInteractionSource() }
Box(
    Modifier
        .size(100.dp)
        .clickable(
            onClick = {},
            interactionSource = interactionSource,

            // Also show a ripple effect
            indication = ripple()
        ),
    contentAlignment = Alignment.Center
) {
    Text("Hello!")
}

InteractionSource ile çalışın

Bir bileşenle kurulan etkileşimler hakkında alt düzey bilgilere ihtiyacınız varsa söz konusu bileşenin InteractionSource öğesi için standart akış API'leri kullanmanız gerekir. Örneğin, basın ve sürükleme hareketlerinin bir listesini tutmak istediğinizi InteractionSource için etkileşimler. Bu kod işin yarısını yapar, yeni basın mensupları listeye eklendi:

val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }

LaunchedEffect(interactionSource) {
    interactionSource.interactions.collect { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                interactions.add(interaction)
            }
            is DragInteraction.Start -> {
                interactions.add(interaction)
            }
        }
    }
}

Ancak, yeni etkileşimleri eklemenin yanı sıra etkileşimi kaldırmanız da gerekir. kullanıcı parmağını tekrar kaldırdığında (örneğin, kullanıcı parmağını tekrar bileşeni). Bunu yapmak kolaydır, çünkü son etkileşimler her zaman ilişkilendirilmiş başlangıç etkileşimine referans vermelidir. Bu kod, Sona eren etkileşimler:

val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }

LaunchedEffect(interactionSource) {
    interactionSource.interactions.collect { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                interactions.add(interaction)
            }
            is PressInteraction.Release -> {
                interactions.remove(interaction.press)
            }
            is PressInteraction.Cancel -> {
                interactions.remove(interaction.press)
            }
            is DragInteraction.Start -> {
                interactions.add(interaction)
            }
            is DragInteraction.Stop -> {
                interactions.remove(interaction.start)
            }
            is DragInteraction.Cancel -> {
                interactions.remove(interaction.start)
            }
        }
    }
}

Bileşene şu anda basılıp sürüklenmediğini öğrenmek isterseniz tek yapmanız gereken interactions alanının boş olup olmadığını kontrol etmektir:

val isPressedOrDragged = interactions.isNotEmpty()

En son etkileşimin ne olduğunu öğrenmek istiyorsanız son etkileşiminize öğesini seçin. Örneğin, Compose dalga uygulaması şu şekildedir: en son etkileşim için kullanılacak uygun durum yer paylaşımını belirler:

val lastInteraction = when (interactions.lastOrNull()) {
    is DragInteraction.Start -> "Dragged"
    is PressInteraction.Press -> "Pressed"
    else -> "No state"
}

Tüm Interaction'ler aynı yapıyı izlediğinden farklı türlerde kullanıcı etkileşimleri ile çalışırken kodda farklılık gösterir. aynıdır.

Bu bölümde yer alan örneklerin şunun Flow öğesini temsil ettiğini unutmayın: State kullanan etkileşimler Böylece, güncellenen değerleri gözlemlemeyi kolaylaştırır. değer okunması, otomatik olarak yeniden bestelere neden olur. Ancak, bestenin ön çerçeveden toplu olarak işlenmesini sağlar. Bu, durumda değişiklik olursa ve aynı çerçeve içinde tekrar değiştiğinde, durumu gözlemleyen bileşenler değişikliği görün.

Etkileşimler düzenli olarak başlayıp bitebileceği için bu önemlidir aynı çerçevede. Örneğin, önceki örnek Button ile kullanıldığında:

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()

Button(onClick = { /* do something */ }, interactionSource = interactionSource) {
    Text(if (isPressed) "Pressed!" else "Not pressed")
}

Bir basma işlemi aynı çerçeve içinde başlayıp biterse, metin hiçbir zaman "Basıldı!". Çoğu durumda bu bir sorun değildir. Bu, kullanıcılara görsel küçük bir süre ekranda titremeye neden olur ve çok fazla fark edebilirsiniz. Dalga etkisi veya dalga etkisi istiyorsanız efekti en azından minimum bir miktarda göstermek isteyebilirsiniz. otomatik olarak ayarlamayı deneyin. Alıcı: animasyonları doğrudan toplamanın içinden başlatabilir ve durdurabilirsiniz lambda yazar. Bu modelin bir örneği de Animasyonlu kenarlıkla gelişmiş Indication oluşturma bölümünü tıklayın.

Örnek: Özel etkileşim işleme içeren derleme bileşeni

Girişe yönelik özel bir yanıtla bileşenleri nasıl oluşturacağınızı öğrenmek için örneğini görebilirsiniz. Bu örnekte, düğme yerine görünüşünü değiştirerek baskılara yanıt verir:

Tıklandığında dinamik olarak alışveriş sepeti simgesi ekleyen düğme animasyonu
Şekil 3. Tıklandığında dinamik olarak simge ekleyen bir düğme.

Bunun için, Button tabanlı özel bir composable derleyin ve bu composable'ın ek icon parametresini kullanın (bu örnekte, alışveriş sepeti). Siz kullanıcınıncollectIsPressedAsState() düğme; bu simgelerle birlikte simgeyi de eklersiniz. Kod şu şekilde görünür:

@Composable
fun PressIconButton(
    onClick: () -> Unit,
    icon: @Composable () -> Unit,
    text: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    interactionSource: MutableInteractionSource? = null
) {
    val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false

    Button(
        onClick = onClick,
        modifier = modifier,
        interactionSource = interactionSource
    ) {
        AnimatedVisibility(visible = isPressed) {
            if (isPressed) {
                Row {
                    icon()
                    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
                }
            }
        }
        text()
    }
}

Yeni composable'ı şu şekilde kullanabilirsiniz:

PressIconButton(
    onClick = {},
    icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) },
    text = { Text("Add to cart") }
)

Çünkü bu yeni PressIconButton mevcut Malzemenin üzerine inşa edilmiştir Button, kullanıcı etkileşimlerine her zamanki şekilde tepki verir. Kullanıcı düğmeye bastığında opaklığını normal bir Malzeme Button.

Indication ile yeniden kullanılabilir özel efekt oluşturup uygulayın

Önceki bölümlerde, yanıt olarak bir bileşenin parçasını nasıl değiştireceğinizi farklı Interaction'lere gönderebilirsiniz. Örneğin, basıldığında bir simge görüntüleyebilirsiniz. Bu aynı Bu yaklaşım, bir reklam grubuna sağladığınız parametrelerin değerini değiştirmek için ya da bileşenin içinde görüntülenen içeriği değiştirebilirsiniz, ancak bu, yalnızca bileşen bazında geçerlidir. Genellikle, bir uygulama veya tasarım sistemi durum bilgili görsel efektler için genel bir sisteme sahip olur. Bu efekt, ve tüm bileşenlere tutarlı bir şekilde uygulanır.

Bu tür bir tasarım sistemi oluşturuyorsanız, bir bileşeni özelleştirerek bu özelleştirmeyi diğer bileşenlerde yeniden kullanmak, şu nedenlerle:

  • Tasarım sistemindeki her bileşen aynı ortak çalışmaya ihtiyaç duyar
  • Bu efekti yeni oluşturulan bileşenlere ve özel efektlere uygulamayı kolayca unutabilirsiniz tıklanabilir bileşenler
  • Özel efekti diğer efektlerle birleştirmek zor olabilir

Bu sorunları önlemek ve özel bir bileşeni sisteminizde kolayca ölçeklendirmek için Indication kullanabilirsiniz. Indication, şunlara uygulanabilecek yeniden kullanılabilir bir görsel efekti temsil eder: bileşenlerden bahsedeceğiz. Indication, ikiye ayrıldı parçalar:

  • IndicationNodeFactory: Şu özelliklere sahip Modifier.Node örnek oluşturan bir fabrika: görsel efekt oluşturmak için kullanılır. Basit uygulamalar için bu bir tekli nesne (nesne) olabilir ve farklı bileşenlerde tüm uygulamayı kapsar.

    Bu örnekler durum bilgili veya durum bilgisiz olabilir. Her biri bileşenine göre, bir CompositionLocal bileşeninden değer alabilirler ve başka herhangi bir bileşende olduğu gibi belirli bir bileşenin içinde Modifier.Node.

  • Modifier.indication: Şunun için Indication çeken bir değiştirici: bir bileşenidir. Modifier.clickable ve diğer üst düzey etkileşim değiştiriciler bir gösterge parametresini doğrudan kabul eder. Böylece, Interaction'lar ancak kullandıkları Interaction'ler için görsel efektler de çizebilir oluşturur. Basit durumlarda Modifier.clickable olmadan da kullanabilirsiniz Modifier.indication gerekiyor.

Efekti Indication ile değiştirin

Bu bölümde, bir öğeye manuel ölçek efektinin nasıl değiştirileceği açıklanmaktadır. birden fazla cihazda yeniden kullanılabilecek gösterge eşdeğeri olan belirli bir düğme bileşenlerine ayıralım.

Aşağıdaki kod, basıldığında aşağı doğru ölçeklendirilen bir düğme oluşturur:

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale")

Button(
    modifier = Modifier.scale(scale),
    onClick = { },
    interactionSource = interactionSource
) {
    Text(if (isPressed) "Pressed!" else "Not pressed")
}

Yukarıdaki snippet'teki ölçek efektini Indication biçimine dönüştürmek için şu adımları uygulayın:

  1. Ölçek efektinin uygulanmasından sorumlu olan Modifier.Node dosyasını oluşturun. Takıldığında düğüm, öncekine benzer şekilde etkileşim kaynağını gözlemler örnekler. Buradaki tek fark, animasyonları doğrudan başlatma saptamak yerine bunları kullanır.

    Düğümün geçersiz kılması için DrawModifierNode öğesini uygulaması gerekir ContentDrawScope#draw() ve aynı çizimi kullanarak bir ölçek efekti oluşturun komutlarını kullanabilirsiniz.

    ContentDrawScope adlı alıcıdan drawContent() için yapılan telefon görüşmesinde çekilecek Indication uygulanması gereken gerçek bileşene bir ölçek dönüşümü içinde çağrılması gerekir. Lütfen Indication uygulamaları bir noktada her zaman drawContent() yöntemini çağırır; Aksi takdirde, Indication öğesini uyguladığınız bileşen çizilmez.

    private class ScaleNode(private val interactionSource: InteractionSource) :
        Modifier.Node(), DrawModifierNode {
    
        var currentPressPosition: Offset = Offset.Zero
        val animatedScalePercent = Animatable(1f)
    
        private suspend fun animateToPressed(pressPosition: Offset) {
            currentPressPosition = pressPosition
            animatedScalePercent.animateTo(0.9f, spring())
        }
    
        private suspend fun animateToResting() {
            animatedScalePercent.animateTo(1f, spring())
        }
    
        override fun onAttach() {
            coroutineScope.launch {
                interactionSource.interactions.collectLatest { interaction ->
                    when (interaction) {
                        is PressInteraction.Press -> animateToPressed(interaction.pressPosition)
                        is PressInteraction.Release -> animateToResting()
                        is PressInteraction.Cancel -> animateToResting()
                    }
                }
            }
        }
    
        override fun ContentDrawScope.draw() {
            scale(
                scale = animatedScalePercent.value,
                pivot = currentPressPosition
            ) {
                this@draw.drawContent()
            }
        }
    }

  2. IndicationNodeFactory oluşturun. Tek sorumluluğu, yeni bir yeni düğüm örneği sağlayın. Herhangi bir parametrelerini ayarlamak yerine, fabrika bir nesne olabilir:

    object ScaleIndication : IndicationNodeFactory {
        override fun create(interactionSource: InteractionSource): DelegatableNode {
            return ScaleNode(interactionSource)
        }
    
        override fun equals(other: Any?): Boolean = other === ScaleIndication
        override fun hashCode() = 100
    }

  3. Modifier.clickable, dahili olarak Modifier.indication kullanıyor. Bu nedenle, ScaleIndication ile tıklanabilir bir bileşene sahipse, tek yapmanız gereken clickable parametresi olarak Indication:

    Box(
        modifier = Modifier
            .size(100.dp)
            .clickable(
                onClick = {},
                indication = ScaleIndication,
                interactionSource = null
            )
            .background(Color.Blue),
        contentAlignment = Alignment.Center
    ) {
        Text("Hello!", color = Color.White)
    }

    Bu, aynı zamanda özel bir algoritma kullanarak üst düzey, yeniden kullanılabilir bileşenler oluşturmayı da Indication — bir düğme şöyle görünebilir:

    @Composable
    fun ScaleButton(
        onClick: () -> Unit,
        modifier: Modifier = Modifier,
        enabled: Boolean = true,
        interactionSource: MutableInteractionSource? = null,
        shape: Shape = CircleShape,
        content: @Composable RowScope.() -> Unit
    ) {
        Row(
            modifier = modifier
                .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp)
                .clickable(
                    enabled = enabled,
                    indication = ScaleIndication,
                    interactionSource = interactionSource,
                    onClick = onClick
                )
                .border(width = 2.dp, color = Color.Blue, shape = shape)
                .padding(horizontal = 16.dp, vertical = 8.dp),
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically,
            content = content
        )
    }

Ardından, düğmeyi aşağıdaki şekilde kullanabilirsiniz:

ScaleButton(onClick = {}) {
    Icon(Icons.Filled.ShoppingCart, "")
    Spacer(Modifier.padding(10.dp))
    Text(text = "Add to cart!")
}

Basıldığında küçülen alışveriş sepeti simgesinin yer aldığı düğme animasyonu
Şekil 4. Özel Indication ile oluşturulmuş bir düğme.
ziyaret edin.

Animasyonlu kenarlık içeren gelişmiş Indication oluştur

Indication yalnızca bir öğeyi ölçeklendirerek bir bileşenidir. IndicationNodeFactory, Modifier.Node döndürdüğü için diğer çizim API'lerinde olduğu gibi içeriğin üzerinde veya altında her türlü efekt. Örneğin, bileşenin etrafına animasyonlu bir kenarlık ve bileşeninin üst tarafında:

Basıldığında süslü gökkuşağı efektli düğme
Şekil 5. Indication ile çizilmiş animasyonlu kenarlık efekti.

Buradaki Indication uygulaması önceki örneğe çok benzerdir — bazı parametreler içeren bir düğüm oluşturur. Animasyonlu kenarlık Indication öğesinin kullanıldığı bileşenin şekline ve kenarlığına Indication uygulaması için şekil ve kenarlık genişliğinin de sağlanması gerekir kullanabilirsiniz:

data class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : IndicationNodeFactory {

    override fun create(interactionSource: InteractionSource): DelegatableNode {
        return NeonNode(
            shape,
            // Double the border size for a stronger press effect
            borderWidth * 2,
            interactionSource
        )
    }
}

Modifier.Node uygulaması, daha karmaşıktır. Daha önce olduğu gibi InteractionSource gözlemliyor eklendiğinde, animasyonları başlatır ve çizim için DrawModifierNode uygular aşağıdaki etkiyi yaratır:

private class NeonNode(
    private val shape: Shape,
    private val borderWidth: Dp,
    private val interactionSource: InteractionSource
) : Modifier.Node(), DrawModifierNode {
    var currentPressPosition: Offset = Offset.Zero
    val animatedProgress = Animatable(0f)
    val animatedPressAlpha = Animatable(1f)

    var pressedAnimation: Job? = null
    var restingAnimation: Job? = null

    private suspend fun animateToPressed(pressPosition: Offset) {
        // Finish any existing animations, in case of a new press while we are still showing
        // an animation for a previous one
        restingAnimation?.cancel()
        pressedAnimation?.cancel()
        pressedAnimation = coroutineScope.launch {
            currentPressPosition = pressPosition
            animatedPressAlpha.snapTo(1f)
            animatedProgress.snapTo(0f)
            animatedProgress.animateTo(1f, tween(450))
        }
    }

    private fun animateToResting() {
        restingAnimation = coroutineScope.launch {
            // Wait for the existing press animation to finish if it is still ongoing
            pressedAnimation?.join()
            animatedPressAlpha.animateTo(0f, tween(250))
            animatedProgress.snapTo(0f)
        }
    }

    override fun onAttach() {
        coroutineScope.launch {
            interactionSource.interactions.collect { interaction ->
                when (interaction) {
                    is PressInteraction.Press -> animateToPressed(interaction.pressPosition)
                    is PressInteraction.Release -> animateToResting()
                    is PressInteraction.Cancel -> animateToResting()
                }
            }
        }
    }

    override fun ContentDrawScope.draw() {
        val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition(
            currentPressPosition, size
        )
        val brush = animateBrush(
            startPosition = startPosition,
            endPosition = endPosition,
            progress = animatedProgress.value
        )
        val alpha = animatedPressAlpha.value

        drawContent()

        val outline = shape.createOutline(size, layoutDirection, this)
        // Draw overlay on top of content
        drawOutline(
            outline = outline,
            brush = brush,
            alpha = alpha * 0.1f
        )
        // Draw border on top of overlay
        drawOutline(
            outline = outline,
            brush = brush,
            alpha = alpha,
            style = Stroke(width = borderWidth.toPx())
        )
    }

    /**
     * Calculates a gradient start / end where start is the point on the bounding rectangle of
     * size [size] that intercepts with the line drawn from the center to [pressPosition],
     * and end is the intercept on the opposite end of that line.
     */
    private fun calculateGradientStartAndEndFromPressPosition(
        pressPosition: Offset,
        size: Size
    ): Pair<Offset, Offset> {
        // Convert to offset from the center
        val offset = pressPosition - size.center
        // y = mx + c, c is 0, so just test for x and y to see where the intercept is
        val gradient = offset.y / offset.x
        // We are starting from the center, so halve the width and height - convert the sign
        // to match the offset
        val width = (size.width / 2f) * sign(offset.x)
        val height = (size.height / 2f) * sign(offset.y)
        val x = height / gradient
        val y = gradient * width

        // Figure out which intercept lies within bounds
        val intercept = if (abs(y) <= abs(height)) {
            Offset(width, y)
        } else {
            Offset(x, height)
        }

        // Convert back to offsets from 0,0
        val start = intercept + size.center
        val end = Offset(size.width - start.x, size.height - start.y)
        return start to end
    }

    private fun animateBrush(
        startPosition: Offset,
        endPosition: Offset,
        progress: Float
    ): Brush {
        if (progress == 0f) return TransparentBrush

        // This is *expensive* - we are doing a lot of allocations on each animation frame. To
        // recreate a similar effect in a performant way, it would be better to create one large
        // gradient and translate it on each frame, instead of creating a whole new gradient
        // and shader. The current approach will be janky!
        val colorStops = buildList {
            when {
                progress < 1 / 6f -> {
                    val adjustedProgress = progress * 6f
                    add(0f to Blue)
                    add(adjustedProgress to Color.Transparent)
                }
                progress < 2 / 6f -> {
                    val adjustedProgress = (progress - 1 / 6f) * 6f
                    add(0f to Purple)
                    add(adjustedProgress * MaxBlueStop to Blue)
                    add(adjustedProgress to Blue)
                    add(1f to Color.Transparent)
                }
                progress < 3 / 6f -> {
                    val adjustedProgress = (progress - 2 / 6f) * 6f
                    add(0f to Pink)
                    add(adjustedProgress * MaxPurpleStop to Purple)
                    add(MaxBlueStop to Blue)
                    add(1f to Blue)
                }
                progress < 4 / 6f -> {
                    val adjustedProgress = (progress - 3 / 6f) * 6f
                    add(0f to Orange)
                    add(adjustedProgress * MaxPinkStop to Pink)
                    add(MaxPurpleStop to Purple)
                    add(MaxBlueStop to Blue)
                    add(1f to Blue)
                }
                progress < 5 / 6f -> {
                    val adjustedProgress = (progress - 4 / 6f) * 6f
                    add(0f to Yellow)
                    add(adjustedProgress * MaxOrangeStop to Orange)
                    add(MaxPinkStop to Pink)
                    add(MaxPurpleStop to Purple)
                    add(MaxBlueStop to Blue)
                    add(1f to Blue)
                }
                else -> {
                    val adjustedProgress = (progress - 5 / 6f) * 6f
                    add(0f to Yellow)
                    add(adjustedProgress * MaxYellowStop to Yellow)
                    add(MaxOrangeStop to Orange)
                    add(MaxPinkStop to Pink)
                    add(MaxPurpleStop to Purple)
                    add(MaxBlueStop to Blue)
                    add(1f to Blue)
                }
            }
        }

        return linearGradient(
            colorStops = colorStops.toTypedArray(),
            start = startPosition,
            end = endPosition
        )
    }

    companion object {
        val TransparentBrush = SolidColor(Color.Transparent)
        val Blue = Color(0xFF30C0D8)
        val Purple = Color(0xFF7848A8)
        val Pink = Color(0xFFF03078)
        val Orange = Color(0xFFF07800)
        val Yellow = Color(0xFFF0D800)
        const val MaxYellowStop = 0.16f
        const val MaxOrangeStop = 0.33f
        const val MaxPinkStop = 0.5f
        const val MaxPurpleStop = 0.67f
        const val MaxBlueStop = 0.83f
    }
}

Buradaki temel fark şudur: animasyonunda, animateToResting() işleviyle birlikte çalışır. hemen kullanılabilirse basın animasyonu devam eder. Ayrıca, birden fazla hızlı basma için (animateToPressed) animasyon sırasında gerçekleştiğini varsayalım. Önceki animasyon işlemi iptal edilir ve basın animasyonu baştan başlar. Birden fazla desteği eşzamanlı efektler (ör. yeni bir dalga animasyonu üstünde), animasyonları liste halinde izlemek yerine mevcut animasyonları iptal etmek ve yenilerini başlatmak.

ziyaret edin.