목록을 사용하면 사용자는 Wear OS 기기에서 여러 선택 항목 중 하나를 고를 수 있습니다.
많은 Wear OS 기기가 원형 화면을 사용하므로 화면의 상단과 하단 근처에 표시되는 목록 항목을 보기가 어렵습니다. 이러한 이유로 Wear OS용 Compose에는 크기 조정 및 모핑 애니메이션을 지원하는 TransformingLazyColumn이라는 LazyColumn 클래스의 한 가지 버전이 포함되어 있습니다.
항목이 가장자리로 이동하면 작아지고 흐려집니다.
추천되는 크기 조절 및 스크롤 효과를 적용하려면 다음 단계를 따르세요.
- Compose가 항목이 화면을 스크롤할 때 높이 변화를 계산할 수 있도록
Modifier.transformedHeight를 사용합니다. 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)과 일치하도록 설정합니다. - 컨테이너 변환:
SwipeToReveal구성요소 자체에transformedHeight수정자와transformationSpec를 적용합니다. - 이중 변환 금지: 내부 스와이프된 항목 (
SwipeToReveal컨테이너 내 카드 또는 버튼)에transformedHeight또는transformation수정자를 적용하지 마세요.
목록의 맞춤 컴포저블
TransformingLazyColumn용 맞춤 화면 구성요소를 빌드할 때는 컴포저블이 디스플레이 가장자리 근처에서 원활하게 확장되고, 페이드되고, 변형되도록 다음 권장사항을 따르세요.
SurfaceTransformation노출:Card,Button과 같은 표준 Wear Compose Material 3 구성요소와 일치하는 선택적SurfaceTransformation매개변수 (기본값은null)를 허용합니다. 이렇게 하면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()에서 생성된 페인터를 사용하여 컨테이너 레이어 내에 배경을 그립니다. - 콘텐츠 변환 레이어:
applyContentTransformation()로 두 번째Modifier.graphicsLayer를 적용하고 베젤에 접근할 때 내부 콘텐츠가 더 빨리 사라지도록 컨테이너 모양으로 클리핑합니다.
- 컨테이너 변환 레이어:
다음 스니펫은 이러한 변환을 순서대로 적용하는 맞춤 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 } }
그런 다음 Modifier.transformedHeight를 첫 번째 수정자로 전달하고 SurfaceTransformation(transformationSpec)와 함께 사용하여 TransformingLazyColumn 내에서 BoardingPassCard를 사용할 수 있습니다.
@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가 사용 중지되어 있으면 링크 텍스트가 표시됩니다.
- Wear OS용 Compose Codelab
- 목록 및 그리드
- Compose에서 뷰 사용