使用者可透過清單功能,從 Wear OS 裝置上的一組選項中選取所需項目。
由於許多 Wear OS 裝置都採用圓形螢幕,導致使用者難以查看螢幕頂端和底部附近的清單項目,因此,Compose for Wear OS 納入了名為 TransformingLazyColumn 的 LazyColumn 類別版本,藉此支援縮放和變形動畫。項目移到邊緣時,會縮小並淡出。
如要套用建議的縮放和捲動效果,請按照下列步驟操作:
- 使用
Modifier.transformedHeight,讓 Compose 在項目捲動瀏覽畫面時,計算高度變化。 - 使用
transformation = SurfaceTransformation(transformationSpec)套用視覺效果,包括縮小項目內容。 - 對於不接受
transformation做為參數的元件 (例如Text),請使用自訂TransformationSpec。
以下動畫顯示清單元素在靠近螢幕頂端和底部時,如何縮放及變更形狀:
下列程式碼片段說明如何使用TransformingLazyColumn 版面配置建立清單,在各種 Wear OS 螢幕尺寸上呈現絕佳效果。
程式碼片段也示範了 minimumVerticalContentPadding 修飾符的用法,您應在清單項目上設定這個修飾符,以便在清單頂端和底部套用正確的邊框間距。
如要顯示捲動指標,請在 ScreenScaffold 和 TransformingLazyColumn 之間分享 columnState:
val columnState = rememberTransformingLazyColumnState() val transformationSpec = rememberTransformationSpec() ScreenScaffold( scrollState = columnState ) { contentPadding -> TransformingLazyColumn( state = columnState, contentPadding = contentPadding ) { item { ListHeader( modifier = Modifier .fillMaxWidth() .transformedHeight(this, transformationSpec) .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding), transformation = SurfaceTransformation(transformationSpec) ) { Text(text = "Header") } } // ... other items item { Button( modifier = Modifier .fillMaxWidth() .transformedHeight(this, transformationSpec) .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding), transformation = SurfaceTransformation(transformationSpec), onClick = { /* ... */ }, icon = { Icon( imageVector = Icons.Default.Build, contentDescription = "build", ) }, ) { Text( text = "Build", maxLines = 1, overflow = TextOverflow.Ellipsis, ) } } } }
新增貼齊與快速滑過效果
「貼齊」可確保使用者完成捲動或快速滑動手勢後,清單會停在特定位置,通常是螢幕中央。在圓形螢幕上,項目會隨著遠離中心而縮放及變形,因此特別適合使用對齊功能,確保最相關的項目在最佳觀看區域中保持完全可見且可讀。如要新增快速滑動行為,請將 flingBehavior 參數設為 TransformingLazyColumnDefaults.snapFlingBehavior(columnState)。使用 RotaryScrollableDefaults.snapBehavior(columnState) 將 rotaryScrollableBehavior 設為相符,以便在使用實體錶冠或錶圈時獲得一致的體驗。
val columnState = rememberTransformingLazyColumnState() ScreenScaffold(scrollState = columnState) { contentPadding -> TransformingLazyColumn( state = columnState, flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(columnState), rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(columnState) ) { // ... // ... } }
反向版面配置
根據預設,可捲動的清單會錨定至頂端。如果使用者捲動至標準清單底部,且清單尾端新增項目,清單會維持使用者目前檢視的項目。舉例來說,如果使用者在畫面底部查看項目 10,且系統新增項目 11,檢視畫面仍會聚焦於項目 10,而項目 11 會顯示在目前檢視畫面下方的畫面外。
對於訊息應用程式或即時記錄等用途,通常不希望出現這種行為。如果使用者已在清單底部,通常會想在收到新項目時立即查看最新內容。如果一次抵達多個項目,清單應略過並在底部顯示最新項目 (也就是說,除非使用者向上捲動,否則可能完全不會顯示某些中間項目)。
為支援這些用途,TransformingLazyColumn 可讓您設定 reverseLayout = true,反轉版面配置。這會將清單的錨點從頂端邊緣變更為底部邊緣。
為方便起見,設定 reverseLayout = true 也會反轉項目的視覺順序和捲動手勢的方向:
- 項目由下往上組成,也就是說,索引 0 會顯示在畫面底部。
- 向上捲動即可查看指數較高的項目。
如要新增快速捲動和甩動行為,並搭配反向版面配置,可以合併 flingBehavior 和 rotaryScrollableBehavior,如以下程式碼片段所示:
val columnState = rememberTransformingLazyColumnState() val transformationSpec = rememberTransformationSpec() ScreenScaffold(scrollState = columnState) { contentPadding -> TransformingLazyColumn( state = columnState, contentPadding = contentPadding, reverseLayout = true, modifier = Modifier.fillMaxWidth() ) { items(10) { index -> Button( label = { Text( text = "Item ${index + 1}" ) }, onClick = {}, modifier = Modifier .fillMaxWidth() .transformedHeight(this, transformationSpec) .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding), transformation = SurfaceTransformation(transformationSpec) ) } item { // With reverseLayout = true, the last item declared appears at the top. ListHeader( modifier = Modifier .fillMaxWidth() .transformedHeight(this, transformationSpec) .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding), transformation = SurfaceTransformation(transformationSpec) ) { Text("Header") } } } }
下圖顯示一般清單和反向清單的差異:

清單中的邊緣按鈕
如果是 Material 3,您可以新增 EdgeButton,這是位於清單底部的邊緣按鈕。不過,請注意不要將這個項目新增至 TransformingLazyColumn 中,而是使用 ScreenScaffold 中的 edgeButton 插槽。
使用 edgeButton 插槽可確保按鈕正確放置在畫面底部,並在捲動清單時正常運作。
下列程式碼片段說明如何實作 EdgeButton:
val columnState = rememberTransformingLazyColumnState() ScreenScaffold( scrollState = columnState, edgeButton = { EdgeButton( onClick = { /* TODO */ }, modifier = Modifier.scrollable( columnState, orientation = Orientation.Vertical, reverseDirection = true, // Apply overscroll to the EdgeButton for proper scrolling behavior. overscrollEffect = rememberOverscrollEffect(), ) ) { Text("More") } } ) { contentPadding -> TransformingLazyColumn( contentPadding = contentPadding, state = columnState, ) { // ... // ... } }
在清單中滑動顯示
SwipeToReveal 元件可讓您透過滑動手勢存取清單項目的動作,例如 Card 或 Chip。滑動通常會從側邊顯示一或兩個動作按鈕 (例如「刪除」或「更多」)。
在 TransformingLazyColumn 中使用 SwipeToReveal 時,請遵循下列準則:
- 捲動時重設:使用者捲動清單時,將所有已滑開的項目重設為遮蓋狀態。
- 高度一致:將動作按鈕高度設為與內部滑動項目 (
Button或Card) 相同,確保外觀一致。 - 轉換容器:將
transformedHeight修飾符和transformationSpec套用至SwipeToReveal元件本身。 - 請勿重複轉換:請勿對內部滑動項目 (
SwipeToReveal容器內的資訊卡或按鈕) 套用transformedHeight或transformation修飾符。
清單中的自訂可組合函式
為 TransformingLazyColumn 建構自訂介面元件時,請遵循下列最佳做法,確保可組合函式在螢幕邊緣附近順暢縮放、淡出及變形:
- 公開
SurfaceTransformation:接受選用的SurfaceTransformation參數 (預設為null),與標準 Wear OS Compose Material 3 元件 (例如Card和Button) 相符。這樣一來,TransformingLazyColumn中的呼叫端就能傳遞SurfaceTransformation(transformationSpec),同時讓元件在清單外正常運作。 - 在呼叫程式碼中先套用
Modifier.transformedHeight:將自訂可組合函式放在TransformingLazyColumn中時,請在呼叫程式碼的修飾符鏈結中,將Modifier.transformedHeight(this, transformationSpec)做為第一個修飾符傳遞。SurfaceTransformation會套用視覺縮放和淡出效果,但transformedHeight至關重要,因為它會告知清單版面配置重新計算項目縮小時的高度。 - 依序套用轉換層、呼叫端
modifier和繪圖師:- 容器轉換層:使用
Modifier.graphicsLayer和applyContainerTransformation()啟動根容器的修飾符鏈結,以便在縮放和傾斜的座標空間內繪製背景和內容。 - 呼叫端
modifier:套用呼叫端傳遞的modifier參數 (包括Modifier.transformedHeight),然後再進行任何內部大小調整或邊框間距。 - 沒有轉換時的形狀剪輯:如果
transformation是null,請在繪製背景前套用Modifier.clip(shape)。createContainerPainter()傳回的繪圖器會將自身裁剪成形狀,但一般繪圖器不會,因此如果沒有這個項目,背景就會在清單外繪製成方形角落。 - 變形背景繪圖師:使用
Modifier.drawBehind和從createContainerPainter()建立的繪圖師,在容器層內繪製背景。 - 內容轉換層:套用第二個
Modifier.graphicsLayer,並使用applyContentTransformation()將內容裁剪成容器形狀,讓內部內容在接近邊框時提早淡出。
- 容器轉換層:使用
下列程式碼片段顯示如何實作自訂 BoardingPassCard 可組合函式,依序套用這些轉換:
@Composable fun BoardingPassCard( flightNumber: String, origin: String, destination: String, gate: String, seat: String, departureTime: String, modifier: Modifier = Modifier, transformation: SurfaceTransformation? = null, shape: Shape = RoundedCornerShape(18.dp), statusBadge: @Composable () -> Unit = {} ) { // 1. Create morphing container painter val backgroundPainter = ColorPainter(MaterialTheme.colorScheme.surfaceContainer) val finalPainter = if (transformation != null) { remember(transformation, backgroundPainter, shape) { transformation.createContainerPainter(backgroundPainter, shape, border = null) } } else { backgroundPainter } Column( modifier = Modifier // 2a. Container layer: Scales, fades, and tilts the whole card surface .then( if (transformation != null) { Modifier.graphicsLayer { transformation.run { applyContainerTransformation() } } } else Modifier ) // 2b. Caller modifier: Includes Modifier.transformedHeight in a list .then(modifier) .fillMaxWidth() // 2c. Shape clip: Only needed without a transformation, because the // painter from createContainerPainter clips itself to the shape .then(if (transformation == null) Modifier.clip(shape) else Modifier) // 2d. Morphing background: Drawn inside the transformed container layer .drawBehind { with(finalPainter) { draw(size) } } // 2e. Content layer: Fades content earlier and clips children to shape .then( if (transformation != null) { Modifier.graphicsLayer { this.shape = shape this.clip = true transformation.run { applyContentTransformation() } } } else Modifier ) .padding(horizontal = 14.dp, vertical = 10.dp) ) { // Card content goes here } }
然後,您可以在 TransformingLazyColumn 內使用 BoardingPassCard,方法是將 Modifier.transformedHeight 做為第一個修飾符傳遞,並搭配 SurfaceTransformation(transformationSpec):
@Composable fun BoardingPassListSample(flights: List<FlightInfo>) { val listState = rememberTransformingLazyColumnState() val transformationSpec = rememberTransformationSpec() ScreenScaffold(scrollState = listState) { contentPadding -> TransformingLazyColumn( state = listState, contentPadding = contentPadding, modifier = Modifier.fillMaxSize() ) { items(flights.size) { index -> val flight = flights[index] BoardingPassCard( flightNumber = flight.number, origin = flight.origin, destination = flight.destination, gate = flight.gate, seat = flight.seat, departureTime = flight.time, modifier = Modifier .transformedHeight(this, transformationSpec) .minimumVerticalContentPadding( CardDefaults.minimumVerticalListContentPadding ), transformation = SurfaceTransformation(transformationSpec) ) } } } }
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- Compose for Wear OS 程式碼研究室
- 清單和格線
- 在 Compose 中使用 View