在 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
如要瞭解如何使用 Modifier.draggable2D,請參閱「使用 Modifier.draggable2D 拖曳兩個維度」。