ใน Jetpack Compose scrollable2D และ draggable2D เป็น ตัวปรับแต่งระดับต่ำที่ออกแบบมาเพื่อจัดการอินพุตพอยน์เตอร์ใน 2 มิติ ในขณะที่ตัวปรับแต่ง 1 มิติมาตรฐาน scrollable และ draggable ถูกจำกัดไว้ที่การวางแนวเดียว แต่ตัวแปร 2 มิติจะติดตามการเคลื่อนไหวทั้งในแกน X และ Y พร้อมกัน
เช่น ตัวปรับแต่ง scrollable ที่มีอยู่ใช้สำหรับการเลื่อนและปัดในทิศทางเดียว
ขณะที่ scrollable2d ใช้สำหรับการเลื่อนและปัด
ใน 2 มิติ ซึ่งช่วยให้คุณสร้างเลย์เอาต์ที่ซับซ้อนมากขึ้นซึ่งเลื่อนได้ในทุก
ทิศทาง เช่น สเปรดชีตหรือโปรแกรมดูรูปภาพ ตัวแก้ไข scrollable2d
ยังรองรับการเลื่อนที่ซ้อนกันในสถานการณ์ 2 มิติด้วย
เลือก scrollable2D หรือ draggable2D
การเลือก API ที่เหมาะสมจะขึ้นอยู่กับองค์ประกอบ UI ที่คุณต้องการย้ายและลักษณะการทำงานจริงที่ต้องการสำหรับองค์ประกอบเหล่านี้
Modifier.scrollable2D: ใช้ตัวปรับแต่งนี้กับคอนเทนเนอร์เพื่อย้ายเนื้อหาภายใน เช่น ใช้กับแผนที่ สเปรดชีต หรือโปรแกรมดูรูปภาพ ซึ่งเนื้อหาของคอนเทนเนอร์ต้องเลื่อนทั้งในแนวนอนและแนวตั้ง โดยมีฟีเจอร์การปัดในตัวเพื่อให้เนื้อหาเลื่อนต่อไปหลังจากปัด และทำงานร่วมกับคอมโพเนนต์การเลื่อนอื่นๆ ในหน้าเว็บ
Modifier.draggable2D: ใช้ตัวปรับแต่งนี้เพื่อย้ายคอมโพเนนต์ด้วยตัวคอมโพเนนต์เอง ซึ่งเป็นตัวปรับแต่งที่มีน้ำหนักเบา ดังนั้นการเคลื่อนไหวจะหยุดลงทันทีเมื่อนิ้วของผู้ใช้หยุด โดยไม่รวมการรองรับการตวัด
หากต้องการทำให้คอมโพเนนต์ลากได้ แต่ไม่จำเป็นต้องรองรับการปัดหรือการเลื่อนที่ซ้อนกัน
ให้ใช้ draggable2D
ใช้ตัวปรับแต่ง 2 มิติ
ส่วนต่อไปนี้จะแสดงตัวอย่างเพื่อแสดงวิธีใช้ตัวปรับแต่ง 2 มิติ
ใช้งาน Modifier.scrollable2D
ใช้ตัวปรับแต่งนี้กับคอนเทนเนอร์ที่ผู้ใช้ต้องย้ายเนื้อหาในทุกทิศทาง
บันทึกข้อมูลการเคลื่อนไหวแบบ 2 มิติ
ตัวอย่างนี้แสดงวิธีบันทึกข้อมูลการเคลื่อนไหว 2 มิติแบบดิบและแสดงออฟเซ็ต 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()}", // ... ) } } } }
ข้อมูลโค้ดก่อนหน้าจะทำสิ่งต่อไปนี้
- ใช้
offsetเป็นสถานะที่เก็บระยะทางทั้งหมดที่ผู้ใช้เลื่อน - ภายใน
rememberScrollable2DStateมีการกำหนดฟังก์ชัน Lambda เพื่อจัดการทุกเดลต้าที่เกิดจากนิ้วของผู้ใช้ โค้ดoffset.value += deltaอัปเดตสถานะด้วยตนเองด้วยตำแหน่งใหม่ - คอมโพเนนต์
Textจะแสดงค่า X และ Y ปัจจุบันของoffsetสถานะดังกล่าว ซึ่งจะอัปเดตแบบเรียลไทม์เมื่อผู้ใช้ลาก
เลื่อนวิวพอร์ตขนาดใหญ่
ตัวอย่างนี้แสดงวิธีใช้ข้อมูล 2 มิติที่เลื่อนได้ซึ่งบันทึกไว้และใช้ translationX และ translationY กับเนื้อหาที่มีขนาดใหญ่กว่าคอนเทนเนอร์หลัก
@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.scrollable2DModifier.scrollable2Dข้อมูลโค้ดข้างต้นประกอบด้วยข้อมูลต่อไปนี้
- คอนเทนเนอร์ได้รับการตั้งค่าให้มีขนาดคงที่ (
600x400dp) ในขณะที่เนื้อหามีขนาดใหญ่กว่ามาก (1200x800dp) เพื่อหลีกเลี่ยงไม่ให้มีการปรับขนาดเป็นขนาดของคอนเทนเนอร์ระดับบนสุด - ตัวแก้ไข
clipToBounds()ในคอนเทนเนอร์ช่วยให้มั่นใจได้ว่าส่วนใดก็ตามของ เนื้อหาขนาดใหญ่ที่อยู่นอกกรอบ600x400จะถูกซ่อนจากมุมมอง scrollable2Dจะไม่ย้ายเนื้อหาให้คุณโดยอัตโนมัติ ซึ่งแตกต่างจากคอมโพเนนต์ระดับสูง เช่นLazyColumnแต่คุณต้องใช้offsetที่ติดตามกับเนื้อหาโดยใช้การแปลงgraphicsLayerหรือออฟเซ็ตเลย์เอาต์- ภายในบล็อก
graphicsLayerบล็อกtranslationX = offset.value.xและtranslationY = offset.value.yจะเปลี่ยนตำแหน่งการวาดของรูปภาพหรือ ข้อความตามการเคลื่อนไหวของนิ้วมือ ซึ่งจะสร้างเอฟเฟกต์ภาพของการเลื่อน
ใช้การเลื่อนที่ซ้อนกันด้วย Scrollable2D
ตัวอย่างนี้แสดงวิธีผสานรวมคอมโพเนนต์แบบ 2 ทิศทางเข้ากับองค์ประกอบหลักมาตรฐานแบบ 1 มิติ เช่น ฟีดข่าวแนวตั้ง
โปรดคำนึงถึงประเด็นต่อไปนี้เมื่อติดตั้งใช้งานการเลื่อนที่ซ้อนกัน
- Lambda สำหรับ
rememberScrollable2DStateควรแสดงเฉพาะส่วนต่างที่ใช้แล้ว เพื่อให้รายการหลักเข้ามาแทนที่โดยอัตโนมัติเมื่อบัญชีย่อยถึงขีดจำกัด - เมื่อผู้ใช้ปัดในแนวทแยง ระบบจะแชร์ความเร็ว 2 มิติ หากองค์ประกอบย่อย ชนขอบเขตระหว่างภาพเคลื่อนไหว ระบบจะส่งต่อโมเมนตัมที่เหลือ ไปยังองค์ประกอบหลักเพื่อให้เลื่อนต่อไปได้อย่างเป็นธรรมชาติ
@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) ) } }
ในข้อมูลโค้ดก่อนหน้า
- คอมโพเนนต์ 2 มิติสามารถใช้การเคลื่อนที่แกน X เพื่อเลื่อนภายในขณะเดียวกันก็ส่งการเคลื่อนที่แกน Y ไปยังรายการหลักเมื่อถึงขอบเขตแนวตั้งของรายการย่อย
- ระบบจะคำนวณเดลต้าที่ใช้และส่งส่วนที่เหลือขึ้นไปตามลำดับชั้นแทนที่จะจำกัดผู้ใช้ไว้ในพื้นผิว 2 มิติ ซึ่งจะช่วยให้ผู้ใช้เลื่อนดูส่วนอื่นๆ ของหน้าเว็บได้ต่อไปโดยไม่ต้องยกนิ้ว
ใช้งาน Modifier.draggable2D
ใช้ตัวแก้ไข draggable2D เพื่อย้ายองค์ประกอบ UI แต่ละรายการ
ลากองค์ประกอบที่ประกอบกันได้
ตัวอย่างนี้แสดง Use Case ที่พบบ่อยที่สุดสำหรับ draggable2D ซึ่งก็คือการอนุญาตให้ผู้ใช้เลือกองค์ประกอบ UI และจัดตำแหน่งใหม่ที่ใดก็ได้ภายในคอนเทนเนอร์ระดับบน
@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) } } }
ข้อมูลโค้ดก่อนหน้ามีข้อมูลต่อไปนี้
- ติดตามตำแหน่งของกล่องโดยใช้
offsetสถานะ - ใช้ตัวปรับแต่ง
offsetเพื่อเปลี่ยนตำแหน่งของคอมโพเนนต์ตามเดลต้าการลาก - เนื่องจากไม่มีการรองรับการปัด กล่องจึงหยุดเคลื่อนที่ทันทีที่ผู้ใช้ ยกนิ้วขึ้น
ลาก Composable ขององค์ประกอบย่อยตามพื้นที่ลากขององค์ประกอบหลัก
ตัวอย่างนี้แสดงวิธีใช้ draggable2D เพื่อสร้างพื้นที่อินพุต 2 มิติ
ซึ่งจำกัดปุ่มหมุนของตัวเลือกไว้ภายในพื้นผิวที่เฉพาะเจาะจง การใช้งานนี้จะใช้เดลต้า 2 มิติเพื่อย้าย "ตัวเลือก" ที่ใช้ได้กับ Composable ย่อยในเครื่องมือเลือกสี ซึ่งแตกต่างจากตัวอย่างองค์ประกอบที่ลากได้ซึ่งย้ายคอมโพเนนต์เอง
@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) } ) ) } }
ข้อมูลโค้ดข้างต้นประกอบด้วยข้อมูลต่อไปนี้
- โดยใช้ตัวแก้ไข
onSizeChangedเพื่อบันทึกขนาดจริงของ คอนเทนเนอร์การไล่ระดับสี ตัวเลือกจะทราบตำแหน่งขอบอย่างแม่นยำ - ภายใน
graphicsLayerจะปรับtranslationXและtranslationYเพื่อให้แน่ใจว่าตัวเลือกจะอยู่ตรงกลางขณะลาก