Memungkinkan pengguna menyeret komponen secara fisik di seluruh layar sangat penting untuk aplikasi Android modern. Baik Anda membuat penggeser kustom, komponen geser untuk menutup, atau kanvas interaktif, Jetpack Compose menyediakan serangkaian API komprehensif untuk menangani peristiwa seret dengan mudah.
API seret Compose mengabstraksi kompleksitas peristiwa sentuh mentah.
Daripada menghitung koordinat sentuh secara manual dan mengelola siklus proses gestur
dengan pointerInput, Anda dapat menggunakan pengubah bawaan yang terikat
langsung ke sistem pengelolaan status dan animasi Compose.
Halaman ini menjelaskan cara dan waktu penggunaan setiap API seret.
Memilih API seret yang benar
Ada beberapa API yang dapat Anda gunakan untuk menerapkan konten yang dapat diseret.
- Pilih komponen Material standar (seperti
SwipeToDismissBoxatau panel navigasi) jika: Anda memerlukan perilaku geser atau seret Material 3 default (misalnya, menghapus email, mengarsipkan tugas, atau membuka menu samping / sheet bawah). Komponen ini menyediakan perilaku bawaan tanpa memerlukan pengelolaan status atau tata letak kustom. - Pilih
Modifier.draggablejika: Anda hanya memerlukan delta seret mentah (misalnya, menyeret penggeser kustom). Untuk mengetahui informasi selengkapnya, lihat Menyeret sepanjang satu sumbu dengan draggable.- Pilih
Modifier.draggable2Djika: Anda memerlukan komponen untuk bergerak terus-menerus di seluruh sumbu X dan Y secara bersamaan (misalnya, menggeser peta atau membuat tombol tindakan mengambang yang dapat diseret pengguna ke mana saja di layar). Untuk mengetahui informasi selengkapnya, lihat Menyeret dalam dua dimensi denganModifier.draggable2D.
- Pilih
- Pilih
Modifier.anchoredDraggablejika: Anda membuat komponen interaktif kustom yang perlu di-snap di antara dua atau beberapa posisi terpisah (misalnya, sheet bawah, panel navigasi, atau tindakan geser untuk menampilkan). Untuk mengetahui informasi selengkapnya, lihat Menyeret komponen di antara titik anchor dengananchoredDraggable.
Menyeret sepanjang satu sumbu dengan draggable
Pengubah draggable adalah titik entri tingkat tinggi untuk gestur seret dalam
satu orientasi, dan melaporkan jarak seret dalam piksel.
Penting untuk diperhatikan bahwa pengubah ini mirip dengan scrollable, karena pengubah hanya mendeteksi gestur. Anda harus menyimpan status dan menampilkannya 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 seret, pertimbangkan untuk menggunakan detektor gestur seret, 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 } } ) } }
Menyeret komponen di antara titik anchor dengan anchoredDraggable
Pengubah anchoredDraggable memungkinkan Anda menyeret komponen di antara sekumpulan
posisi yang telah ditentukan sebelumnya yang disebut titik anchor. Saat pengguna melepaskan gestur seret, komponen akan berada di anchor terdekat. anchoredDraggable berguna untuk membuat komponen seperti sheet bawah kustom, penggeser kompleks, atau panel laci.
Platform API
API AnchoredDraggable terdiri dari bagian utama berikut:
- Anchor (
DraggableAnchors): Builder yang menentukan pemetaan antara status kustom Anda (misalnya, class enum untukStart, Center, End) dan nilai offset piksel yang sesuai. - Status (
AnchoredDraggableState<T>): Penampung status yang mengelola posisi (offset) komponen saat ini, anchor tempat komponen berada, dan progres seretnya. Status ini harus dibuat menggunakanrememberSaveabledanAnchoredDraggableState.Saver<T>untuk mempertahankan statusnya di seluruh perubahan konfigurasi. - Pengubah gestur (
Modifier.anchoredDraggable): Pengubah yang mendeteksi gestur seret di sepanjangOrientation, memperbarui status dengan delta seret, dan menangani gestur lempar untuk menganimasikan komponen ke anchor terdekat saat dilepaskan.
Contoh: Mem-build komponen SwipeToDismiss kustom
Contoh ini menunjukkan cara mem-build komponen SwipeToDismiss kustom (banner putih) yang dapat digeser ke kiri untuk menutup, atau diseret ke kanan untuk melihat efek peregangan overscroll.
Tentukan nilai anchor. Tentukan enum yang mewakili posisi terpisah yang dapat digunakan untuk menempatkan konten yang dapat diseret:
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. Untuk membuat komponen dapat diseret dan memastikan titik anchor-nya beradaptasi dengan ukurannya, terapkan pengubah berikut ke konten yang harus bergerak 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, mengirim delta seret dalam orientasi yang ditentukan, dan menangani gestur lempar untuk menganimasikan komponen ke anchor terdekat saat dilepaskan. Tambahkan pengubah ini ke komponen yang harus merespons gestur seret dan teruskanstatedanorientationseret (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 seret. Terapkan pengubah ini ke konten yang harus bergerak 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. API
overscrollEffectmenerapkan efek visual yang ditampilkan saat tepi penampung scroll telah tercapai dengan scroll atau lempar. Anda dapat memberikan masukan visual saat pengguna menyeret melewati batas anchor yang ditentukan dengan efek overscroll (biasanya peregangan atau cahaya).- Kirim: Teruskan instance
overscrollEffectke pengubahanchoredDraggable. Tindakan ini mengirim delta seret 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 harus merender efek secara visual.
- Kirim: 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 "Drag me to dismiss!", yang dapat digeser ke kiri untuk menutup (atau diseret ke kanan untuk melihat efek peregangan overscroll):
Contoh: Membuat panel navigasi kustom yang digerakkan oleh gestur
Contoh ini menunjukkan cara mem-build panel navigasi kustom yang digerakkan oleh gestur. Saat pengguna menyeret secara horizontal, layar latar depan (ScreenContents) akan bergeser ke kanan, diperkecil dari 1.0f menjadi 0.8f, dan sudutnya dibulatkan untuk menampilkan panel navigasi yang mendasarinya.
Tentukan status dan posisi anchor. Dalam contoh ini, anchor adalah status panel yang dibuka dan ditutup. 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, ) }
Animasikan
anchoredDraggableStatesaat item diklik. TempatkanHomeScreenDrawerdi bagian bawahBoxdanScreenContentsdi bagian atas. Panel tetap diam, sementara lapisan atas diterjemahkan dan diskalakan di atasnya.Saat pengguna memilih item panel, perbarui status layar aktif dan otomatis tutup panel 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. Selain hanya mengimbangi konten, Anda dapat mengubah beberapa properti denganModifier.graphicsLayeruntuk menganimasikan terjemahan, skala, dan radius sudut secara bersamaan berdasarkan progres seret:- Gunakan
state.requireOffset()untuk mendapatkan offset seret saat ini. - Gunakan
lerp()(interpolasi linear) untuk menghitung skala proporsional (1fhingga0.8f) dan radius sudut (0.dphingga32.dp) relatif terhadap offset dan lebar panel. Modifier.graphicsLayermenjalankan perubahan dalam fase gambar, sehingga memastikan animasi berperforma tinggi tanpa memicu rekomposisi selama seret.
@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 secara terprogram (seperti saat mengetuk ikon menu atau memilih item panel) 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
Menyeret dalam dua dimensi dengan Modifier.draggable2D
Gunakan pengubah draggable2D untuk memindahkan elemen UI individual di seluruh sumbu X dan Y secara bersamaan.
Menyeret elemen composable
Contoh ini menunjukkan kasus penggunaan paling umum untuk draggable2D — memungkinkan pengguna mengambil elemen UI dan memposisikannya 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 seret. - Karena tidak ada dukungan lempar, kotak berhenti bergerak saat pengguna mengangkat jari.
Menyeret composable turunan berdasarkan area seret induk
Contoh ini menunjukkan cara menggunakan draggable2D untuk membuat area input 2D tempat tombol pemilih dibatasi dalam permukaan tertentu. Tidak seperti contoh elemen yang dapat diseret, yang memindahkan komponen itu sendiri, implementasi ini menggunakan delta 2D untuk memindahkan composable turunan "pemilih" 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:
- Cuplikan ini menggunakan pengubah
onSizeChangeduntuk mengambil dimensi sebenarnya dari penampung gradien. Pemilih mengetahui dengan tepat lokasi tepi. - Di dalam
graphicsLayer, cuplikan ini menyesuaikantranslationXdantranslationYuntuk memastikan pemilih tetap berada di tengah saat diseret.
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Memahami gestur
- Contoh animasi lanjutan: Gestur {:#gesture-and-animation}
- Animasi berbasis nilai