Kullanıcı arayüzü bileşenleri, kullanıcı etkileşimlerine yanıt verme şekilleriyle cihaz kullanıcısına geri bildirim verir. Her bileşenin etkileşimlere yanıt verme şekli farklıdır. Bu sayede kullanıcı, etkileşimlerinin ne işe yaradığını anlayabilir. Örneğin, kullanıcı bir cihazın dokunmatik ekranındaki bir düğmeye dokunursa düğme muhtemelen bir şekilde değişir (ör. vurgu rengi eklenir). Bu değişiklik, kullanıcıya düğmeye dokunduğunu bildirir. Kullanıcı bu işlemi yapmak istemiyorsa parmağını düğmeden çekmeden önce başka bir yere sürüklemesi gerektiğini bilir. Aksi takdirde düğme etkinleşir.
Compose Hareketler dokümanlarında, Compose bileşenlerinin işaretçi hareketleri ve tıklamalar gibi alt düzey işaretçi etkinliklerini nasıl işlediği açıklanmaktadır. Compose, bu alt düzey etkinlikleri daha üst düzey etkileşimlere dönüştürür. Örneğin, bir dizi işaretçi etkinliği, bir düğmeye basma ve bırakma işlemine karşılık gelebilir. Bu üst düzey soyutlamaları anlamak, kullanıcı arayüzünüzün kullanıcıya nasıl yanıt vereceğini özelleştirmenize yardımcı olabilir. Örneğin, bir bileşenin görünümünün kullanıcı etkileşimiyle nasıl değişeceğini özelleştirmek veya yalnızca bu kullanıcı işlemlerinin günlüğünü tutmak isteyebilirsiniz. Bu belgede, standart kullanıcı arayüzü öğelerini değiştirmek veya kendi öğelerinizi tasarlamak için ihtiyacınız olan bilgiler verilmektedir.
Etkileşimler
Çoğu durumda, Compose bileşeninizin kullanıcı etkileşimlerini nasıl yorumladığını bilmeniz gerekmez. Örneğin, Button, kullanıcının düğmeyi tıklayıp tıklamadığını anlamak için Modifier.clickable kullanır. Uygulamanıza tipik bir düğme ekliyorsanız düğmenin onClick kodunu tanımlayabilirsiniz. Modifier.clickable, uygun olduğunda bu kodu çalıştırır. Yani kullanıcının ekrana dokunup dokunmadığını veya düğmeyi klavyeyle seçip seçmediğini bilmeniz gerekmez. Modifier.clickable, kullanıcının tıklama yaptığını 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 istiyorsanız arka planda neler olduğunu daha ayrıntılı bir şekilde öğrenmeniz gerekebilir. Bu bölümde, bu bilgilerden bazıları verilmektedir.
Kullanıcı bir kullanıcı arayüzü bileşeniyle etkileşim kurduğunda sistem, bir dizi Interaction etkinliği oluşturarak kullanıcının davranışını temsil eder. Örneğin, kullanıcı bir düğmeye dokunursa düğme PressInteraction.Press oluşturur.
Kullanıcı parmağını düğmenin içinde kaldırırsa PressInteraction.Release oluşturulur ve düğmeye tıklamanın tamamlandığı bildirilir. Öte yandan, kullanıcı parmağını düğmenin dışına sürükleyip parmağını kaldırırsa düğme, düğmeye basma işleminin tamamlanmadığını, iptal edildiğini belirtmek için PressInteraction.Cancel oluşturur.
Bu etkileşimler tarafsızdır. Yani bu alt düzey etkileşim etkinlikleri, kullanıcı işlemlerinin anlamını veya sırasını yorumlamayı amaçlamaz. Ayrıca, hangi kullanıcı işlemlerinin diğer işlemlerden daha öncelikli olabileceğini de yorumlamazlar.
Bu etkileşimler genellikle başlangıç ve bitiş olmak üzere çiftler halinde gerçekleşir. İkinci etkileşim, ilk etkileşime bir referans içerir. Örneğin, bir kullanıcı bir düğmeye dokunup parmağını kaldırırsa dokunma işlemi bir PressInteraction.Press etkileşimi, bırakma işlemi ise bir PressInteraction.Release oluşturur. Release, ilk PressInteraction.Press'i tanımlayan bir press özelliğine sahiptir.
Belirli bir bileşenin etkileşimlerini, InteractionSource değerini inceleyerek görebilirsiniz. InteractionSource, Kotlin akışları üzerine inşa edildiğinden, etkileşimleri diğer akışlarda olduğu gibi toplayabilirsiniz. Bu tasarım kararı hakkında daha fazla bilgi için Illuminating Interactions blog yayınını inceleyin.
Etkileşim durumu
Etkileşimleri kendiniz de izleyerek bileşenlerinizin yerleşik işlevlerini genişletmek isteyebilirsiniz. Örneğin, bir düğmenin basıldığında renk değiştirmesini isteyebilirsiniz. Etkileşimleri izlemenin en basit yolu, uygun etkileşim durumunu gözlemlemektir. InteractionSource, çeşitli etkileşim durumlarını durum olarak gösteren çeşitli yöntemler sunar. Örneğin, belirli bir düğmenin basılı olup olmadığını görmek istiyorsanız düğmenin InteractionSource.collectIsPressedAsState() yöntemini çağırabilirsiniz:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button( onClick = { /* do something */ }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Compose, collectIsPressedAsState() dışında collectIsFocusedAsState(), collectIsDraggedAsState() ve collectIsHoveredAsState() de sunar. Bu yöntemler aslında alt düzey InteractionSource API'leri üzerine inşa edilmiş kolaylık yöntemleridir. Bazı durumlarda bu alt düzey işlevleri doğrudan kullanmak isteyebilirsiniz.
Örneğin, bir düğmeye basılıp basılmadığını ve aynı zamanda sürüklenip sürüklenmediğini bilmeniz gerektiğini varsayalım. Hem collectIsPressedAsState() hem de collectIsDraggedAsState() kullanırsanız Compose, birçok yinelenen işlem yapar ve tüm etkileşimleri doğru sırada alacağınızın garantisi yoktur. Bu gibi durumlarda doğrudan InteractionSource ile çalışmak isteyebilirsiniz. Etkileşimleri InteractionSource ile kendiniz izleme hakkında daha fazla bilgi için InteractionSource ile çalışma başlıklı makaleyi inceleyin.
Aşağıdaki bölümde, InteractionSource ve MutableInteractionSource ile etkileşimlerin nasıl kullanılacağı ve yayınlanacağı açıklanmaktadır.
Interaction tüketir ve yayar
InteractionSource, salt okunur bir Interactions akışını temsil eder. InteractionSource'e Interaction göndermek mümkün değildir. Interaction yaymak için InteractionSource'den genişletilen bir MutableInteractionSource kullanmanız gerekir.
Değiştiriciler ve bileşenler Interactions tüketebilir, yayabilir veya hem tüketip hem yayabilir.
Aşağıdaki bölümlerde, hem değiştiricilerden hem de bileşenlerden etkileşimlerin nasıl kullanılacağı ve yayınlanacağı açıklanmaktadır.
Tüketim değiştirici örneği
Odaklanmış durum için kenarlık çizen bir değiştirici için yalnızca Interactions öğesini gözlemlemeniz gerekir. Böylece bir InteractionSource kabul edebilirsiniz:
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
İşlev imzasından bu değiştiricinin bir tüketici olduğu anlaşılıyor. Interactions tüketebilir ancak bunları yayınlayamaz.
Üretici değiştirici örneği
Modifier.hoverable gibi fareyle üzerine gelme etkinliklerini işleyen bir değiştirici için Interactions yayınlamanız ve parametre olarak MutableInteractionSource kabul etmeniz gerekir:
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
Bu değiştirici bir üreticidir. Fareyle üzerine gelindiğinde veya fareyle üzerine gelme işlemi iptal edildiğinde HoverInteractions yayınlamak için sağlanan MutableInteractionSource değerini kullanabilir.
Tüketen ve üreten bileşenler oluşturma
Material Button gibi üst düzey bileşenler hem üretici hem de tüketici olarak işlev görür. Giriş ve odaklanma etkinliklerini işler ve bu etkinliklere yanıt olarak görünümlerini değiştirirler (ör. dalga efekti gösterme veya yükseltmelerini canlandırma). Bu nedenle, kendi hatırlanan örneğinizi sağlayabilmeniz için MutableInteractionSource öğesini doğrudan parametre olarak gösterirler:
@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 MutableInteractionSource bileşenin dışına çıkarılabilir ve bileşen tarafından üretilen tüm Interaction gözlemlenebilir. Bu bileşenin veya kullanıcı arayüzünüzdeki diğer bileşenlerin görünümünü kontrol etmek için bu özelliği kullanabilirsiniz.
Kendi etkileşimli üst düzey bileşenlerinizi oluşturuyorsanız MutableInteractionSource parametresini bu şekilde kullanmanızı öneririz. Bu, state hoisting ile ilgili en iyi uygulamaları takip etmenin yanı sıra bir bileşenin görsel durumunu, diğer durum türleri (ör. etkin durum) gibi kolayca okuyup kontrol etmenizi sağlar.
Compose, katmanlı bir mimari yaklaşım izler. Bu nedenle, üst düzey Material bileşenleri, dalgaları ve diğer görsel efektleri kontrol etmek için ihtiyaç duydukları Interaction öğelerini üreten temel yapı taşları üzerine inşa edilir. Temel kitaplık, Modifier.hoverable, Modifier.focusable ve Modifier.draggable gibi üst düzey etkileşim değiştiriciler sağlar.
Fareyle üzerine gelme etkinliklerine yanıt veren bir bileşen oluşturmak için Modifier.hoverable kullanıp parametre olarak MutableInteractionSource iletebilirsiniz.
Bileşenin üzerine gelindiğinde HoverInteraction yayınlanır. Bunu, bileşenin görünümünü değiştirmek için kullanabilirsiniz.
// This InteractionSource will emit hover interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
Bu bileşeni odaklanabilir hale getirmek için Modifier.focusable ekleyebilir ve aynı MutableInteractionSource öğesini parametre olarak iletebilirsiniz. Artık hem HoverInteraction.Enter/Exit hem de FocusInteraction.Focus/Unfocus aynı MutableInteractionSource üzerinden yayınlanıyor ve her iki etkileşim türünün görünümünü aynı yerden özelleştirebilirsiniz:
// 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, hoverable ve focusable'ten daha yüksek düzeyde bir soyutlamadır. Tıklanabilir bir bileşen, dolaylı olarak üzerine gelinebilir ve tıklanabilir bileşenler de odaklanılabilir olmalıdır. Alt düzey API'leri birleştirmenize gerek kalmadan fareyle üzerine gelme, odaklanma ve basma etkileşimlerini işleyen bir bileşen oluşturmak için Modifier.clickable kullanabilirsiniz. Bileşeninizi tıklanabilir hale de getirmek istiyorsanız hoverable ve focusable öğelerini clickable ile değiştirebilirsiniz:
// 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ışma
Bir bileşenle etkileşimler hakkında alt düzey bilgilere ihtiyacınız varsa bu bileşenin InteractionSource için standart akış API'lerini kullanabilirsiniz.
Örneğin, bir InteractionSource için basma ve sürükleme etkileşimlerinin listesini tutmak istediğinizi varsayalım. Bu kod, işin yarısını yapar ve yeni
basılanları geldikçe listeye ekler:
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şimler eklemenin yanı sıra, sona erdiklerinde (örneğin, kullanıcı parmağını bileşenden kaldırdığında) etkileşimleri kaldırmanız da gerekir. Bitiş etkileşimleri her zaman ilişkili başlangıç etkileşimine referans içerdiğinden bunu yapmak kolaydır. Bu kod, sona eren etkileşimleri nasıl kaldıracağınızı gösterir:
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şenin şu anda basılıp basılmadığını veya sürüklenip sürüklenmediğini öğrenmek için
interactions değerinin boş olup olmadığını kontrol etmeniz yeterlidir:
val isPressedOrDragged = interactions.isNotEmpty()
En son etkileşimin ne olduğunu öğrenmek istiyorsanız listedeki son öğeye bakmanız yeterlidir. Örneğin, Compose dalga efekti uygulaması, en son etkileşim için kullanılacak uygun durum yer paylaşımını şu şekilde belirler:
val lastInteraction = when (interactions.lastOrNull()) { is DragInteraction.Start -> "Dragged" is PressInteraction.Press -> "Pressed" else -> "No state" }
Tüm Interactions aynı yapıyı izlediğinden, farklı kullanıcı etkileşimi türleriyle çalışırken kodda çok fazla fark olmaz. Genel kalıp aynıdır.
Bu bölümdeki önceki örneklerin, State kullanarak etkileşimlerin Flow temsil ettiğini unutmayın. Durum değerinin okunması otomatik olarak yeniden oluşturmalara neden olacağından güncellenen değerleri gözlemlemek kolaylaşır. Ancak oluşturma işlemi, kare öncesinde toplu olarak yapılır. Yani durum değişir ve aynı kare içinde tekrar değişirse durumu gözlemleyen bileşenler değişikliği görmez.
Etkileşimler aynı karede başlayıp bitebileceğinden bu durum etkileşimler için önemlidir. Örneğin, Button ile önceki örneği kullanırsak:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button(onClick = { /* do something */ }, interactionSource = interactionSource) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Basma işlemi aynı karede başlayıp biterse metin hiçbir zaman "Basıldı!" olarak gösterilmez. Çoğu durumda bu bir sorun değildir. Bu kadar kısa bir süre için görsel efekt göstermek titremeye neden olur ve kullanıcı tarafından pek fark edilmez. Dalgalanma efekti veya benzer bir animasyon gösterme gibi bazı durumlarda, düğmeye basılmadığında efekti hemen durdurmak yerine en azından minimum bir süre boyunca göstermek isteyebilirsiniz. Bunun için bir duruma yazmak yerine animasyonları doğrudan collect lambda'nın içinden başlatıp durdurabilirsiniz. Bu kalıbın bir örneğini Animasyonlu kenarlığa sahip gelişmiş bir Indication oluşturma bölümünde bulabilirsiniz.
Örnek: Özel etkileşim işleme ile bileşen oluşturma
Girişe özel yanıt veren bileşenleri nasıl oluşturabileceğinizi görmek için değiştirilmiş bir düğme örneğini inceleyebilirsiniz. Bu durumda, basıldığında görünümünü değiştirerek yanıt veren bir düğme istediğinizi varsayalım:
Bunun için Button tabanlı özel bir composable oluşturun ve simgeyi (bu örnekte alışveriş sepeti) çizmek için ek bir icon parametresi almasını sağlayın. Kullanıcının fareyle düğmenin üzerine gelip gelmediğini izlemek için collectIsPressedAsState() yöntemini çağırırsınız. Kullanıcı fareyle düğmenin üzerine geldiğinde simgeyi 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'ın kullanımı ise şu şekildedir:
PressIconButton( onClick = {}, icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, text = { Text("Add to cart") } )
Bu yeni PressIconButton, mevcut Material Button üzerine inşa edildiğinden kullanıcı etkileşimlerine her zamanki gibi tepki verir. Kullanıcı düğmeye bastığında, tıpkı sıradan bir Material Button gibi opaklığı biraz değişir.
Indication ile yeniden kullanılabilir özel efekt oluşturma ve uygulama
Önceki bölümlerde, farklı Interaction'lere yanıt olarak bir bileşenin bir bölümünü nasıl değiştireceğinizi (ör. basıldığında simge gösterme) öğrendiniz. Aynı yaklaşım, bir bileşene sağladığınız parametrelerin değerini değiştirmek veya bir bileşenin içinde görüntülenen içeriği değiştirmek için de kullanılabilir ancak bu yalnızca bileşen bazında geçerlidir. Genellikle bir uygulama veya tasarım sisteminde, durum bilgisi olan görsel efektler için genel bir sistem bulunur. Bu efekt, tüm bileşenlere tutarlı bir şekilde uygulanmalıdır.
Bu tür bir tasarım sistemi oluşturuyorsanız bir bileşeni özelleştirmek ve bu özelleştirmeyi diğer bileşenler için yeniden kullanmak aşağıdaki nedenlerden dolayı zor olabilir:
- Tasarım sistemindeki her bileşen aynı standart metne ihtiyaç duyar.
- Bu efekti yeni oluşturulan bileşenlere ve özel tıklanabilir bileşenlere uygulamayı unutmak kolaydır.
- Ö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, bir uygulama veya tasarım sistemindeki bileşenlere uygulanabilen, yeniden kullanılabilir bir görsel efekti temsil eder. Indication iki bölüme ayrılmıştır:
IndicationNodeFactory: Bir bileşen için görsel efektler oluşturanModifier.Nodeörnekleri oluşturan bir fabrika. Bileşenler arasında değişmeyen daha basit uygulamalar için bu, tekil (nesne) olabilir ve uygulamanın tamamında yeniden kullanılabilir.Bu örnekler durum bilgisi olan veya olmayan türde olabilir. Bileşen başına oluşturuldukları için, diğer
Modifier.Node'lerde olduğu gibi belirli bir bileşen içindeki görünüm veya davranışlarını değiştirmek içinCompositionLocal'den değer alabilirler.Modifier.indication: Bir bileşen içinIndicationçizen bir değiştirici.Modifier.clickableve diğer üst düzey etkileşim değiştiriciler, doğrudan bir gösterge parametresi kabul eder. Bu sayede yalnızcaInteractionyaymakla kalmaz, yaydıklarıInteraction'ler için görsel efektler de çizebilirler. Bu nedenle, basit durumlardaModifier.indicationkullanmanıza gerek kalmadanModifier.clickablekullanabilirsiniz.
Efekti Indication ile değiştir
Bu bölümde, belirli bir düğmeye uygulanan manuel ölçek efektinin, birden fazla bileşende yeniden kullanılabilen bir gösterge eşdeğeriyle nasıl değiştirileceği açıklanmaktadır.
Aşağıdaki kod, basıldığında küçülen 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'te yer alan ölçek efektini Indication öğesine dönüştürmek için aşağıdaki adımları uygulayın:
Ölçek efektini uygulamaktan sorumlu
Modifier.Nodeoluşturun. Düğüm, eklendiğinde önceki örneklere benzer şekilde etkileşim kaynağını gözlemler. Buradaki tek fark, gelen etkileşimleri duruma dönüştürmek yerine doğrudan animasyonları başlatmasıdır.Düğümün
DrawModifierNode'yi uygulaması gerekir. BöyleceContentDrawScope#draw()'yi geçersiz kılabilir ve Compose'daki diğer grafik API'leriyle aynı çizim komutlarını kullanarak ölçek efekti oluşturabilir.ContentDrawScopealıcısından kullanılabilendrawContent()çağrısı,Indicationuygulanması gereken gerçek bileşeni çizer. Bu nedenle, bu işlevi yalnızca bir ölçek dönüşümü içinde çağırmanız gerekir.Indicationuygulamalarınızın her zaman bir noktadadrawContent()yöntemini çağırdığından emin olun. Aksi takdirde,Indicationuyguladığı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() } } }
IndicationNodeFactoryoluşturun. Tek sorumluluğu, sağlanan bir etkileşim kaynağı için yeni bir düğüm örneği oluşturmaktır. Göstergeyi yapılandıracak parametre olmadığından 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 }
Modifier.clickable, dahili olarakModifier.indicationkullanır. Bu nedenle,ScaleIndicationile tıklanabilir bir bileşen oluşturmak içinIndicationparametresiniclickableparametresine sağlamanız yeterlidir:Box( modifier = Modifier .size(100.dp) .clickable( onClick = {}, indication = ScaleIndication, interactionSource = null ) .background(Color.Blue), contentAlignment = Alignment.Center ) { Text("Hello!", color = Color.White) }
Bu sayede, özel bir
Indicationkullanarak üst düzey, yeniden kullanılabilir bileşenler oluşturmak da kolaylaşır. Örneğin, 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!") }
Indication ile oluşturulmuş bir düğme.Animasyonlu kenarlığa sahip gelişmiş bir Indication oluşturma
Indication, yalnızca bir bileşeni ölçeklendirme gibi dönüşüm efektleriyle sınırlı değildir. IndicationNodeFactory, bir Modifier.Node döndürdüğü için diğer çizim API'lerinde olduğu gibi içeriğin üstüne veya altına her türlü efekti çizebilirsiniz. Örneğin, bileşenin etrafına animasyonlu bir kenarlık ve bileşene basıldığında bileşenin üzerine bir yer paylaşımı çizebilirsiniz:
Indication ile çizilmiş animasyonlu kenarlık efekti.Buradaki Indication uygulaması, önceki örneğe çok benzer. Yalnızca bazı parametrelerle bir düğüm oluşturur. Animasyonlu kenarlık, Indication öğesinin kullanıldığı bileşenin şekline ve kenarlığına bağlı olduğundan Indication uygulamasında şekil ve kenarlık genişliğinin de parametre olarak sağlanması gerekir:
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 ) } }
Çizim kodu daha karmaşık olsa bile Modifier.Node uygulaması da kavramsal olarak aynıdır. Daha önce olduğu gibi, eklendiğinde InteractionSource öğesini gözlemler, animasyonları başlatır ve efekti içeriğin üzerine çizmek için DrawModifierNode öğesini uygular:
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, animateToResting() işleviyle animasyon için artık minimum bir süre olmasıdır. Bu sayede, basma işlemi hemen bırakılsa bile basma animasyonu devam eder. animateToPressed'ın başında birden fazla hızlı basma işlemi de işlenir. Mevcut bir basma veya bekleme animasyonu sırasında basma işlemi gerçekleşirse önceki animasyon iptal edilir ve basma animasyonu baştan başlar. Aynı anda birden fazla efektin (ör. yeni bir dalga animasyonunun diğer dalgaların üzerine çizileceği dalgalar) desteklenmesi için mevcut animasyonları iptal edip yenilerini başlatmak yerine animasyonları bir listede takip edebilirsiniz.
Sizin için önerilenler
- Not: JavaScript devre dışı bırakıldığında bağlantı metni gösterilir.
- Hareketleri anlama
- Jetpack Compose için Kotlin
- Materyal bileşenleri ve düzenleri