Hareketleri anlama

Bir uygulamada hareket işleme üzerinde ç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 da ayrıntılı bilgi veriyor.

Tanımlar

Bu sayfadaki çeşitli kavramları anlamak için kullanılan terminolojinin bazılarını anlamanız gerekir:

  • İşaretçi: Uygulamanızla etkileşimde bulunmak için kullanabileceğiniz bir fiziksel nesne. Mobil cihazlarda en yaygın işaretçi, dokunmatik ekranla etkileşimde bulunan parmağınızdır. Alternatif olarak parmağınızı ekran kalemiyle değiştirebilirsiniz. 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ı "doğrultus" özelliğini kullanabilmesi gerekir. Bu nedenle, örneğin bir klavyenin işaretçi olarak kabul edilmesi mümkün değildir. Oluşturma'da işaretçi türü, PointerType kullanılarak işaretçi değişikliklerine dahil edilir.
  • İşaretçi etkinliği: Belirli bir zamanda bir veya daha fazla işaretçinin uygulamayla alt düzey etkileşimini tanımlar. Parmağınızı ekrana koyma veya fareyi sürükleme gibi işaretçi etkileşimleri bir etkinliği tetikler. Compose'da bu tür bir etkinlikle alakalı tüm bilgiler PointerEvent sınıfında bulunur.
  • Hareket: Tek bir işlem olarak yorumlanabilen işaretçi etkinlikleri dizisi. Örneğin, dokunma hareketi bir aşağı etkinlik ve ardından bir yukarı etkinlik dizisi olarak kabul edilebilir. Dokunma, sürükleme veya dönüştürme gibi birçok uygulama tarafından kullanılan genel hareketler vardır, ancak gerektiğinde kendi özel hareketinizi de oluşturabilirsiniz.

Farklı soyutlama düzeyleri

Jetpack Compose, hareketleri işlemek için farklı düzeylerde soyutlama sağlar. Bileşen desteği en üst düzeydedir. Button gibi composable'lar otomatik olarak hareket desteği 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.

Kural olarak, ihtiyacınız olan işlevi sunan en üst düzeyde soyutlama oluşturun. Bu şekilde, katmanda yer alan en iyi uygulamalardan yararlanabilirsiniz. Örneğin, Button, ham pointerInput uygulamasından daha fazla bilgi içeren clickable ile kıyaslandığında, erişilebilirlik için kullanılan semantik daha fazla bilgi içerir.

Bileşen desteği

Compose'daki kullanıma hazır bileşenlerin çoğu bir tür dahili hareket işleme içerir. Örneğin, LazyColumn sürükleme hareketlerine içeriğini kaydırarak yanıt verir, Button üzerine bastığınızda dalga gösterilir 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şlemenin yanında, 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 cihazına onClick lambda geçirirsiniz. Benzer şekilde, kullanıcının kaydırma çubuğu tutma yerini sürüklemesine tepki vermek için Slider öğesine bir onValueChange lambda eklersiniz.

Kullanım alanınıza uygunsa bileşenlerde bulunan hareketleri tercih edin. Bu bileşenler odaklama ve erişilebilirlik için kullanıma hazır destek sunar ve iyi test edilir. Örneğin, bir Button, erişilebilirlik hizmetlerinin bunu yalnızca herhangi bir tıklanabilir öğe yerine, doğru bir şekilde bir düğme olarak tanımlaması için özel bir şekilde işaretlenir:

// 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!") }

Oluşturma'da erişilebilirlik hakkında daha fazla bilgi edinmek için Oluşturma işleminde erişilebilirlik konusuna bakın.

Değiştiricilerle rastgele composable'lara belirli hareketler ekleyin

Hareket değiştiricileri, rastgele composable'ın hareketleri dinlemesini sağlamak için istediğiniz composable'a uygulayabilirsiniz. Örneğin, genel bir Box dokunma hareketlerini clickable yaparak işlemesine izin verebilir veya verticalScroll uygulayarak bir Column öğesinin dikey kaydırmayı tutmasını sağlayabilirsiniz.

Farklı hareket türlerini işlemek için birçok değiştirici vardır:

Kural olarak, özel hareket işleme yerine kullanıma hazır hareket değiştiricileri tercih edin. Değiştiriciler, sadece işaretçi etkinlik işlemeye ek olarak daha fazla işlev ekler. Örneğin, clickable değiştiricisi, sadece basma ve dokunma hareketlerini algılama özelliğinin yanı sıra semantik bilgiler, etkileşimler, fareyle üzerine gelme, odaklama ve klavye desteği hakkında görsel göstergeler ekler. İşlevin nasıl eklendiğini görmek için clickable kaynak koduna bakabilirsiniz.

pointerInput değiştirici ile rastgele composable'lara özel hareket ekleyin

Her hareket, kullanıma hazır bir hareket değiştiriciyle uygulanmaz. Örneğin, uzun basma, kontrol tıklaması veya üç parmakla dokunma işlemlerinden sonra gerçekleşen sürüklemeye tepki vermek için bir 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şim sağlayan pointerInput değiştiricisiyle bir hareket işleyici oluşturabilirsiniz.

Aşağıdaki kod ham işaretçi etkinliklerini işler:

@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 bölerseniz temel bileşenler:

  • pointerInput değiştiricisi. Bir veya daha fazla anahtar iletirsiniz. Bu anahtarlardan birinin değeri değiştiğinde içerik lambda değiştiricisi 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 etkinlik gerçekleşene kadar eş çobanı askıya alır.

Ham giriş etkinliklerini dinlemek güçlü olsa da bu ham verilere dayalı özel bir hareket yazmak da karmaşık bir işlemdir. Özel hareketlerin oluşturulmasını kolaylaştırmak için birçok yardımcı yöntem mevcuttur.

Tüm hareketleri algıla

Ham işaretçi etkinliklerini yönetmek yerine, gerçekleşecek belirli hareketleri dinleyebilir ve uygun şekilde yanıt verebilirsiniz. AwaitPointerEventScope aşağıdakileri dinlemek için yöntemler sunar:

Bunlar üst düzey algılayıcılardır. Bu nedenle tek bir pointerInput değiştiriciye 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" }
            }
    )
}

detectTapGestures yöntemi dahili olarak eş bayramı engeller ve ikinci algılayıcıya hiçbir zaman ulaşılmaz. Bir composable'a birden fazla hareket işleyici eklemeniz gerekirse 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 işleme

Hareketler tanımları gereği bir işaretçi aşağı 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 içeren bloğu yeniden başlatır. Bu, hareketin tamamlandığını belirtir:

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

Pratikte, işaretçi etkinliklerine, hareketleri tanımlamadan yanıt vermiyorsanız neredeyse her zaman awaitEachGesture kullanmak istersiniz. Bunun bir örneği, aşağı ve yukarı işaretçilere tepki vermeyen hoverable kullanmaktır. Yalnızca bir işaretçinin sınırlarına ne zaman girdiğini veya sınırlarından çıktığını bilmesi yeterlidir.

Belirli bir etkinliği veya alt hareketi bekleme

Hareketlerin yaygın kısımlarının tanımlanmasına yardımcı olan bir dizi yöntem vardır:

Çok noktalı etkinlikler için hesaplamaları uygulama

Bir kullanıcı birden fazla işaretçi kullanarak çok noktalı bir hareket gerçekleştirdiğinde, ham değerlere dayalı olarak 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. Bunlar calculateCentroid, calculateCentroidSize, calculatePan, calculateRotation ve calculateZoom yöntemleridir.

Etkinlik gönderme ve isabet testi

Her işaretçi etkinliği her pointerInput değiştiriciye gönderilmez. Etkinlik gönderme şu şekilde çalışır:

  • İşaretçi etkinlikleri composable 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çiyle giriş işleme özelliklerine sahip olan composable'lar uygun kabul edilir. İsabet testi, kullanıcı arayüzü ağacının tepesinden alt kısmına doğru ilerler. composable, işaretçi etkinliği söz konusu composable'ın sınırları içinde gerçekleştiğinde "isabet"tir. Bu süreç, testler olumlu sonuç veren bir composable zincirine yol açar.
  • Varsayılan olarak, ağacın aynı düzeyinde birden fazla uygun composable olduğunda, yalnızca en yüksek Z-endeksine sahip composable "isabet" olur. Örneğin, bir Box öğesine çakışan iki Button composable eklediğinizde, yalnızca en üste çizilen öğe işaretçi etkinliklerini alır. Teorik olarak bu davranışı geçersiz kılmak için kendi PointerInputModifierNode uygulamanızı oluşturabilir ve sharePointerInputWithSiblings değerini "true" (doğru) olarak ayarlayabilirsiniz.
  • Aynı işaretçi için diğer etkinlikler aynı composables zincirine gönderilir ve etkinlik yayılım mantığına göre akış yapılır. Sistem bu işaretçi için artık isabet testi uygulamaz. Yani zincirdeki her composable, söz konusu composable'ın sınırlarının dışında olsa bile bu işaretçi için tüm etkinlikleri alır. Zincirde olmayan composable'lar, işaretçi kendi sınırlarının içinde olsa bile hiçbir zaman işaretçi etkinliği almaz.

Fareyle veya ekran kaleminin üzerine gelmesiyle tetiklenen fareyle üzerine gelme etkinlikleri, burada tanımlanan kurallara bir istisnadır. Hover etkinlikleri, isabetli oldukları tüm composable'lara gönderilir. Böylece kullanıcı, bir composable'ın sınırlarında işaretçiyi diğerinin üzerine getirdiğinde etkinlikleri ilk composable'a göndermek yerine yeni composable'a gönderilir.

Etkinlik tüketimi

Birden fazla composable'a bir hareket işleyici atandığında bu işleyiciler çakışmamalıdır. Örneğin, şu kullanıcı arayüzüne göz atın:

Bir Resim, iki metin içeren bir sütun ve bir Düğme içeren liste öğesi.

Kullanıcı yer işareti düğmesine dokunduğunda düğmenin onClick lambdası bu hareketi işler. Kullanıcı liste öğesinin herhangi bir bölümüne dokunduğunda ListItem bu hareketi işler ve makaleye gider. İşaretçi girişi açısından, Düğmenin bu etkinliği tüketmesi gerekir. Böylece üst öğe, bundan böyle etkinliğe tepki vermeyeceğ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 kullanmanız gerekir. Bunu PointerInputChange.consume yöntemiyle yaparsınız:

Modifier.pointerInput(Unit) {

    awaitEachGesture {
        while (true) {
            val event = awaitPointerEvent()
            // consume all changes
            event.changes.forEach { it.consume() }
        }
    }
}

Bir etkinliği kullanmak, etkinliğin diğer composable'lara yayılımını durdurmaz. Bunun yerine, tüketilen etkinlikleri composable'ın açıkça yoksayması gerekir. Özel hareketleri yazarken, bir etkinliğin halihazırda başka bir öğe tarafından tüketilip tüketilmediğini 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 belirtildiği gibi, işaretçi değişiklikleri, isabetli her composable'a iletilir. Ancak böyle 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ü aşağıdaki kullanıcı arayüzü ağacına çevrilir. Burada yalnızca ListItem ve Button, işaretçi etkinliklerine yanıt verir:

Ağaç yapısı. Üst katman ListItem, ikinci katmanda Resim, Sütun ve Düğme yer alıyor ve Sütun iki Metine ayrılıyor. ListItem ve Button vurgulanmış.

İşaretçi etkinlikleri, üç "geçiş" sırasında bu composable'ların her biri üzerinden üç kez geçer:

  • İlk geçişte etkinlik, kullanıcı arayüzü ağacının üst tarafından alta doğru akar. Bu akış, çocuk tüketmeden önce bir ebeveynin etkinliğe müdahale etmesine olanak tanır. Örneğin, ipuçlarının alt öğelere vermek yerine uzun basmayı engellemesi gerekir. Örneğimizde ListItem, etkinliği Button öğesinden ö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. Bu geçişte hareketlerin işlenmesi, yaprak düğümlerinin üst öğelerine göre öncelikli olduğu anlamına gelir. Bu, çoğu hareket için en mantıklı davranıştır. Örneğimizde Button, etkinliği ListItem öğesinden önce alır.
  • Son geçiş'te etkinlik, kullanıcı arayüzü ağacının tepesinden yaprak düğümlerine bir kez daha akar. Bu akış, yığında daha yukarıda bulunan öğelerin, üst öğeleri tarafından tüketilen etkinlik tüketimine yanıt vermesine olanak tanır. Örneğin, bir düğme, basıldığında kaydırılabilir üst öğesinin sürüklenmesine dönüştüğünde ilgili düğmenin dalga gösterimi kaldırılır.

Görsel olarak, etkinlik akışı aşağıdaki gibi gösterilebilir:

Bir giriş değişikliği yapıldıktan sonra, bu bilgi akışın ilgili noktasından itibaren aktarılır:

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 da bu bekleme yöntemi çağrılarının her biri, aynı etkinliği döndürür.

Hareketleri test et

Test yöntemlerinizde, performTouchInput yöntemini kullanarak işaretçi etkinliklerini manuel olarak gönderebilirsiniz. Bu, daha yüksek düzeydeki tam hareketleri (çimdikleme veya uzun tıklama gibi) veya düşük düzeyli hareketleri (imleci belirli bir miktarda piksel hareket ettirme gibi) gerçekleştirebilmenizi sağlar:

composeTestRule.onNodeWithTag("MyList").performTouchInput {
    swipeUp()
    swipeDown()
    click()
}

Daha fazla örnek için performTouchInput dokümanlarına göz atın.

Daha fazla bilgi

Jetpack Compose'daki hareketler hakkında daha fazla bilgiyi aşağıdaki kaynaklardan edinebilirsiniz: