Memungkinkan pengguna menarik komponen secara fisik di seluruh layar adalah hal mendasar untuk aplikasi Android modern. Baik Anda membuat penggeser kustom, komponen geser untuk menutup, maupun kanvas interaktif, Jetpack Compose menyediakan serangkaian API yang komprehensif untuk menangani peristiwa tarik dengan mudah.
API penyeretan Compose menyederhanakan kompleksitas peristiwa sentuh mentah.
Daripada menghitung koordinat sentuh secara manual dan mengelola siklus proses gestur
dengan pointerInput, Anda dapat menggunakan pengubah bawaan yang terkait
langsung dengan sistem animasi dan pengelolaan status Compose.
Halaman ini menjelaskan cara dan waktu penggunaan setiap API penyeretan.
Memilih API penyeretan yang benar
Ada beberapa API yang dapat Anda gunakan untuk menerapkan konten yang dapat ditarik.
- Pilih komponen Material standar (seperti
SwipeToDismissBoxatau panel navigasi) saat: Anda memerlukan perilaku geser atau tarik Material 3 default (misalnya, menghapus email, mengarsipkan tugas, atau membuka menu samping / panel bawah). Komponen ini menyediakan perilaku bawaan tanpa memerlukan pengelolaan status atau tata letak kustom. - Pilih
Modifier.draggablejika: Anda hanya memerlukan delta tarik mentah (misalnya, menarik penggeser kustom). Untuk mengetahui informasi selengkapnya, lihat Menarik di sepanjang satu sumbu dengan dapat ditarik.- Pilih
Modifier.draggable2Djika: Anda memerlukan komponen untuk bergerak terus-menerus di sumbu X dan Y secara bersamaan (misalnya, menggeser peta atau membuat tombol tindakan mengambang yang dapat pengguna tarik ke mana saja di layar). Untuk mengetahui informasi selengkapnya, lihat Menarik dalam dua dimensi denganModifier.draggable2D.
- Pilih
- Pilih
Modifier.anchoredDraggablejika: Anda membuat komponen interaktif kustom yang perlu diposisikan di antara dua atau beberapa posisi terpisah (misalnya, panel bawah, panel navigasi, atau tindakan geser untuk menampilkan). Untuk mengetahui informasi selengkapnya, lihat Menarik komponen di antara titik anchor dengananchoredDraggable.
Tarik di sepanjang satu sumbu dengan draggable
Pengubah draggable adalah titik entri tingkat tinggi untuk gestur tarik dalam
satu orientasi, dan melaporkan jarak tarik dalam piksel.
Penting untuk diperhatikan bahwa pengubah ini mirip dengan scrollable, karena pengubah hanya mendeteksi gestur. Anda perlu menyimpan status dan merepresentasikannya di layar, misalnya, dengan memindahkan elemen melalui pengubah offset:
@Composable private fun DraggableText() { var offsetX by remember { mutableFloatStateOf(0f) } Text( modifier = Modifier .offset { IntOffset(offsetX.roundToInt(), 0) } .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> offsetX += delta } ), text = "Drag me!" ) }
Jika Anda perlu mengontrol seluruh gestur menarik, pertimbangkan untuk menggunakan pendeteksi gestur
tarik, melalui pengubah pointerInput.
@Composable private fun DraggableTextLowLevel() { Box(modifier = Modifier.fillMaxSize()) { var offsetX by remember { mutableFloatStateOf(0f) } var offsetY by remember { mutableFloatStateOf(0f) } Box( Modifier .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) } .background(Color.Blue) .size(50.dp) .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consume() offsetX += dragAmount.x offsetY += dragAmount.y } } ) } }
Tarik komponen di antara titik link dengan anchoredDraggable
Pengubah anchoredDraggable memungkinkan Anda menarik komponen di antara sekumpulan
posisi yang telah ditetapkan, yang disebut titik link. Saat pengguna melepaskan gestur
tarik, komponen akan berada di anchor terdekat. anchoredDraggable berguna untuk membuat komponen seperti panel bawah kustom, penggeser kompleks, atau panel panel samping.
Platform API
AnchoredDraggable API terdiri dari bagian utama berikut:
- Anchors (
DraggableAnchors): Builder yang menentukan pemetaan antara status kustom Anda (misalnya, class enum untukStart, Center, End) dan nilai offset piksel yang sesuai. - State (
AnchoredDraggableState<T>): Holder status yang mengelola posisi komponen saat ini (offset), anchor tempat komponen ditetapkan, dan progres tarik. Objek ini harus dibuat menggunakanrememberSaveabledanAnchoredDraggableState.Saver<T>untuk mempertahankan statusnya di seluruh perubahan konfigurasi. - Pengubah gestur (
Modifier.anchoredDraggable): Pengubah yang mendeteksi gestur tarik di sepanjangOrientation, memperbarui status dengan delta tarik, dan menangani gestur ayunkan jari untuk menganimasikan komponen ke anchor terdekat setelah dilepaskan.
Contoh: Membangun komponen SwipeToDismiss kustom
Contoh ini menunjukkan cara membuat komponen SwipeToDismiss kustom (banner
putih) yang dapat digeser ke kiri oleh pengguna untuk menutup, atau ditarik ke kanan untuk
melihat efek peregangan overscroll.
Tentukan nilai anchor. Tentukan enum yang mewakili posisi terpisah yang dapat ditempati oleh konten yang dapat ditarik:
private enum class AnchoredDraggableSwipeDismissValue { DismissedStart, Resting }
Buat instance dan simpan
AnchoredDraggableState. GunakanrememberSaveabledenganAnchoredDraggableState.Saver()untuk mempertahankan posisi komponen saat ini di seluruh pembuatan ulang aktivitas dan perubahan konfigurasi (misalnya, rotasi layar atau perubahan tema):val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) { AnchoredDraggableState(initialValue = AnchoredDraggableSwipeDismissValue.Resting) }
Terapkan pengubah
layout,offset, dandraggable. Agar komponen dapat ditarik dan memastikan titik angkurnya menyesuaikan dengan ukurannya, terapkan pengubah berikut ke konten yang akan dipindahkan secara visual:Modifier.layout: Posisi anchor sering kali bergantung pada ukuran komponen, yang tidak diketahui selama komposisi awal. Gunakan pengubahlayoutuntuk mengukur komponen, menghitung petaDraggableAnchorssecara dinamis berdasarkan lebarnya, dan memperbarui status menggunakanstate.updateAnchors().
Modifier.anchoredDraggable: Terhubung ke status, mengirimkan delta tarik dalam orientasi yang ditentukan, dan menangani gestur ayunkan jari untuk menganimasikan komponen ke anchor terdekat saat dilepaskan. Tambahkan pengubah ini ke komponen yang harus merespons gestur tarik dan teruskanstatedan tarikorientation(dan, secara opsional,overscrollEffect).Modifier.offset: Membaca offset daristate.requireOffset()di dalam lambda offset. Membaca offset dalam lambda ini memastikan nilai hanya dibaca selama fase penempatan, sehingga menghindari rekomposisi yang tidak perlu selama penyeretan. Terapkan pengubah ini ke konten yang harus dipindahkan secara visual.
Box( modifier = Modifier .fillMaxWidth() // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase .offset { IntOffset(state.requireOffset().roundToInt(), 0) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) val anchors = DraggableAnchors { AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat() AnchoredDraggableSwipeDismissValue.Resting at 0f } state.updateAnchors(anchors) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll. .overscroll(overscrollEffect) .anchoredDraggable( state, Orientation.Horizontal, overscrollEffect = overscrollEffect ) ) { dismissContent() }
[Opsional] Mengintegrasikan overscroll.
overscrollEffectAPI menerapkan efek visual yang ditampilkan saat tepi penampung scroll telah dicapai dengan scroll atau fling. Anda dapat memberikan masukan visual saat pengguna menarik melewati batas anchor yang ditentukan dengan efek overscroll (biasanya berupa peregangan atau cahaya).- Pengiriman: Teruskan instance
overscrollEffectke pengubahanchoredDraggable. Tindakan ini akan mengirimkan delta tarik ke statusAnchoredDraggableatau efek overscroll, bergantung pada apakah gestur berada di dalam atau di luar batas anchor. - Render: Terapkan
Modifier.overscrolldengan instanceOverscrollEffectyang sama ke komponen yang akan merender efek secara visual.
- Pengiriman: Teruskan instance
Cuplikan berikut menunjukkan cara menambahkan efek peregangan standar:
val overscrollEffect = rememberOverscrollEffect() Box( modifier.fillMaxWidth() ) { Box(Modifier.fillMaxWidth()) { background() } Box( modifier = Modifier .fillMaxWidth() // Read state.requireOffset() inside the offset lambda to defer reading to the placement phase .offset { IntOffset(state.requireOffset().roundToInt(), 0) } .layout { measurable, constraints -> val placeable = measurable.measure(constraints) val anchors = DraggableAnchors { AnchoredDraggableSwipeDismissValue.DismissedStart at -placeable.width.toFloat() AnchoredDraggableSwipeDismissValue.Resting at 0f } state.updateAnchors(anchors) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } // Pass overscrollEffect to anchoredDraggable so deltas beyond the bounds (min and max anchor) are dispatched to overscrollEffect, and render the visual effect with Modifier.overscroll. .overscroll(overscrollEffect) .anchoredDraggable( state, Orientation.Horizontal, overscrollEffect = overscrollEffect ) ) { dismissContent() } }
Hasil
Cuplikan kode sebelumnya menghasilkan banner putih, dengan teks "Tarik saya untuk menutup!", yang dapat digeser pengguna ke kiri untuk menutup (atau ditarik ke kanan untuk melihat efek peregangan overscroll):
Contoh: Membuat panel navigasi kustom berbasis gestur
Contoh ini menunjukkan cara membuat panel navigasi kustom yang digerakkan oleh gestur. Saat pengguna menarik secara horizontal, layar latar depan (ScreenContents)
bergeser ke kanan, memperkecil skala dari 1.0f menjadi 0.8f, dan membulatkan sudutnya
untuk menampilkan panel navigasi di bawahnya.
Tentukan status dan posisi anchor. Dalam contoh ini, angkarnya adalah status panel samping yang terbuka dan tertutup. Tentukan enum untuk status
OpendanClosed, lalu petakan ke offset piksel menggunakanDraggableAnchors:enum class DrawerState { Open, Closed } val anchors = DraggableAnchors { DrawerState.Open at drawerWidth DrawerState.Closed at 0f } val state = remember { AnchoredDraggableState( initialValue = DrawerState.Closed, anchors = anchors, ) }
Menganimasikan
anchoredDraggableStatesaat item diklik. TempatkanHomeScreenDrawerdi bagian bawahBoxdanScreenContentsdi atas. Panel samping tetap diam, sementara lapisan atas diterjemahkan dan diskalakan di atasnya.Saat pengguna memilih item panel samping, perbarui status layar aktif dan tutup panel samping secara otomatis menggunakan
state.animateTo(DrawerState.Closed):@Composable private fun AnchoredDraggableDrawerOnClickSample( modifier: Modifier = Modifier, state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { var screenState by remember { mutableStateOf(ScreenState.Home) } Box(modifier = modifier.fillMaxSize()) { HomeScreenDrawer( selectedScreen = screenState, onScreenSelected = { screen -> screenState = screen // Dismiss drawer when a destination is selected coroutineScope.launch { state.animateTo(DrawerState.Closed) } } ) // ScreenContents(...) } }
Gunakan
state.requireOffset()untuk mengubah beberapa properti visual. Daripada hanya mengimbangi konten, Anda dapat mengubah beberapa properti denganModifier.graphicsLayeruntuk menganimasikan terjemahan, skala, dan radius sudut secara bersamaan berdasarkan progres tarik:- Gunakan
state.requireOffset()untuk mendapatkan offset tarik saat ini. - Gunakan
lerp()(interpolasi linear) untuk menghitung skala proporsional (1fhingga0.8f) dan radius sudut (0.dphingga32.dp) yang relatif terhadap offset dan lebar panel samping. Modifier.graphicsLayermenjalankan perubahan dalam fase gambar, sehingga memastikan animasi berperforma tinggi tanpa memicu rekomposisi selama operasi tarik.
@Composable private fun AnchoredDraggableDrawerGraphicsLayerSample( state: AnchoredDraggableState<DrawerState>, drawerWidth: Float, selectedScreen: ScreenState, onDrawerClicked: () -> Unit ) { ScreenContents( selectedScreen = selectedScreen, onDrawerClicked = onDrawerClicked, modifier = Modifier .graphicsLayer { this.translationX = state.requireOffset() val scale = lerp(1f, 0.8f, state.requireOffset() / drawerWidth) this.scaleX = scale this.scaleY = scale val cornerRadius = lerp(0f, 32f, state.requireOffset() / drawerWidth) this.shape = RoundedCornerShape(cornerRadius.dp) this.clip = true } .anchoredDraggable(state, Orientation.Horizontal) ) }
- Gunakan
Memicu animasi terprogram dengan
animateTo. Kontrol panel samping secara terprogram (seperti saat mengetuk ikon menu atau memilih item panel samping) dengan meluncurkanstate.animateTo()dalam cakupan coroutine:fun toggleDrawerState( state: AnchoredDraggableState<DrawerState>, coroutineScope: CoroutineScope ) { coroutineScope.launch { if (state.currentValue == DrawerState.Open) { state.animateTo(DrawerState.Closed) } else { state.animateTo(DrawerState.Open) } } }
Hasil
Tarik dalam dua dimensi dengan Modifier.draggable2D
Gunakan pengubah draggable2D untuk memindahkan setiap elemen UI di sumbu X dan Y secara bersamaan.
Menarik elemen composable
Contoh ini menunjukkan kasus penggunaan paling umum untuk draggable2D — memungkinkan pengguna
mengambil elemen UI dan memosisikannya ulang di mana saja dalam penampung induk.
@Composable private fun DraggableComposableElement() { // 1. Track the position of the floating window var offset by remember { mutableStateOf(Offset.Zero) } Box(modifier = Modifier.fillMaxSize().background(Color(0xFFF5F5F5))) { Box( modifier = Modifier // 2. Apply the offset to the box's position .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) } // ... // 3. Attach the 2D drag logic .draggable2D( state = rememberDraggable2DState { delta -> // 4. Update the position based on the movement delta offset += delta } ), contentAlignment = Alignment.Center ) { Text("Video Preview", color = Color.White, fontSize = 12.sp) } } }
Cuplikan kode sebelumnya mencakup hal berikut:
- Melacak posisi kotak menggunakan status
offset. - Menggunakan pengubah
offsetuntuk menggeser posisi komponen berdasarkan delta tarik. - Karena tidak ada dukungan fling, kotak akan berhenti bergerak begitu pengguna mengangkat jarinya.
Menarik composable turunan berdasarkan area tarik induk
Contoh ini menunjukkan cara menggunakan draggable2D untuk membuat area input 2D
tempat kenop pemilih dibatasi dalam permukaan tertentu. Tidak seperti
contoh elemen yang dapat ditarik, yang memindahkan komponen itu sendiri, implementasi ini
menggunakan delta 2D untuk memindahkan "pemilih" composable turunan di seluruh pemilih warna:
@Composable private fun ExampleColorSelector( // ... ) { // 1. Maintain the 2D position of the selector in state. var selectorOffset by remember { mutableStateOf(Offset.Zero) } // 2. Track the size of the background container. var containerSize by remember { mutableStateOf(IntSize.Zero) } Box( modifier = Modifier .size(300.dp, 200.dp) // Capture the actual pixel dimensions of the container when it's laid out. .onSizeChanged { containerSize = it } .clip(RoundedCornerShape(12.dp)) .background( brush = remember(hue) { // Create a simple gradient representing Saturation and Value for the given Hue. Brush.linearGradient(listOf(Color.White, Color.hsv(hue, 1f, 1f))) } ) ) { Box( modifier = Modifier .size(24.dp) .graphicsLayer { // Center the selector on the finger by subtracting half its size. translationX = selectorOffset.x - (24.dp.toPx() / 2) translationY = selectorOffset.y - (24.dp.toPx() / 2) } // ... // 3. Configure 2D touch dragging. .draggable2D( state = rememberDraggable2DState { delta -> // 4. Calculate the new position and clamp it to the container bounds val newX = (selectorOffset.x + delta.x) .coerceIn(0f, containerSize.width.toFloat()) val newY = (selectorOffset.y + delta.y) .coerceIn(0f, containerSize.height.toFloat()) selectorOffset = Offset(newX, newY) } ) ) } }
Cuplikan sebelumnya mencakup hal berikut:
- Fungsi ini menggunakan pengubah
onSizeChangeduntuk menangkap dimensi sebenarnya dari penampung gradien. Pemilih tahu persis di mana tepinya. - Di dalam
graphicsLayer, fungsi ini menyesuaikantranslationXdantranslationYuntuk memastikan pemilih tetap berada di tengah saat ditarik.
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Memahami gestur
- Contoh animasi lanjutan: Gestur {:#gesture-and-animation}
- Animasi berbasis nilai