Bir uygulamada hareketleri işlemeyle ilgili çalışırken anlaşılması gereken birkaç terim ve kavram vardır. Bu sayfada işaretçiler, işaretçi etkinlikleri ve hareketler terimleri açıklanmakta ve hareketler için farklı soyutlama düzeyleri tanıtılmaktadır. Ayrıca etkinlik tüketimi ve yayılımı hakkında daha ayrıntılı bilgi edineceksiniz.
Tanımlar
Bu sayfadaki çeşitli kavramları anlamak için kullanılan bazı terimleri bilmeniz gerekir:
- İşaretçi: Uygulamanızla etkileşim kurmak için kullanabileceğiniz fiziksel bir nesnedir.
Mobil cihazlarda en yaygın işaretçi, dokunmatik ekranla etkileşim kuran parmağınızdır. Alternatif olarak, parmağınız yerine ekran kalemi kullanabilirsiniz.
Büyük ekranlarda, ekranla dolaylı olarak etkileşim kurmak için fare veya dokunmatik yüzey kullanabilirsiniz. Bir giriş cihazının işaretçi olarak kabul edilebilmesi için bir koordinatı "işaret edebilmesi" gerekir. Bu nedenle, örneğin klavye işaretçi olarak kabul edilemez. Compose'da işaretçi türü,
PointerTypekullanılarak işaretçi değişikliklerine dahil edilir. - İşaretçi etkinliği: Bir veya daha fazla işaretçinin belirli bir zamanda uygulamayla olan düşük düzeyli etkileşimini açıklar. Ekrana parmakla dokunma veya fareyi sürükleme gibi her türlü işaretçi etkileşimi bir etkinlik tetikler. Compose'da, bu tür bir etkinlikle ilgili tüm bilgiler
PointerEventsınıfında yer alır. - Hareket: Tek bir işlem olarak yorumlanabilecek bir dizi işaretçi etkinliği. Örneğin, dokunma hareketi, aşağı etkinliğinin ardından yukarı etkinliğinin geldiği bir sıra olarak kabul edilebilir. Birçok uygulama tarafından kullanılan dokunma, sürükleme veya dönüştürme gibi yaygın hareketler vardır ancak gerektiğinde kendi özel hareketinizi de oluşturabilirsiniz.
Farklı soyutlama düzeyleri
Jetpack Compose, hareketleri işlemek için farklı soyutlama seviyeleri sunar.
En üst düzeyde bileşen desteği bulunur. Button gibi composable'lar, hareket desteğini otomatik olarak içerir. Özel bileşenlere hareket desteği eklemek için rastgele composable'lara clickable gibi hareket değiştiriciler ekleyebilirsiniz. Son olarak, özel bir harekete ihtiyacınız varsa pointerInput değiştiricisini kullanabilirsiniz.
Genel kural olarak, ihtiyacınız olan işlevleri sunan en yüksek soyutlama düzeyini temel alın. Böylece katmanda yer alan en iyi uygulamalardan yararlanabilirsiniz. Örneğin, Button, erişilebilirlik için kullanılan daha fazla semantik bilgi içerir. clickable ise ham pointerInput uygulamasından daha fazla bilgi içerir.
Bileşen desteği
Compose'daki birçok kullanıma hazır bileşen, bir tür dahili hareket işleme içerir. Örneğin, bir LazyColumn, içeriğini kaydırarak sürükleme hareketlerine yanıt verir, bir Button, üzerine bastığınızda bir dalga gösterir ve SwipeToDismiss bileşeni, bir öğeyi kapatmak için kaydırma mantığı içerir. Bu tür hareket işleme otomatik olarak çalışır.
Dahili hareket işleme özelliğinin yanı sıra birçok bileşen, arayanın hareketi işlemesini de gerektirir. Örneğin, Button dokunmaları otomatik olarak algılar ve bir tıklama etkinliğini tetikler. Harekete tepki vermek için Button öğesine bir onClick lambda iletirsiniz. Benzer şekilde, kullanıcının kaydırma çubuğu tutma yerini sürüklemesine yanıt vermek için Slider öğesine bir onValueChange lambda eklersiniz.
Kullanım alanınıza uygun olduğunda, bileşenlere dahil edilen hareketleri tercih edin. Bu hareketler, odaklanma ve erişilebilirlik için kullanıma hazır destek içerir ve kapsamlı bir şekilde test edilmiştir. Örneğin, Button özel bir şekilde işaretlenir. Böylece erişilebilirlik hizmetleri, bunu herhangi bir tıklanabilir öğe olarak değil, düğme olarak doğru şekilde tanımlar:
// Talkback: "Click me!, Button, double tap to activate" Button(onClick = { /* TODO */ }) { Text("Click me!") } // Talkback: "Click me!, double tap to activate" Box(Modifier.clickable { /* TODO */ }) { Text("Click me!") }
Compose'da erişilebilirlik hakkında daha fazla bilgi edinmek için Compose'da erişilebilirlik başlıklı makaleyi inceleyin.
Değiştiricilerle rastgele composable'lara belirli hareketler ekleme
Hareket değiştiricileri herhangi bir composable'a uygulayarak composable'ın hareketleri dinlemesini sağlayabilirsiniz. Örneğin, genel bir Box öğesini clickable yaparak dokunma hareketlerini işlemesini sağlayabilir veya verticalScroll uygulayarak bir Column öğesinin dikey kaydırma işlemini yapmasını sağlayabilirsiniz.
Farklı hareket türlerini işlemek için birçok değiştirici vardır:
clickable,combinedClickable,selectable,toggleablevetriStateToggleabledeğiştiricileriyle dokunma ve basma işlemlerini yönetin.horizontalScroll,verticalScrollve daha genelscrollabledeğiştiricileriyle kaydırma işlemini yönetmedraggableveswipeabledeğiştiricisiyle sürüklemeyi yönetme.transformabledeğiştiricisiyle kaydırma, döndürme ve yakınlaştırma gibi çoklu dokunma hareketlerini işleyin.
Kural olarak, özel hareket işleme yerine hazır hareket değiştiricileri tercih edin.
Değiştiriciler, saf işaretçi etkinlik işleme özelliğinin üzerine daha fazla işlev ekler.
Örneğin, clickable değiştiricisi yalnızca basma ve dokunma işlemlerinin algılanmasını sağlamakla kalmaz, aynı zamanda semantik bilgiler, etkileşimlerle ilgili görsel göstergeler, fareyle üzerine gelme, odaklanma ve klavye desteği de ekler. İşlevin nasıl eklendiğini görmek için clickable uygulamasının kaynak kodunu inceleyebilirsiniz.
pointerInput değiştiricisiyle rastgele composable'lara özel hareket ekleme
Her hareket, hazır bir hareket değiştiriciyle uygulanmaz. Örneğin, uzun basma, kontrol tuşuna basarak tıklama veya üç parmakla dokunma işleminden sonra sürükleme işlemine tepki vermek için değiştirici kullanamazsınız. Bunun yerine, bu özel hareketleri tanımlamak için kendi hareket işleyicinizi yazabilirsiniz. Ham işaretçi etkinliklerine erişmenizi sağlayan pointerInput değiştiricisiyle bir hareket işleyici oluşturabilirsiniz.
Aşağıdaki kod, ham işaretçi etkinliklerini dinler:
@Composable private fun LogPointerEvents(filter: PointerEventType? = null) { var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(filter) { awaitPointerEventScope { while (true) { val event = awaitPointerEvent() // handle pointer event if (filter == null || event.type == filter) { log = "${event.type}, ${event.changes.first().position}" } } } } ) } }
Bu snippet'i parçalara ayırırsanız temel bileşenler şunlardır:
pointerInputdeğiştiricisi. Bir veya daha fazla anahtar iletirsiniz. Bu anahtarlardan birinin değeri değiştiğinde değiştirici içerik lambda'sı yeniden yürütülür. Örnek, composable'a isteğe bağlı bir filtre iletir. Bu filtrenin değeri değişirse doğru etkinliklerin günlüğe kaydedildiğinden emin olmak için işaretçi etkinlik işleyici yeniden yürütülmelidir.awaitPointerEventScope, işaretçi etkinliklerini beklemek için kullanılabilecek bir eş yordam kapsamı oluşturur.awaitPointerEvent, bir sonraki işaretçi etkinliği gerçekleşene kadar eş yordamı askıya alır.
Ham giriş etkinliklerini dinlemek güçlü bir yöntem olsa da bu ham verilere dayalı özel bir hareket yazmak da karmaşıktır. Özel hareketlerin oluşturulmasını basitleştirmek için birçok yardımcı yöntem mevcuttur.
Tüm hareketleri algılama
Ham işaretçi etkinliklerini işlemek yerine belirli hareketlerin gerçekleşmesini dinleyebilir ve uygun şekilde yanıt verebilirsiniz. AwaitPointerEventScope, aşağıdakileri dinlemek için yöntemler sağlar:
- Basma, dokunma, iki kez dokunma ve uzun basma:
detectTapGestures - Sürüklemeler:
detectHorizontalDragGestures,detectVerticalDragGestures,detectDragGesturesvedetectDragGesturesAfterLongPress - Dönüşümler:
detectTransformGestures
Bunlar üst düzey algılayıcılardır. Bu nedenle, bir pointerInput değiştiricisine birden fazla algılayıcı ekleyemezsiniz. Aşağıdaki snippet yalnızca dokunmaları algılar, sürüklemeleri algılamaz:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } // Never reached detectDragGestures { _, _ -> log = "Dragging" } } ) }
Dahili olarak detectTapGestures yöntemi, eş yordamı engeller ve ikinci algılayıcıya hiçbir zaman ulaşılmaz. Bir composable'a birden fazla hareket dinleyici eklemeniz gerekiyorsa bunun yerine ayrı pointerInput değiştirici örnekleri kullanın:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } } .pointerInput(Unit) { // These drag events will correctly be triggered detectDragGestures { _, _ -> log = "Dragging" } } ) }
Hareket başına etkinlikleri yönetme
Tanım gereği, hareketler bir işaretçi basma etkinliğiyle başlar. Her ham etkinlikten geçen while(true) döngüsü yerine awaitEachGesture yardımcı yöntemini kullanabilirsiniz. awaitEachGesture yöntemi, tüm işaretçiler kaldırıldığında kapsayıcı bloğu yeniden başlatarak hareketin tamamlandığını gösterir:
@Composable private fun SimpleClickable(onClick: () -> Unit) { Box( Modifier .size(100.dp) .pointerInput(onClick) { awaitEachGesture { awaitFirstDown().also { it.consume() } val up = waitForUpOrCancellation() if (up != null) { up.consume() onClick() } } } ) }
Uygulamada, hareketleri tanımlamadan işaretçi etkinliklerine yanıt vermediğiniz sürece neredeyse her zaman awaitEachGesture kullanmak istersiniz. Buna örnek olarak, işaretçinin aşağı veya yukarı hareketlerine yanıt vermeyen, yalnızca işaretçinin sınırlarına ne zaman girdiğini veya çıktığını bilmesi gereken hoverable verilebilir.
Belirli bir etkinliği veya alt hareketi bekleme
Hareketlerin ortak kısımlarını belirlemeye yardımcı olan bir dizi yöntem vardır:
awaitFirstDownile bir işaretçi aşağı gidene kadar askıya alın veyawaitForUpOrCancellationile tüm işaretçilerin yukarı gitmesini bekleyin.awaitTouchSlopOrCancellationveawaitDragOrCancellationkullanarak alt düzey bir sürükleme dinleyicisi oluşturun. Hareket işleyici, işaretçi dokunma eşiğine ulaşana kadar askıya alınır ve ardından ilk sürükleme etkinliği gerçekleşene kadar askıya alınır. Yalnızca tek bir eksen boyunca sürüklemelerle ilgileniyorsanızawaitHorizontalTouchSlopOrCancellationveawaitHorizontalDragOrCancellationya daawaitVerticalTouchSlopOrCancellationveawaitVerticalDragOrCancellationkullanın.awaitLongPressOrCancellationile uzun basma işlemi gerçekleşene kadar askıya alın.- Sürükleme etkinliklerini sürekli olarak dinlemek için
dragyöntemini, tek bir eksendeki sürükleme etkinliklerini dinlemek için isehorizontalDragveyaverticalDragyöntemini kullanın.
Çok noktalı etkinlikler için hesaplamaları uygulama
Kullanıcı birden fazla işaretçi kullanarak çoklu dokunma hareketi yaparken ham değerlere göre gerekli dönüşümü anlamak karmaşıktır.
transformable değiştiricisi veya detectTransformGestures yöntemleri kullanım alanınız için yeterince ayrıntılı kontrol sağlamıyorsa ham etkinlikleri dinleyebilir ve bunlara hesaplamalar uygulayabilirsiniz. Bu yardımcı yöntemler şunlardır: calculateCentroid, calculateCentroidSize, calculatePan, calculateRotation ve calculateZoom.
Etkinlik gönderme ve isabet testi
Her işaretçi etkinliği her pointerInput değiştiriciye gönderilmez. Etkinlik gönderme işlemi şu şekilde çalışır:
- İşaretçi etkinlikleri, oluşturulabilir hiyerarşiye gönderilir. Yeni bir işaretçi ilk işaretçi etkinliğini tetiklediği anda sistem, "uygun" composable'lar için isabet testi yapmaya başlar. İşaretçi girişi işleme özelliklerine sahip olan composable'lar uygun kabul edilir. İsabet testi, kullanıcı arayüzü ağacının üst kısmından alt kısmına doğru ilerler. İşaretçi etkinliği, composable'ın sınırları içinde gerçekleştiğinde composable "isabet alır". Bu işlem, isabet testini olumlu sonuçlandıran bir oluşturulabilir öğe zinciri ile sonuçlanır.
- Varsayılan olarak, ağacın aynı seviyesinde birden fazla uygun composable olduğunda yalnızca en yüksek z-endeksine sahip composable "isabet alır". Örneğin, bir
Boxöğesine çakışan ikiButtonbileşeni eklediğinizde yalnızca üstte çizilen bileşen işaretçi etkinliklerini alır. Teorik olarak kendiPointerInputModifierNodeuygulamanızı oluşturupsharePointerInputWithSiblingsdeğerini true olarak ayarlayarak bu davranışı geçersiz kılabilirsiniz. - Aynı işaretçi için diğer etkinlikler aynı composable zincirine gönderilir ve etkinlik yayma mantığına göre akar. Sistem, bu işaretçi için başka isabet testi yapmaz. Bu, zincirdeki her composable öğenin, söz konusu composable öğenin sınırları dışında gerçekleşse bile söz konusu işaretçiyle ilgili tüm etkinlikleri aldığı anlamına gelir. Zincirde olmayan composable'lar, işaretçi sınırları içinde olsa bile hiçbir zaman işaretçi etkinlikleri almaz.
Fare veya ekran kalemiyle üzerine gelindiğinde tetiklenen fareyle üzerine gelme etkinlikleri, burada tanımlanan kuralların istisnasıdır. Fareyle üzerine gelme etkinlikleri, isabet ettikleri tüm composable'lara gönderilir. Bu nedenle, kullanıcı işaretçiyi bir composable'ın sınırlarından diğerine getirdiğinde etkinlikler ilk composable'a değil, yeni composable'a gönderilir.
Etkinlik tüketimi
Birden fazla composable'a hareket işleyici atandığında bu işleyiciler çakışmamalıdır. Örneğin, şu kullanıcı arayüzüne bakın:
Kullanıcı yer işareti düğmesine dokunduğunda düğmenin onClick lambda'sı bu hareketi işler. Kullanıcı, liste öğesinin başka bir bölümüne dokunduğunda ListItem bu hareketi işler ve makaleye gider. İşaretçi girişi açısından, Button'ın bu etkinliği kullanması gerekir. Böylece üst öğesi, artık bu etkinliğe tepki vermemesi gerektiğini bilir. Kullanıma hazır bileşenlerde ve yaygın hareket değiştiricilerde bulunan hareketler bu tüketim davranışını içerir ancak kendi özel hareketinizi yazıyorsanız etkinlikleri manuel olarak tüketmeniz gerekir. Bunu PointerInputChange.consume yöntemiyle
yapabilirsiniz:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() // consume all changes event.changes.forEach { it.consume() } } } }
Bir etkinliğin kullanılması, etkinliğin diğer composable'lara yayılmasını durdurmaz. Bunun yerine, bir composable'ın tüketilen etkinlikleri açıkça yoksayması gerekir. Özel hareketler yazarken bir etkinliğin başka bir öğe tarafından kullanılıp kullanılmadığını kontrol etmeniz gerekir:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() if (event.changes.any { it.isConsumed }) { // A pointer is consumed by another gesture handler } else { // Handle unconsumed event } } } }
Etkinlik yayılımı
Daha önce de belirtildiği gibi, işaretçi değişiklikleri isabet ettiği her composable'a iletilir.
Ancak bu tür birden fazla composable varsa etkinlikler hangi sırayla yayılır? Son bölümdeki örneği ele alırsak bu kullanıcı arayüzü, yalnızca ListItem ve Button öğelerinin işaretçi etkinliklerine yanıt verdiği aşağıdaki kullanıcı arayüzü ağacına çevrilir:
İşaretçi etkinlikleri, üç "geçiş" sırasında bu composable'ların her birinden üç kez geçer:
- İlk geçişte etkinlik, kullanıcı arayüzü ağacının üst kısmından alt kısmına doğru akar. Bu akış, ebeveynlerin bir etkinliği çocuktan önce görmesine olanak tanır. Örneğin, ipuçlarının uzun basma işlemini alt öğelerine iletmek yerine kesmesi gerekir. Örneğimizde
ListItem, etkinliğiButton'den önce alır. - Ana geçişte etkinlik, kullanıcı arayüzü ağacının yaprak düğümlerinden kullanıcı arayüzü ağacının köküne doğru akar. Bu aşama, normalde hareketleri kullandığınız aşamadır ve etkinlikleri dinlerken varsayılan geçiştir. Hareketlerin bu aşamada işlenmesi, yaprak düğümlerin üst düğümlerine göre öncelikli olduğu anlamına gelir. Bu, çoğu hareket için en mantıklı davranıştır. Örneğimizde,
ButtonetkinliğiListItem'den önce alır. - Son aşamada etkinlik, kullanıcı arayüzü ağacının üst kısmından yaprak düğümlere doğru bir kez daha akar. Bu akış, yığının üst kısımlarındaki öğelerin, üst öğeleri tarafından etkinlik tüketimine yanıt vermesine olanak tanır. Örneğin, bir düğmeye basıldığında kaydırılabilir üst öğesi sürüklenirse düğmenin dalga göstergesi kaldırılır.
Görsel olarak, etkinlik akışı aşağıdaki gibi gösterilebilir:
Bir giriş değişikliği kullanıldıktan sonra bu bilgi, akışta o noktadan itibaren iletilir:
Kodda, ilgilendiğiniz kartı belirtebilirsiniz:
Modifier.pointerInput(Unit) { awaitPointerEventScope { val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial) val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final) } }
Bu kod snippet'inde, tüketimle ilgili veriler değişmiş olsa bile bu await yöntem çağrılarının her biri aynı etkinliği döndürür.
Hareketleri test etme
Test yöntemlerinizde, performTouchInput yöntemini kullanarak işaretçi etkinliklerini manuel olarak gönderebilirsiniz. Bu sayede, daha üst düzey tam hareketleri (ör. sıkıştırma veya uzun tıklama) ya da alt düzey hareketleri (ör. imleci belirli bir piksel miktarı kadar hareket ettirme) gerçekleştirebilirsiniz:
composeTestRule.onNodeWithTag("MyList").performTouchInput { swipeUp() swipeDown() click() }
Daha fazla örnek için performTouchInput belgelerini inceleyin.
Daha fazla bilgi
Jetpack Compose'daki hareketler hakkında daha fazla bilgiyi aşağıdaki kaynaklardan edinebilirsiniz:
Sizin için önerilenler
- Not: JavaScript devre dışı bırakıldığında bağlantı metni gösterilir.
- Compose'da erişilebilirlik
- Kaydırma
- Dokunma ve basma