Di Jetpack Compose, scrollable2D dan draggable2D adalah
pengubah tingkat rendah yang dirancang untuk menangani input pointer dalam dua dimensi. Meskipun
pengubah 1D standar scrollable dan draggable dibatasi untuk satu orientasi, varian 2D melacak pergerakan di seluruh sumbu X dan Y secara bersamaan.
Misalnya, pengubah scrollable yang ada digunakan untuk scrolling dan flinging satu orientasi, sedangkan scrollable2d digunakan untuk scrolling dan flinging
dalam 2D. Hal ini memungkinkan Anda membuat tata letak yang lebih kompleks yang bergerak ke segala arah, seperti spreadsheet atau penampil gambar. Pengubah scrollable2d juga mendukung scrolling bertingkat dalam skenario 2D.
Memilih scrollable2D atau draggable2D
Memilih API yang tepat bergantung pada elemen UI yang ingin Anda pindahkan dan perilaku fisik yang diinginkan untuk elemen ini.
Modifier.scrollable2D: Gunakan pengubah ini pada penampung untuk memindahkan konten di dalamnya. Misalnya, gunakan dengan peta, spreadsheet, atau penampil foto, yang konten penampungnya perlu di-scroll dalam arah horizontal dan vertikal. Pengubah ini mencakup dukungan flinging bawaan sehingga konten terus bergerak setelah geser, dan berkoordinasi dengan komponen scrolling lainnya di halaman.
Modifier.draggable2D: Gunakan pengubah ini untuk memindahkan komponen itu sendiri. Pengubah ini ringan, sehingga pergerakan berhenti tepat saat jari pengguna berhenti. Pengubah ini tidak menyertakan dukungan flinging.
Jika Anda ingin membuat komponen dapat ditarik, tetapi tidak memerlukan dukungan flinging atau scroll bertingkat, gunakan draggable2D.
Mengimplementasikan pengubah 2D
Bagian berikut memberikan contoh untuk menunjukkan cara menggunakan pengubah 2D.
Mengimplementasikan Modifier.scrollable2D
Gunakan pengubah ini untuk penampung tempat pengguna perlu memindahkan konten ke segala arah.
Mengambil data pergerakan 2D
Contoh ini menunjukkan cara mengambil data pergerakan 2D mentah dan menampilkan offset X,Y:
@Composable private fun Scrollable2DSample() { // 1. Manually track the total distance the user has moved in both X and Y directions var offset by remember { mutableStateOf(Offset.Zero) } Box( modifier = Modifier .fillMaxSize() // ... contentAlignment = Alignment.Center ) { Box( modifier = Modifier .size(200.dp) // 2. Attach the 2D scroll logic to capture XY movement deltas .scrollable2D( state = rememberScrollable2DState { delta -> // 3. Update the cumulative offset state with the new movement delta offset += delta // Return the delta to indicate the entire movement was handled by this box delta } ) // ... contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { // 4. Display the current X and Y values from the offset state in real-time Text( text = "X: ${offset.x.roundToInt()}", // ... ) Spacer(modifier = Modifier.height(8.dp)) Text( text = "Y: ${offset.y.roundToInt()}", // ... ) } } } }
Cuplikan sebelumnya melakukan hal berikut:
- Menggunakan
offsetsebagai status yang menyimpan total jarak yang telah di-scroll pengguna. - Di dalam
rememberScrollable2DState, fungsi lambda ditentukan untuk menangani setiap delta, yang dihasilkan oleh jari pengguna. Kodeoffset.value += deltamemperbarui status manual dengan posisi baru. - Komponen
Textmenampilkan nilai X dan Y saat ini dari statusoffsettersebut, yang diperbarui secara real-time saat pengguna menarik.
Menggeser area pandang yang besar
Contoh ini menunjukkan cara menggunakan data scrollable 2D yang diambil dan menerapkan translationX dan translationY ke konten yang lebih besar dari penampung induknya:
@Composable private fun Panning2DImage() { // Manually track the total distance the user has moved in both X and Y directions val offset = remember { mutableStateOf(Offset.Zero) } // Define how gestures are captured. The lambda is called for every finger movement val scrollState = rememberScrollable2DState { delta -> offset.value += delta delta } // The Viewport (Container): A fixed-size box that acts as a window into the larger content Box( modifier = Modifier .size(600.dp, 400.dp) // The visible area dimensions // ... // Hide any parts of the large content that sit outside this container's boundaries .clipToBounds() // Apply the 2D scroll modifier to intercept touch and fling gestures in all directions .scrollable2D(state = scrollState), contentAlignment = Alignment.Center, ) { // The Content: An image given a much larger size than the container viewport Image( painter = painterResource(R.drawable.cheese_5), contentDescription = null, modifier = Modifier .requiredSize(1200.dp, 800.dp) // Manual Scroll Effect: Since scrollable2D doesn't move content automatically, // we use graphicsLayer to shift the drawing position based on the tracked offset. .graphicsLayer { translationX = offset.value.x translationY = offset.value.y }, contentScale = ContentScale.FillBounds ) } }
Modifier.scrollable2D.Modifier.scrollable2D.Cuplikan sebelumnya mencakup hal berikut:
- Penampung ditetapkan ke ukuran tetap (
600x400dp), sedangkan konten diberi ukuran yang jauh lebih besar (1200x800dp) agar tidak mengubah ukuran ke ukuran induknya. - Pengubah
clipToBounds()pada penampung memastikan bahwa bagian konten besar yang berada di luar kotak600x400disembunyikan dari tampilan. - Tidak seperti komponen tingkat tinggi seperti
LazyColumn,scrollable2Dtidak memindahkan konten untuk Anda secara otomatis. Sebagai gantinya, Anda harus menerapkanoffsetyang dilacak ke konten, baik menggunakan transformasigraphicsLayermaupun offset tata letak. - Di dalam blok
graphicsLayer,translationX = offset.value.xdantranslationY = offset.value.ymenggeser posisi gambar atau teks berdasarkan pergerakan jari Anda, sehingga membuat efek visual scrolling.
Mengimplementasikan scrolling bertingkat dengan scrollable2D
Contoh ini menunjukkan cara mengintegrasikan komponen dua arah ke induk satu dimensi standar, seperti feed berita vertikal.
Perhatikan poin-poin berikut saat mengimplementasikan scrolling bertingkat:
- Lambda untuk
rememberScrollable2DStatehanya boleh menampilkan delta yang digunakan, agar daftar induk mengambil alih secara alami saat turunan mencapai batasnya. - Saat pengguna melakukan flinging diagonal, kecepatan 2D akan dibagikan. Jika turunan mencapai batas selama animasi, momentum yang tersisa akan disebarkan ke induk untuk melanjutkan scroll secara alami.
@Composable private fun NestedScrollable2DSample() { var offset by remember { mutableStateOf(Offset.Zero) } val maxScrollDp = 250.dp val maxScrollPx = with(LocalDensity.current) { maxScrollDp.toPx() } Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .background(Color(0xFFF5F5F5)), horizontalAlignment = Alignment.CenterHorizontally ) { Text( "Scroll down to find the 2D Box", modifier = Modifier.padding(top = 100.dp, bottom = 500.dp), style = TextStyle(fontSize = 18.sp, color = Color.Gray) ) // The Child: A 2D scrollable box with nested scroll coordination Box( modifier = Modifier .size(250.dp) .scrollable2D( state = rememberScrollable2DState { delta -> val oldOffset = offset // Calculate new potential offset and clamp it to our boundaries val newX = (oldOffset.x + delta.x).coerceIn(-maxScrollPx, maxScrollPx) val newY = (oldOffset.y + delta.y).coerceIn(-maxScrollPx, maxScrollPx) val newOffset = Offset(newX, newY) // Calculate exactly how much was consumed by the child val consumed = newOffset - oldOffset offset = newOffset // IMPORTANT: Return ONLY the consumed delta. // The remaining (unconsumed) delta propagates to the parent Column. consumed } ) // ... contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { val density = LocalDensity.current Text("2D Panning Zone", color = Color.White.copy(alpha = 0.7f), fontSize = 12.sp) Spacer(Modifier.height(8.dp)) Text("X: ${with(density) { offset.x.toDp().value.roundToInt() }}dp", color = Color.White, fontWeight = FontWeight.Bold) Text("Y: ${with(density) { offset.y.toDp().value.roundToInt() }}dp", color = Color.White, fontWeight = FontWeight.Bold) } } Text( "Once the Purple Box hits Y: 250 or -250,\nthis parent list will take over the vertical scroll.", textAlign = TextAlign.Center, modifier = Modifier.padding(top = 40.dp, bottom = 800.dp), style = TextStyle(fontSize = 14.sp, color = Color.Gray) ) } }
Dalam cuplikan sebelumnya:
- Komponen 2D dapat menggunakan pergerakan sumbu X untuk menggeser secara internal sekaligus mengirim pergerakan sumbu Y ke daftar induk setelah batas vertikal turunan tercapai.
- Daripada menjebak pengguna dalam permukaan 2D, sistem menghitung delta yang digunakan dan meneruskan sisanya ke hierarki. Hal ini memastikan pengguna dapat terus men-scroll halaman lainnya tanpa mengangkat jari.
Mengimplementasikan Modifier.draggable2D
Gunakan pengubah draggable2D untuk memindahkan elemen UI individual.
Menarik elemen composable
Contoh ini menunjukkan kasus penggunaan paling umum untuk draggable2D — memungkinkan pengguna mengambil elemen UI dan memosisikannya kembali 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 flinging, kotak berhenti bergerak saat pengguna mengangkat jari.
Menarik composable turunan berdasarkan area tarik 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 ditarik, 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 ditarik.