Trong Jetpack Compose, scrollable2D và draggable2D là
các đối tượng sửa đổi cấp thấp được thiết kế để xử lý dữ liệu đầu vào của con trỏ theo hai chiều. Trong khi
các đối tượng sửa đổi 1D tiêu chuẩn scrollable và draggable bị
giới hạn ở một hướng duy nhất, thì các biến thể 2D sẽ theo dõi chuyển động trên cả
trục X và Y cùng một lúc.
Ví dụ: đối tượng sửa đổi scrollable hiện có được dùng để cuộn và vuốt theo một hướng
, trong khi scrollable2d được dùng để cuộn và vuốt theo 2D. Điều này cho phép bạn tạo các bố cục phức tạp hơn di chuyển theo mọi hướng, chẳng hạn như bảng tính hoặc trình xem hình ảnh. Đối tượng sửa đổi scrollable2d cũng hỗ trợ tính năng cuộn lồng trong các trường hợp 2D.
Chọn scrollable2D hoặc draggable2D
Việc chọn API phù hợp phụ thuộc vào các thành phần trên giao diện người dùng mà bạn muốn di chuyển và hành vi vật lý ưu tiên cho các thành phần này.
Modifier.scrollable2D: Sử dụng đối tượng sửa đổi này trên một vùng chứa để di chuyển nội dung bên trong vùng chứa đó. Ví dụ: sử dụng đối tượng sửa đổi này với bản đồ, bảng tính hoặc trình xem ảnh, trong đó nội dung của vùng chứa cần cuộn theo cả hướng ngang và dọc. Đối tượng sửa đổi này bao gồm tính năng hỗ trợ hất tích hợp để nội dung tiếp tục di chuyển sau khi vuốt và phối hợp với các thành phần cuộn khác trên trang.
Modifier.draggable2D: Sử dụng đối tượng sửa đổi này để di chuyển chính thành phần đó. Đây là một đối tượng sửa đổi gọn nhẹ, vì vậy, chuyển động sẽ dừng chính xác khi ngón tay của người dùng dừng lại. Đối tượng sửa đổi này không bao gồm tính năng hỗ trợ vuốt.
Nếu bạn muốn tạo một thành phần có thể kéo, nhưng không cần tính năng hỗ trợ vuốt hoặc cuộn lồng, hãy sử dụng draggable2D.
Triển khai đối tượng sửa đổi 2D
Các phần sau đây cung cấp ví dụ để cho biết cách sử dụng đối tượng sửa đổi 2D.
Triển khai Modifier.scrollable2D
Sử dụng đối tượng sửa đổi này cho các vùng chứa mà người dùng cần di chuyển nội dung theo mọi hướng.
Thu thập dữ liệu chuyển động 2D
Ví dụ này cho biết cách thu thập dữ liệu chuyển động 2D thô và hiển thị độ lệch 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()}", // ... ) } } } }
Đoạn mã trước đó thực hiện những việc sau:
- Sử dụng
offsetlàm trạng thái lưu giữ tổng khoảng cách mà người dùng đã cuộn. - Bên trong
rememberScrollable2DState, một hàm lambda được xác định để xử lý mọi delta do ngón tay của người dùng tạo ra. Mãoffset.value += deltacập nhật trạng thái thủ công bằng vị trí mới. - Các thành phần
Texthiển thị giá trị X và Y hiện tại của trạng tháioffsetđó, cập nhật theo thời gian thực khi người dùng kéo.
Lia ngang khung nhìn lớn
Ví dụ này cho biết cách sử dụng dữ liệu có thể cuộn 2D đã thu thập và áp dụng translationX và translationY cho nội dung lớn hơn vùng chứa gốc:
@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.Đoạn mã trước đó bao gồm những điều sau:
- Vùng chứa được đặt thành kích thước cố định (
600x400dp), trong khi nội dung được đặt thành kích thước lớn hơn nhiều (1200x800dp) để tránh việc nội dung đổi kích thước thành kích thước gốc. - Đối tượng sửa đổi
clipToBounds()trên vùng chứa đảm bảo rằng mọi phần của nội dung lớn nằm ngoài hộp600x400đều bị ẩn khỏi khung hiển thị. - Không giống như các thành phần cấp cao như
LazyColumn,scrollable2Dkhông tự động di chuyển nội dung cho bạn. Thay vào đó, bạn phải áp dụngoffsetđược theo dõi cho nội dung của mình, bằng cách sử dụng các phép biến đổigraphicsLayerhoặc độ lệch bố cục. - Bên trong khối
graphicsLayer,translationX = offset.value.xvàtranslationY = offset.value.ysẽ dịch chuyển vị trí vẽ của hình ảnh hoặc văn bản dựa trên chuyển động của ngón tay, tạo hiệu ứng hình ảnh của thao tác cuộn.
Triển khai tính năng cuộn lồng bằng scrollable2D
Ví dụ này minh hoạ cách tích hợp một thành phần hai chiều vào một thành phần gốc một chiều tiêu chuẩn, chẳng hạn như nguồn cấp tin tức dọc.
Hãy lưu ý những điểm sau khi triển khai tính năng cuộn lồng:
- Lambda cho
rememberScrollable2DStatechỉ được trả về delta đã sử dụng để danh sách gốc tự động tiếp quản khi thành phần con đạt đến giới hạn. - Khi người dùng thực hiện thao tác vuốt theo đường chéo, vận tốc 2D sẽ được chia sẻ. Nếu thành phần con chạm vào ranh giới trong quá trình tạo ảnh động, thì động lượng còn lại sẽ được truyền đến thành phần gốc để tiếp tục cuộn một cách tự nhiên.
@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) ) } }
Trong đoạn mã trước đó:
- Thành phần 2D có thể sử dụng chuyển động của trục X để lia ngang bên trong, đồng thời gửi chuyển động của trục Y đến danh sách gốc sau khi đạt đến ranh giới dọc của thành phần con.
- Thay vì giữ người dùng trong bề mặt 2D, hệ thống sẽ tính toán delta đã sử dụng và truyền phần còn lại lên hệ phân cấp. Điều này đảm bảo người dùng có thể tiếp tục cuộn qua phần còn lại của trang mà không cần nhấc ngón tay.
Triển khai Modifier.draggable2D
Sử dụng đối tượng sửa đổi draggable2D để di chuyển các thành phần riêng lẻ trên giao diện người dùng.
Kéo một thành phần kết hợp
Ví dụ này cho thấy trường hợp sử dụng phổ biến nhất cho draggable2D – cho phép người dùng chọn một phần tử trên giao diện người dùng và định vị lại phần tử đó ở bất kỳ đâu trong vùng chứa gốc.
@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) } } }
Đoạn mã trước đó bao gồm những điều sau:
- Theo dõi vị trí của hộp bằng trạng thái
offset. - Sử dụng đối tượng sửa đổi
offsetđể dịch chuyển vị trí của thành phần dựa trên delta kéo. - Vì không có tính năng hỗ trợ vuốt, nên hộp sẽ ngừng di chuyển ngay khi người dùng nhấc ngón tay lên.
Kéo một thành phần kết hợp con dựa trên vùng kéo của thành phần gốc
Ví dụ này minh hoạ cách sử dụng draggable2D để tạo vùng nhập 2D, trong đó núm bộ chọn bị ràng buộc trong một bề mặt cụ thể. Không giống như ví dụ về thành phần có thể kéo, di chuyển chính thành phần đó, quy trình triển khai này sử dụng delta 2D để di chuyển "bộ chọn" thành phần kết hợp con trên công cụ chọn màu:
@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) } ) ) } }
Đoạn mã trước đó bao gồm những điều sau:
- Đoạn mã này sử dụng đối tượng sửa đổi
onSizeChangedđể thu thập kích thước thực tế của vùng chứa chuyển màu. Bộ chọn biết chính xác vị trí của các cạnh. - Bên trong
graphicsLayer, đoạn mã này điều chỉnhtranslationXvàtranslationYđể đảm bảo bộ chọn luôn ở giữa trong khi kéo.