在 Jetpack Compose 中,scrollable2D 和 draggable2D 是低階修飾符,用於處理二維指標輸入。標準的 1D 修飾符 scrollable 和 draggable 只能用於單一方向,但 2D 變體可同時追蹤 X 軸和 Y 軸的移動。
舉例來說,現有的 scrollable 修飾符用於單一方向的捲動和甩動,而 scrollable2d 則用於 2D 的捲動和甩動。這可讓您建立更複雜的版面配置,在所有方向上移動,例如試算表或圖片檢視器。scrollable2d 修飾符也支援 2D 情境中的巢狀捲動。
選擇 scrollable2D 或 draggable2D
選擇合適的 API 取決於您要移動的 UI 元素,以及這些元素偏好的實體行為。
Modifier.scrollable2D:在容器上使用這個修飾符,即可移動容器內的內容。舉例來說,如果容器的內容需要在水平和垂直方向捲動,您就可以搭配地圖、試算表或相片檢視器使用。這項功能內建快速滑過支援,因此內容會在快速滑過後繼續移動,並與網頁上的其他捲動元件協調運作。
Modifier.draggable2D:使用這個修飾符移動元件本身。這是輕量修飾符,因此使用者手指停止移動時,移動也會立即停止。不支援投放功能。
如要讓元件可拖曳,但不需要快速滑過或巢狀捲動支援,請使用 draggable2D。
導入 2D 修飾符
以下各節提供範例,說明如何使用 2D 修飾符。
實作 Modifier.scrollable2D
如果使用者需要在所有方向移動內容,請對容器使用這個修飾符。
擷取 2D 動作資料
這個範例說明如何擷取原始 2D 動作資料,並顯示 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元件會顯示該offset狀態的目前 X 和 Y 值,並在使用者拖曳時即時更新。
平移大型可視區域
這個範例說明如何使用擷取的 2D 可捲動資料,並將 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.scrollable2D 建立。Modifier.scrollable2D 建立。上述程式碼片段包含下列項目:
- 容器設為固定大小 (
600x400dp),而內容的大小則遠大於容器 (1200x800dp),避免內容調整為父項大小。 - 容器上的
clipToBounds()修飾符可確保系統會隱藏600x400方塊外的任何大型內容。 - 與
LazyColumn等高階元件不同,scrollable2D不會自動為您移動內容。您必須改用graphicsLayer轉換或版面配置偏移,將追蹤的offset套用至內容。 - 在
graphicsLayer區塊內,translationX = offset.value.x和translationY = offset.value.y會根據手指移動的距離,位移圖片或文字的繪圖位置,產生捲動的視覺效果。
使用 scrollable2D 實作巢狀捲動
這個範例說明如何將雙向元件整合至標準一維父項,例如直向新聞動態消息。
實作巢狀捲動時,請注意下列幾點:
rememberScrollable2DState的 lambda 應只傳回已取用的差異,讓父項清單在子項達到限制時自然接管。- 使用者執行對角線甩動時,系統會分享 2D 速度。如果子項在動畫期間抵達邊界,剩餘動量會傳播至父項,以繼續自然捲動。
@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) ) } }
在上述程式碼片段中:
- 2D 元件可消耗 X 軸移動,在內部平移,同時在達到子項本身的垂直界線後,將 Y 軸移動分派至父項清單。
- 系統不會將使用者限制在 2D 表面,而是會計算所消耗的差異,並將餘數傳遞至階層。確保使用者不必鬆開手指,就能繼續捲動頁面。
實作 Modifier.draggable2D
使用 draggable2D 修飾符移動個別 UI 元素。
拖曳可組合函式元素
這個範例顯示 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修飾符,根據拖曳增量調整元件位置。 - 由於不支援輕拂手勢,使用者一放開手指,方塊就會立即停止移動。
根據父項的拖曳區域拖曳子項可組合項
這個範例說明如何使用 draggable2D 建立 2D 輸入區域,將選取器旋鈕限制在特定表面內。與移動元件本身的
可拖曳元素範例不同,這項實作會使用 2D 增量,在顏色挑選器中移動子項可組合項「選取器」:
@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,確保選取器在拖曳時保持置中。