Kullanıcı arayüzü bileşenleri, kullanıcı etkileşimlerine yanıt verme şekillerine göre cihaz kullanıcısına geri bildirim verir. Her bileşenin, etkileşimlere yanıt vermek için kendi yöntemi vardır. Bu, kullanıcının etkileşimlerinin ne yaptığını bilmesine yardımcı olur. Örneğin, kullanıcı cihazın dokunmatik ekranındaki bir düğmeye dokunursa düğme, muhtemelen bir vurgu rengi ekleyerek bir şekilde değişecek. Bu değişiklik, kullanıcının düğmeye dokunduğunu bilmesini sağlar. Kullanıcı bunu yapmak istemediyse, serbest bırakmadan önce parmağınızı düğmeden uzağa sürüklemesi gerektiğini bilir, aksi takdirde düğme etkinleşir.
Oluşturma Hareketleri dokümanında, Oluşturma bileşenlerinin işaretçi hareketleri ve tıklamalar gibi düşük seviyeli işaretçi etkinliklerini nasıl işlediği ele alınmaktadır. Compose, kullanıma hazır olduğunda bu alt düzey etkinlikleri daha üst düzey etkileşimlere soyutlar. Örneğin, bir dizi işaretçi etkinliği bir düğmeye basıp serbest bırakma işlevine dönüşebilir. Bu üst düzey soyutlamaları anlamak, kullanıcı arayüzünüzün kullanıcıya nasıl tepki verdiğini özelleştirmenize yardımcı olabilir. Örneğin, kullanıcı etkileşimde bulunduğunda bileşenin görünümünün nasıl değiştiğini özelleştirmek veya bu kullanıcı işlemlerinin bir günlüğünü tutmak isteyebilirsiniz. Bu belgede, standart kullanıcı arayüzü öğelerini değiştirmek veya kendi arayüzünüzü tasarlamak için gereken bilgileri bulabilirsiniz.
Etkileşimler
Çoğu durumda, yalnızca Oluşturma bileşeninizin kullanıcı etkileşimlerini nasıl yorumladığını bilmenize gerek yoktur. Örneğin Button
, kullanıcının düğmeyi tıklayıp tıklamadığını belirlemek için Modifier.clickable
aracından yararlanı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. Diğer bir deyişle, kullanıcının ekrana dokunup dokunmadığını veya klavyeyle düğmeyi seçip seçmediğini bilmeniz gerekmez. Modifier.clickable
, kullanıcının bir 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 yanıtını özelleştirmek isterseniz, arka planda neler olup bittiğini daha iyi bilmeniz gerekebilir. Bu bölümde bu bilgilerden bazıları gösterilmektedir.
Bir kullanıcı bir kullanıcı arayüzü bileşeniyle etkileşimde bulunduğunda, sistem bir dizi Interaction
etkinliği oluşturarak bu 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 bir PressInteraction.Release
oluşturur ve düğmenin tıklamanın tamamlandığını bilmesini sağlar. Öte yandan, kullanıcı parmağını düğmenin dışına sürükler ve ardından parmağını kaldırırsa düğme, düğmeye basmanın iptal edildiğini, tamamlanmadığını belirtmek için PressInteraction.Cancel
değerini oluşturur.
Bu etkileşimler fikirsizdir. Yani bu alt düzey etkileşim etkinlikleri, kullanıcı işlemlerinin anlamını veya sırasını yorumlamaz. Ayrıca hangi kullanıcı işlemlerinin diğer işlemlere göre öncelikli olabileceğini de yorumlamaz.
Bu etkileşimler genellikle başlangıç ve bitiş noktalarıyla çiftler halinde gerçekleşir. İkinci etkileşim, ilkine bir referans içerir. Örneğin, kullanıcı bir düğmeye dokunduktan sonra parmağını kaldırırsa, dokunulduğunda bir PressInteraction.Press
etkileşimi oluşturulur ve sürüm, bir PressInteraction.Release
oluşturur; Release
, ilk PressInteraction.Press
kodunu tanımlayan press
özelliğine sahiptir.
Belirli bir bileşenin InteractionSource
bölümünü inceleyerek bu bileşenin etkileşimlerini görebilirsiniz. InteractionSource
, Kotlin akışlarının temeline dayandığından, bu akışlardan etkileşimleri diğer akışlarda olduğu gibi toplayabilirsiniz. Bu tasarım kararı hakkında daha fazla bilgi için Etkileşimleri Aydınlatma blog yayınına bakın.
Etkileşim durumu
Etkileşimleri kendiniz de izleyerek bileşenlerinizin yerleşik işlevlerini genişletmek isteyebilirsiniz. Örneğin, bir düğmeye basıldığında rengi
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 ilgili 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()
'in yanı sıra collectIsFocusedAsState()
, collectIsDraggedAsState()
ve collectIsHoveredAsState()
özelliklerini de sağlar. Bu yöntemler aslında alt düzey InteractionSource
API'lerinin üzerine inşa edilen pratik yöntemlerdir. Bazı durumlarda bu alt düzey işlevleri
doğrudan kullanmak isteyebilirsiniz.
Örneğin, bir düğmeye basılıp basılmadığını ve ayrıca 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ş çıkarır ve tüm etkileşimleri doğru sırayla elde edeceğinizin garantisi yoktur. Bu gibi durumlarda, doğrudan InteractionSource
ile çalışmak isteyebilirsiniz. InteractionSource
ile etkileşimleri kendiniz izleme hakkında daha fazla bilgi edinmek için InteractionSource
ile çalışma sayfasını inceleyin.
Aşağıdaki bölümde sırasıyla InteractionSource
ve MutableInteractionSource
ile etkileşimlerin nasıl kullanılacağı ve yayınlanacağı açıklanmaktadır.
Tüketim ve yayma: Interaction
InteractionSource
, Interactions
öğesinin salt okunur akışını temsil eder; InteractionSource
öğesine Interaction
gönderilmesi mümkün değildir. Interaction
yaymak için InteractionSource
ile başlayan bir MutableInteractionSource
kullanmanız gerekir.
Değiştiriciler ve bileşenler, Interactions
öğesini tüketebilir, yayabilir veya tüketip yayabilir.
Aşağıdaki bölümlerde, hem değiştiricilerden hem de bileşenlerden gelen etkileşimlerin nasıl kullanılacağı ve yayınlanacağı açıklanmaktadır.
Tüketim değiştirici örneği
Odaklanılmış durum için kenarlık çizen bir düzenleyicide yalnızca Interactions
gözlemlemeniz gerekir. Böylece bir InteractionSource
kabul edilir:
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
İşlev imzasından bu değiştiricinin bir tüketici olduğu açıktır. Interaction
öğeleri tüketebilir ancak üretemez.
Değiştirici oluşturma örneği
Modifier.hoverable
gibi fareyle üzerine gelme etkinliklerini işleyen bir değiştirici için Interactions
yayınlamanız ve bunun yerine parametre olarak MutableInteractionSource
kabul etmeniz gerekir:
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
Bu değiştirici bir üreticidir. Üzerine gelindiğinde veya fareyle üzerine gelindiğinde HoverInteractions
yayınlanması için sağlanan MutableInteractionSource
değerini kullanabilir.
Bunları tüketen ve üreten bileşenler oluşturun
Malzeme Button
gibi üst düzey bileşenler hem üretici hem de tüketici işlevi görür. Giriş ve odak etkinliklerini yönetir ve ayrıca bu etkinliklere yanıt olarak (ör. dalga göstermek veya yüksekliklerini canlandırmak) görünümlerini değiştirirler. Bunun sonucunda, 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, MutableInteractionSource
öğesinin bileşenden kaldırılmasına ve bileşen tarafından üretilen tüm Interaction
'lerin gözlemlenmesine olanak tanır. Bu bileşenin veya kullanıcı arayüzünüzdeki başka bir bileşenin görünümünü kontrol etmek için bunu kullanabilirsiniz.
Kendi etkileşimli üst düzey bileşenlerinizi oluşturuyorsanız MutableInteractionSource
öğesini bu şekilde parametre olarak göstermenizi öneririz. Bu, durum yükseltme en iyi uygulamalarını izlemenin yanı sıra, bir bileşenin görsel durumunu başka herhangi bir tür durumun (ör. etkin olma durumu) okunup kontrol edilebildiği gibi okumayı ve kontrol etmeyi de kolaylaştırır.
Compose, katmanlı bir mimari yaklaşımı uygular. Böylece üst düzey Material bileşenleri, dalgaları ve diğer görsel efektleri kontrol etmek için gereken Interaction
'leri üreten temel yapı taşlarının üzerine inşa edilir. Temel kitaplığı Modifier.hoverable
, Modifier.focusable
ve Modifier.draggable
gibi üst düzey etkileşim değiştiriciler sunar.
Fareyle öğelerin üzerine gelerek gerçekleşen etkinliklere yanıt veren bir bileşen oluşturmak için Modifier.hoverable
kullanabilir ve parametre olarak bir MutableInteractionSource
iletebilirsiniz.
Bileşenin üzerine gelindiğinde HoverInteraction
sn. yayar. Bileşenin görünümünü değiştirmek için bunu 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 odaklanılabilir hale getirmek için Modifier.focusable
öğesini ekleyebilir ve aynı MutableInteractionSource
öğesini parametre olarak iletebilirsiniz. Artık hem HoverInteraction.Enter/Exit
hem de FocusInteraction.Focus/Unfocus
aynı MutableInteractionSource
üzerinden yayınlanır ve her iki etkileşim türünün görünümünü aynı yerde ö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
soyutlamalarından daha üst düzey bir soyutlamadır. Bir bileşenin tıklanabilir olması için örtülü bir şekilde üzerine gelebilmesi ve tıklanabilen bileşenlere de odaklanılabilmesi gerekir. Alt düzey API'leri birleştirmeye gerek kalmadan fareyle üzerine gelme, odaklama ve basın etkileşimlerini işleyen bir bileşen oluşturmak için Modifier.clickable
kullanabilirsiniz. Bileşeninizi de tıklanabilir yapmak isterseniz 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ışın
Bir bileşenle olan etkileşimler hakkında alt düzey bilgilere ihtiyacınız varsa söz konusu bileşenin InteractionSource
öğesi için standart akış API'lerini kullanabilirsiniz.
Örneğin, bir InteractionSource
için basma ve sürükleme işlemlerinin listesini tutmak istediğinizi varsayalım. Bu kod işin yarısını yapar ve yeni baskıları
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şimleri eklemenin yanı sıra sona erdiğinde (örneğin, kullanıcı parmağını bileşenden geri çektiğinde) etkileşimleri de kaldırmanız gerekir. Son etkileşimler her zaman ilişkili başlangıç etkileşimine bir referans taşıdığından, 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) } } } }
Şimdi, bileşene o anda basıldığını veya sürüklendiğini öğrenmek istiyorsanız yapmanız gereken tek şey interactions
öğesinin boş olup olmadığını kontrol etmektir:
val isPressedOrDragged = interactions.isNotEmpty()
En son etkileşimin ne olduğunu öğrenmek istiyorsanız listedeki son öğeye bakın. Örneğin, Oluştur dalgası 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 Interaction
'ler aynı yapıyı izlediğinden farklı türde kullanıcı etkileşimi türleriyle çalışırken kodda pek bir fark yoktur ve genel kalıp aynıdır.
Bu bölümde yer alan önceki örneklerin, State
kullanan etkileşimlerin Flow
'sini temsil ettiğini unutmayın. Durum değerinin okunması otomatik olarak yeniden derlemelere neden olacağı için bu, güncellenmiş değerleri gözlemlemeyi kolaylaştırır. Ancak beste, çerçeveden önce toplu hale getirilir. Bu, durumun değişmesi ve ardından tekrar aynı çerçeve içinde değişmesi halinde durumu gözlemleyen bileşenlerin değişikliği görmeyeceği anlamına gelir.
Etkileşimler düzenli olarak aynı çerçeve içinde başlayıp bitebileceğinden, etkileşimler için bu önemlidir. Örneğin, önceki örneği Button
ile birlikte kullanarak:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button(onClick = { /* do something */ }, interactionSource = interactionSource) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Basın basınca aynı çerçeve içinde başlar ve biterse metin hiçbir zaman "Basıldı!" olarak görüntülenmez. Çoğu durumda bu bir sorun değildir. Görsel bir efektin bu kadar kısa bir süre için gösterilmesi titremesine yol açar ve kullanıcı tarafından çok fark edilmez. Dalga efekti veya benzer bir animasyon gösterme gibi bazı durumlarda, düğmeye artık basılmadığında hemen durmak yerine efekti en azından minimum bir süre boyunca göstermek isteyebilirsiniz. Bunu yapmak için bir duruma yazmak yerine animasyonları doğrudan Collect lambda'nın içinden başlatabilir ve durdurabilirsiniz. Bu kalıbın bir örneğini Animasyonlu kenarlıkla gelişmiş bir Indication
oluşturma bölümünde bulabilirsiniz.
Örnek: Özel etkileşim işlemeyle bileşen oluşturma
Giriş için özel bir yanıtla bileşenleri nasıl derleyebileceğinizi görmek için değiştirilmiş düğme örneğini aşağıda bulabilirsiniz. Bu durumda, görünümü değiştirerek basmalara yanıt veren bir düğme istediğinizi varsayalım:
Bunu yapmak için Button
temelinde özel bir composable oluşturun ve simgeyi çizmek için ek bir icon
parametresi (bu örnekte bir alışveriş sepeti) almasını sağlayın. Kullanıcının düğmenin üzerine gelip gelmediğini takip etmek için collectIsPressedAsState()
yöntemini çağırırsınız. Düğmenin üzerine geldiğinde ise simge eklersiniz. Kod aşağıdaki gibi 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() } }
İşte yeni composable'ı kullanmak şöyle:
PressIconButton( onClick = {}, icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, text = { Text("Add to cart") } )
Bu yeni PressIconButton
, mevcut MateryalButton
temel alınarak oluşturulduğundan, kullanıcı etkileşimlerine her zamanki gibi tepki verir. Kullanıcı düğmeye bastığında, sıradan bir Malzeme Button
olduğu gibi opaklığı da biraz değişir.
Indication
ile yeniden kullanılabilir bir özel efekt oluşturup uygulayın
Önceki bölümlerde, basıldığında bir simge gösterilmesi gibi farklı Interaction
öğelerine yanıt olarak bileşenin bir bölümünün nasıl değiştirileceğini öğrenmiştiniz. Aynı yaklaşım, bir bileşene sağladığınız parametrelerin değerini veya bir bileşen içinde görüntülenen içeriği değiştirmek için kullanılabilir ancak bu yalnızca her bileşen için geçerlidir. Çoğu zaman bir uygulama veya tasarım sistemi, durum bilgili görsel efektler için genel bir sisteme sahiptir. Bu efekt, tüm bileşenlere tutarlı bir şekilde uygulanması gerekir.
Bu tür bir tasarım sistemi oluşturuyorsanız aşağıdaki nedenlerle bir bileşeni özelleştirmek ve diğer bileşenler için bu özelleştirmeyi yeniden kullanmak zor olabilir:
- Tasarım sistemindeki tüm bileşenler için aynı ortak metin gereklidir
- Bu efekti yeni oluşturulan bileşenlere ve özel tıklanabilir bileşenlere uygulamak kolaydır.
- Özel efekti diğer efektlerle birleştirmek zor olabilir
Bu sorunları önlemek ve sisteminiz genelinde bir özel bileşeni 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ılır:
IndicationNodeFactory
: Bir bileşen için görsel efektler oluşturanModifier.Node
örnekleri oluşturan bir fabrikadır. Bileşenler arasında değişmeyen daha basit uygulamalar için bu, bir tekil (nesne) olabilir ve uygulamanın tamamında yeniden kullanılabilir.Bu örnekler durum bilgili veya durum bilgisiz olabilir. Bileşen başına oluşturulduklarından, diğer
Modifier.Node
öğelerinde olduğu gibi belirli bir bileşenin içinde görünme veya davranış biçimlerini değiştirmek için birCompositionLocal
öğesinden değerler alabilirler.Modifier.indication
: Bir bileşen içinIndication
çizen bir değiştirici.Modifier.clickable
ve diğer üst düzey etkileşim değiştiricileri, bir gösterge parametresini doğrudan kabul eder. Böylece, yalnızcaInteraction
yaymakla kalmaz, aynı zamanda saldıklarıInteraction
'lar için görsel efektler de oluşturabilirler. Yani basit durumlarda,Modifier.indication
eklentisine gerek kalmadanModifier.clickable
kullanabilirsiniz.
Efekti bir Indication
ile değiştir
Bu bölümde, belirli bir düğmeye uygulanan manuel ölçeklendirme efektinin, birden fazla bileşende yeniden kullanılabilecek bir gösterim eşdeğeriyle nasıl değiştirileceği açıklanmaktadır.
Aşağıdaki kod, basıldığında aşağı ölçeklenen 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 bulunan ölçek efektini Indication
biçimine dönüştürmek için şu adımları uygulayın:
Ölçek efektini uygulamadan sorumlu olan
Modifier.Node
öğesini oluşturun. Düğüm, bağlandığında önceki örneklere benzer şekilde etkileşim kaynağını gözlemler. Buradaki tek fark, gelen Etkileşimleri duruma dönüştürmek yerine animasyonları doğrudan başlatmasıdır.Düğümün,
ContentDrawScope#draw()
değerini geçersiz kılabilmesi ve Oluşturma'daki diğer grafik API'leriyle aynı çizim komutlarını kullanarak ölçek efekti oluşturabilmesi içinDrawModifierNode
'i uygulaması gerekir.ContentDrawScope
alıcısından kullanılabilirdrawContent()
çağrısı,Indication
öğesinin uygulanması gereken asıl bileşeni çizer. Bu yüzden, bu işlevi bir ölçek dönüşümü içinde çağırmanız gerekir.Indication
uygulamalarınızın bir noktada her zamandrawContent()
çağrısı yaptığından emin olun. Aksi takdirde,Indication
kodunu 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() } } }
IndicationNodeFactory
oluşturun. Tek sorumluluğu, sağlanan etkileşim kaynağı için yeni bir düğüm örneği oluşturmaktır. Göstergeyi yapılandırmak için herhangi bir 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.indication
kullanır. Bu nedenleScaleIndication
ile tıklanabilir bir bileşen oluşturmak için tek yapmanız gerekenIndication
öğesiniclickable
öğesine parametre olarak sağlamaktır:Box( modifier = Modifier .size(100.dp) .clickable( onClick = {}, indication = ScaleIndication, interactionSource = null ) .background(Color.Blue), contentAlignment = Alignment.Center ) { Text("Hello!", color = Color.White) }
Bu, özel bir
Indication
kullanarak yeniden kullanılabilir üst düzey bileşenler oluşturmayı da kolaylaştırır. Düğme şuna benzeyebilir:@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 ) }
Daha sonra, düğmeyi aşağıdaki şekilde kullanabilirsiniz:
ScaleButton(onClick = {}) { Icon(Icons.Filled.ShoppingCart, "") Spacer(Modifier.padding(10.dp)) Text(text = "Add to cart!") }
Animasyonlu kenarlıkla gelişmiş bir Indication
oluşturun
Indication
, bileşeni ölçeklendirme gibi dönüştürme efektleriyle sınırlı değildir. IndicationNodeFactory
bir Modifier.Node
döndürdüğünden, diğer çizim API'lerinde olduğu gibi içeriğin üstüne veya altına herhangi bir efekt çizebilirsiniz. Örneğin, bileşenin etrafına animasyonlu bir kenarlık ve bileşen basıldığında üst kısmında bir yer paylaşımı çizebilirsiniz:
Buradaki Indication
uygulaması, önceki örneğe çok benzemektedir. Yalnızca bazı parametrelere sahip bir düğüm oluşturulur. Animasyonlu kenarlık, Indication
öğesinin kullanıldığı bileşenin şekline ve kenarlığına bağlı olduğundan, Indication
uygulaması şekil ve kenarlık genişliğinin parametre olarak sağlanmasını da gerektirir:
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
gözlemlenir, animasyonları başlatır ve içeriğin üzerine efekti çizmek için DrawModifierNode
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ürenin olmasıdır. Böylece, basın hemen serbest bırakılsa bile basın animasyonu devam eder. animateToPressed
öğesinin başında birden fazla hızlı basma işlemi de vardır. Mevcut bir basma veya bekleme animasyonu sırasında bir basma işlemi gerçekleşirse önceki animasyon iptal edilir ve basın animasyonu baştan başlar. Birden fazla eşzamanlı efekti desteklemek için (ör. yeni bir dalga animasyonu, diğer dalgaların üzerine yeni bir dalga animasyonu çizildiğinde), mevcut animasyonları iptal edip yenilerini başlatmak yerine animasyonları bir listede izleyebilirsiniz.
Sizin için önerilenler
- Not: Bağlantı metni JavaScript kapalıyken görüntülenir
- Hareketleri anlama
- Jetpack Compose için Kotlin
- Malzeme Bileşenleri ve düzenler