[정리] Compose 가이드 문서 ~ Develop your app's layout

[정리] Compose 가이드 문서 ~ Develop your app's layout

May 12, 2024. | By: pluulove


Layout basics

Compose에서 레이아웃의 목표

  • 고성능
  • Custom 레이아웃을 쉽게 작성하기

Composable 함수의 기본 사항

Composable 함수 : Compose의 기본 구성 요소. UI의 일부를 정의하는 Unit을 리턴하는 함수

기본 레이아웃 컴포넌트

  • Column : 세로 배치
    • 하위 element를 배치 속성 : verticalArrangement/horizontalAlignment
  • Row : 가로 배치
    • 하위 element를 배치 속성 : horizontalArrangement/verticalAlignment
  • Box : 컴포넌트를 상위로 배치 가능. 포함된 element의 특정 정렬을 구성하는 기능 지원

레이아웃 모델

레이아웃 모델에서 UI 트리는 단일 패스로 배치된다. 각 노드는 자신을 측정한 다음 모든 하위 element를 재귀적으로 측정하여 트리 아래로 크기 제약 조건을 하위 element에게 전달한다. 다음으로 리프 노드의 크기와 배치가 결정되고, 결정된 크기와 배치 정의가 트리 위로 다시 전달된다.

  • 부모는 자식보다 먼저 측정(measure)하지만, 자식 다음에 크기를 측정하고 배치(place)한다.
@Composable
fun SearchResult() {
   Row {
      Image(
         // ...
      )
      Column {
         Text(
            // ...
         )
         Text(
            // ...
         )
      }
   }
}

위 코드로 생성된 UI 트리는 다음과 같다.

SearchResult
  Row
    Image
    Column
      Text
      Text

성능

Compose는 하위 element를 한 번만 측정하여 높은 성능을 달성한다. 단일 패스 측정은 성능에 유리하며, Compose가 깊은 UI 트리를 효율적으로 처리할 수 있게 해준다. 레이아웃에 여러 번의 측정이 필요한 경우, Compose 레이아웃의 내장 기능 측정을 사용할 수 있다.

측정(measurement)과 배치(measurement)는 레이아법웃 단계의 개별 하위 단계이므로 측정이 아닌 항목 배치에만 영향을 미치는 변경 사항은 별도로 실행할 수 있다.

레이아웃에서 Modifier 사용

Modifier를 사용하여 Composable에 기능을 더 할 수 있다. Modifier는 레이아웃을 커스텀하는 데 필수적이다.

Column(
   Modifier
      .clickable(onClick = /*...*/)
      .padding(/*...*/)
      .fillMaxWidth()
) {
   /*...*/
}
  • clickable : Composable이 사용자 입력에 반응하고 Ripple을 표시
  • padding : element 주위에 공백을 추가
  • fillMaxWidth : Composable이 부모가 제공하는 최대 너비로 채우기
  • size() : element의 기본 너비와 높이를 지정

반응형 레이아웃

레이아웃은 다양한 화면 방향과 폼 팩터 크기를 고려하여 디자인해야 함

Constraints

부모에서 오는 제약 조건 확인하고 레이아웃을 디자인하려면 BoxWithConstraints를 사용

측정 제약 조건을 사용하여 다양한 다른 레이아웃을 구성할 수 있다

@Composable
fun WithConstraintsComposable() {
   BoxWithConstraints {
      /*...*/
   }
}

슬롯 기반 레이아웃

Material Component는 슬롯 API를 사용

  • Composable 위에 커스터마이징 레이어를 추가하기 위해 Compose에서 도입한 패턴
  • 하위 element의 모든 Configuration 파라미터를 노출할 필요 없이 자체적으로 구성할 수 있는 하위 element를 허용하여 컴포넌트를 유연하게 만든다.
  • 슬롯은 개발자가 원하는 대로 채울 수 있도록 UI에 빈 공간을 남겨둔다다.

TopAppBar에서 커스텀할 수 있는 슬롯의 위와 같다

Composable은 content Composable Lambda( content: @Composable () -> Uni)를 사용한다. 슬롯 API는 특정 용도를 위해 여러 콘텐츠 파라매터를 노출한다.

TopAppBar는 title, navigationIcon, actions을 제공하고 있다.

Scaffold를 사용하면 기본 머티리얼 디자인 레이아웃 구조로 UI를 구현 가능하다.

@Composable
fun HomeScreen(/*...*/) {
    ModalNavigationDrawer(drawerContent = { /* ... */ }) {
        Scaffold(
            topBar = { /*...*/ }
        ) { contentPadding ->
            // ...
        }
    }
}

Modifiers

Modifier로 가능한 것들

  • Composable의 크기, 레이아웃, 동작 및 모양 변경
  • 접근성 레이블과 같은 정보 추가
  • 사용자 입력 처리
  • Element 클릭, 스크롤, 드래그, 확대/축소 등

모든 Composable이 Modifier 파라미터로 전달 및 UI에 적용하는 것은 첫 번째 하위 element로 하는 것을 권장

API 가이드라인 : Elements accept and respect a Modifier parameter

Modifier의 순서가 중요

각 Modifier 함수는 이전 함수에서 반환된 값을 변경

내장 Modifier

  • size : 크기를 설정
  • requiredSize : 제약 조건에 무관하게 Composable 크기 고정 (부모 제약이 설정되어 있어도 requiredSize Modifier가 우선)
  • fillMaxSize, fillMaxHeight, fillMaxWidth : 자식 레이아웃이 부모 레이아웃이 허용하는 모든 크기를 채우기
  • padding : element 주위에 패딩을 추가
  • offset : 원래 위치에서 x축/y축에 오프셋을 적용. 오프셋은 양수/음수 지원
    • padding과 offset의 차이점 : Composable에 offset을 추가해도 측정값이 변경되지 않는다

Compose의 범위 안전성

Compose에는 특정 Composable의 하위 항목에 적용될 때만 사용할 수 있는 Modifier가 있다.

기존 Android View 시스템에는 범위 안전이 없다

Box의 matchParentSize

  • Box 크기에 영향을 주지 않고 하위 element 레이아웃을 부모 Box와 같은 크기 만들기
  • fillMaxSize을 사용하는 경우, parent가 사용 가능한 모든 공간을 차지함

Row/Column의 weight

  • RowScope/ColumnScope에서만 사용
  • 부모 내에서 Composable내에서 각 항목의 가중치를 두어 크기를 정의 가능

Modifier 추출 및 재사용

때로는 동일한 Modifier 체인 인스턴스를 변수로 추출하여 여러 Composable에서 재사용하는 것이 유용할 수 있다.

  • Modifier를 사용하는 Composable에 대해 Recomposition이 발생할 때 Modifier의 재할당이 반복되지 않는다
  • Modifier 체인은 길고 복잡. 동일한 체인 인스턴스를 재사용하면 Compose 런타임에 비교 시 수행해야 하는 작업 부하를 완화 가능
  • 전반적으로 코드 정리, 일관성 및 유지 보수성에 유용
val reusableModifier = Modifier
   .fillMaxWidth()
   .background(Color.Red)
   .padding(12.dp)

자주 변화하는 상태를 관찰할 때 Modifier를 추출하고 재사용

애니메이션/스크롤 상태와 같이 Composable 내에서 자주 변경되는 상태를 관찰할 때 많은 양의 Recomposition이 수행되며, 모든 Recomposition 및 모든 프레임에 대해 Modifier가 할당된다.

@Composable
fun LoadingWheelAnimation() {
   val animatedState = animateFloatAsState(/*...*/)

   LoadingWheel(
      // 이 Modifier의 생성/할당은 애니메이션의 모든 프레임에서 발생
      modifier = Modifier
         .padding(12.dp)
         .background(Color.Gray),
      animatedState = animatedState
   )
}

범위(Scope)가 지정되지 않은 Modifier 추출 및 재사용

val reusableItemModifier = Modifier
   .padding(bottom = 12.dp)
   .size(216.dp)
   .clip(CircleShape)

@Composable
private fun AuthorList(authors: List<Author>) {
   LazyColumn {
      items(authors) {
         AsyncImage(
            // ...
            modifier = reusableItemModifier,
         )
      }
   }
}

추출된 Scope Modifier는 동일한 범위의 직계 하위 항목에만 전달해야 한다

Column(modifier = Modifier.fillMaxWidth()) {
   // Weight Modifier는 Composable ColumnScope로 지정
   val reusableItemModifier = Modifier.weight(1f)

   // Column의 직계 하위 항목
   Text1(
      modifier = reusableItemModifier
      // ...
   )

   Box {
      Text2(
         // Text Composable은 Column의 직접적인 자식이 아니다.
         // Weight는 여기서 아무 작업도 수행하지 않는다.
         modifier = reusableItemModifier
         // ...
      )
   }
}

추출된 Modifier의 추가 연결

.then() 함수를 사용하여 추출된 Modifier 체인을 추가로 연결하거나 추가 가능

val reusableModifier = Modifier
   .fillMaxWidth()
   .background(Color.Red)
   .padding(12.dp)

// reusableModifier에 추가
reusableModifier.clickable { /*...*/ }

// otherModifier에 reusableModifier를 추가
otherModifier.then(reusableModifier)

제약 조건 및 수정자 순서

UI 트리의 Modifier

UI 트리의 레이아웃 노드를 래핑하는 수정자 Modifier 체인으로 시각화한 UI 트리
  • Modifier는 단일 Modifier 또는 레이아웃 노드를 래핑
  • 레이아웃 노드는 여러 하위 노드를 배치 가능

레이아웃 단계의 제약

각 단계에서 레이아웃 노드의 너비, 높이 및 x, y 좌표를 찾는다

  1. 하위 측정 : 노드는 하위가 있는 경우 측정
  2. 자체 크기 결정 : 해당 측정값을 기반으로 노드는 자체 크기를 결정
  3. Place children : 각 하위 노드는 노드 자체 위치를 기준으로 배치

제약조건 유형

  • Bounded : 노드에는 최대 및 최소 너비와 높이가 존재
  • Unbounded : 노드의 크기가 제한 X, 최대 너비 및 높이 경계가 무한대로 설정
  • Exact : 노드는 정확한 크기를 요청. 최소 및 최대 경계는 동일한 값으로 설정
  • Combination : 노드는 위의 제약 유형의 조합

제약조건에 영향을 미치는 Modifier

  • size : 콘텐츠의 기본 크기를 선언
    • 전달된 제약 조건을 준수하면서 전달된 제약 조건에 최대한 가깝게 일치시킴
  • requiredSize : 사용자가 지정한 크기를 정확한 경계로 전달
  • width/height : 제약 조건의 width/height를 적용
  • sizeIn : width/height에 대한 최소/최대 제약 조건을 설정
  • fillMaxSize : 최소 width/height를 모두 최대 값으로 설정
  • wrapContentSize : 자식에게 전달된 사용 가능한 최소 범위의 중앙에 배치
    • 부모에게 전달되는 크기는 최소 bound와 동일

size Modifier가 50dp의 크기를 사용하려고 해도 부모로부터 전달되는 최소 제약 조건을 준수해야 함. 따라서 size Modifier의 값은 무시하고 300x200의 정확한 제약 조건 bound를 출력. 이미지는 300x200의 크기를 전달

Custom modifiers

Modifier의 구성

  • Modifier factory : Compose에서 UI를 수정하는 데 사용되는 Modifier element를 생성
  • Modifier element : Modifier의 동작 구현

기존 Modifier와 연결

기존 Modifier를 사용하여 커스텀 Modifier 생성 가능

// Modifier#clip의 구현
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)

// 반복 사용되는 경우
fun Modifier.myBackground(color: Color) = padding(16.dp)
   .clip(RoundedCornerShape(8.dp))
   .background(color)

Composable modifier factory를 사용하여 Modifier 만들기

Composable modifier factory : Composable 함수를 사용하여 Modifier를 만들어 전달 가능

  • 예) enable 여부에 따라 fade 애니메이션을 표시하는 Modifier
@Composable
fun Modifier.fade(enable: Boolean): Modifier {
   val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f)
   return this then Modifier.graphicsLayer { this.alpha = alpha }
}

CompositionLocal를 Composable modifier factory에서도 사용할 수 있지만, 생성 시점의 Composition tree의 값이 사용된다

@Composable
fun Modifier.myBackground(): Modifier {
   val color = LocalContentColor.current
   return this then Modifier.background(color.copy(alpha = 0.5f))
}

@Composable
fun MyScreen() {
   CompositionLocalProvider(LocalContentColor provides Color.Green) {
      // Background modifier 생성 (Background는 Green)
      val backgroundModifier = Modifier.myBackground()

      // LocalContentColor를 Red로 업데이트
      CompositionLocalProvider(LocalContentColor provides Color.Red) {

         // Red가 아닌 Green이 background로 적용
         Box(modifier = backgroundModifier)
      }
   }
}

Composable function modifier는 Skip하지 않음

반환값이 있는 Composable 함수는 Skip이 불가능하므로 Composable modifier factory도 Skip하지 않음.

  • Modifier function은 Recomposition될 때마다 호출. 빈번한 Recomposition에 비용이 많이 듬

Composable function modifier의 호출 위치

  • Composable function modifier : Composable 함수에서만 호출 가능
  • non-composable function modifier : Composable 함수 외부에서도 호출 가능. 재사용이 용이하며 성능 향상

Modifier.Node를 사용하여 Custom Modifier 구현

Modifier.Node는 Compose에서 Modifier를 만들기 위한 하위 수준의 API. 가장 성능이 좋은 방법

composed {}는 성능 문제로 권장하지 않는 API

Compose Modifiers Deep Dive

Modifier.Node를 사용한 구현

  1. Modifier의 로직과 상태를 보관하는 Modifier.Node 구현
  2. ModifierNodeElement 노드 인스턴스를 생성하고 업데이트
  3. Modifier Factory

ModifierNodeElement와 비교

  • ModifierNodeElement : 상태가 없으며 Recomposition할 때마다 새 인스턴스가 할당
  • Modifier.Node : 상태 저장형. Recomposition에서 복원/재사용 가능

Modifier.Node

예) 원을 그리는 Custom Modifier

private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
   override fun ContentDrawScope.draw() {
      drawCircle(color)
   }
}

Modifier의 필요에 따라서 기능을 재정의 가능

Node Usage Sample Link
LayoutModifierNode Wrapping된 콘텐츠를 측정/배치하는 방식을 변경 Sample
DrawModifierNode Layout 공간에 그리기 Sample
CompositionLocalConsumerModifierNode Modifier.Node에서 CompositionLocal을 읽기 가능 Sample
SemanticsModifierNode 테스트, 접근성에서 사용 가능한 semantics key/value를 추가 Sample
PointerInputModifierNode PointerInputChanges를 수신 Sample
ParentDataModifierNode 부모 레이아웃에 데이터를 제공 Sample
LayoutAwareModifierNode onMeasured/onPlaced 콜백을 수신 Sample
GlobalPositionAwareModifierNode 콘텐츠의 Global 위치가 변경되었을 때 레이아웃의 최종 레이아웃 좌표가 포함된 onGloballyPositioned 콜백을 수신 Sample
ObserverModifierNode ObserverNode를 구현하는 Modifier.Node
observeReads 블록 내에서 읽은 스냅샷 객체의 변경시에 대한 응답으로 호출되는 onObservedReadsChanged의 자체 구현을 제공 가능
Sample
DelegatingNode 다른 Modifier.Node 인스턴스에 작업을 위임할 수 있는 Modifier.Node
여러 노드 구현을 하나로 구성하는 데 유용
Sample
TraversableNode Modifier.Node 클래스가 동일한 유형의 클래스 또는 특정 키에 대해 노드 트리를 위/아래로 탐색 Sample

ModifierNodeElement

커스텀 Modifier를 생성/업데이트하기 위한 데이터를 보관하는 불변 클래스

예) Node의 색상을 변경

private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
   override fun create() = CircleNode(color)

   override fun update(node: CircleNode) {
      node.color = color
   }
}
  • create: Modifier가 처음 적용될 때 노드를 생성하기 위해 호출. 일반적으로 노드를 구성하고 modifier factory에 전달되는 파라미터로 노드를 구성하는 데 사용
  • update: 노드가 이미 존재하지만 속성이 변경된 지점에 이 Modifier가 제공될 때마다 호출.
    • 클래스의 equals 메소드에 의해 호출이 결정
    • 노드의 속성을 업데이트된 파라미터와 일치하도록 업데이트해야 함. 노드를 재사용할 수 있는 기능은 Modifier.Node가 제공하는 성능 향상의 핵심이므로 update 메소드에서 새 노드를 생성하지 말고 기존 노드를 업데이트해야 함.

Modifier factory

Modifier의 공용 API

fun Modifier.circle(color: Color) = this then CircleElement(color)

일반적인 Modifier.Node 케이스

파라미터가 없는 상황

fun Modifier.fixedPadding() = this then FixedPaddingElement

data object FixedPaddingElement : ModifierNodeElement<FixedPaddingNode>() {
   override fun create() = FixedPaddingNode()
   override fun update(node: FixedPaddingNode) {}
}

class FixedPaddingNode : LayoutModifierNode, Modifier.Node() {
   private val PADDING = 16.dp

   override fun MeasureScope.measure(
      measurable: Measurable,
      constraints: Constraints
   ): MeasureResult {
      val paddingPx = PADDING.roundToPx()
      val horizontal = paddingPx * 2
      val vertical = paddingPx * 2

      val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))

      val width = constraints.constrainWidth(placeable.width + horizontal)
      val height = constraints.constrainHeight(placeable.height + vertical)
      return layout(width, height) {
         placeable.place(paddingPx, paddingPx)
      }
   }
}

CompositionLocal 참조

Modifier.Node Modifier는 CompositionLocal과 같은 Composition 상태 객체의 변경 사항을 자동으로 관찰하지 않는다.

  • Modifier에 비해 갖는 장점 : Modifier가 할당된 위치가 아닌 UI 트리에서 Modifier가 사용된 위치에서 currentValueOf를 사용하여 CompositionLocal의 값을 읽을 수 있다는 점입니다.

CompositionLocal 변경에 자동으로 반응하려면 Scope 내에서 현재 값을 읽어야 한다

예) LocalContentColor의 값을 관찰하여 해당 색상에 따라 배경을 그린다. ContentDrawScope는 스냅샷 변경 사항을 관찰하므로 LocalContentColor 값이 변경되면 자동으로 다시 그린다

class BackgroundColorConsumerNode :
   Modifier.Node(),
   DrawModifierNode,
   CompositionLocalConsumerModifierNode {
   override fun ContentDrawScope.draw() {
      val currentColor = currentValueOf(LocalContentColor)
      drawRect(color = currentColor)
      drawContent()
   }
}
  • ObserverModifierNode : Scope 외부의 상태 변경에 반응하고 Modifier를 자동으로 업데이트 가능

Modifier.scrollable에서 LocalDensity의 변화를 관찰

class ScrollableNode :
   Modifier.Node(),
   ObserverModifierNode,
   CompositionLocalConsumerModifierNode {

   // Place holder Behavior는 작은 밀도를 사용할 수 있을 때 초기화
   val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity))

   override fun onAttach() {
      updateDefaultFlingBehavior()
      observeReads { currentValueOf(LocalDensity) } // Density 변경 모니터링
   }

   override fun onObservedReadsChanged() {
      // Density가 변경되면 defaultFlingBehavior를 업데이트
      updateDefaultFlingBehavior()
   }

   private fun updateDefaultFlingBehavior() {
      val density = currentValueOf(LocalDensity)
      defaultFlingBehavior.flingDecay = splineBasedDecay(density)
   }
}

애니메이션 Modifier

CoroutineScope에 접근하여 Animatable Composable API를 사용 가능

예) fade in/fade out을 반복하는 CircleNode를 수정

class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode {
   private val alpha = Animatable(1f)

   override fun ContentDrawScope.draw() {
      drawCircle(color = color, alpha = alpha.value)
      drawContent()
   }

   override fun onAttach() {
      coroutineScope.launch {
         alpha.animateTo(
            0f,
            infiniteRepeatable(tween(1000), RepeatMode.Reverse)
         ) {
         }
      }
   }
}

위임을 사용하여 Modifier 간에 상태 공유

Modifier.Node Modifier는 다른 노드에 위임 가능. Modifier 간에 공통 상태를 공유하는 데에도 사용 가능

예) 상호작용 데이터를 공유하는 클릭 가능한 Modifier 노드의 기본 구현

class ClickableNode : DelegatingNode() {
   val interactionData = InteractionData()
   val focusableNode = delegate(
      FocusableNode(interactionData)
   )
   val indicationNode = delegate(
      IndicationNode(interactionData)
    )
}

노드 자동 무효화 선택 해제

해당 ModifierNodeElement 호출이 업데이트되면 자동으로 무효화된다. 복잡한 Modifier에서는 선택 해제하여 Modifier가 단계를 무효화하는 시점을 보다 세밀하게 제어 가능

Custom Modifier가 레이아웃과 그리기를 모두 수정하는 경우 유용. 자동 무효화를 선택 해제하면 색상과 같은 그리기 관련 속성만 변경될 때 그리기만 무효화하고 레이아웃은 무효화하지 않을 수 있습니다. Modifier의 성능이 향상된다.

예) 색상, 크기 및 onClick 람다를 속성으로 가진 Modifier이며 필요한 것만 무효화하고 필요하지 않은 무효화는 건너뛴다

class SampleInvalidatingNode(
   var color: Color,
   var size: IntSize,
   var onClick: () -> Unit
) : DelegatingNode(), LayoutModifierNode, DrawModifierNode {
   override val shouldAutoInvalidate: Boolean
      get() = false

   private val clickableNode = delegate(
      ClickablePointerInputNode(onClick)
   )

   fun update(color: Color, size: IntSize, onClick: () -> Unit) {
      if (this.color != color) {
         this.color = color
         // 색상이 변결될 때 Draw를 무효화
         invalidateDraw()
      }

      if (this.size != size) {
         this.size = size
         // 사이즈가 변경될 때 Layout을 무효화 
         invalidateMeasurement()
      }

      // onClick만 변경되면 아무 것도 무효화하지 않음
      clickableNode.update(onClick)
   }

   override fun ContentDrawScope.draw() {
      drawRect(color)
   }

   override fun MeasureScope.measure(
      measurable: Measurable,
      constraints: Constraints
   ): MeasureResult {
      val size = constraints.constrain(size)
      val placeable = measurable.measure(constraints)
      return layout(size.width, size.height) {
         placeable.place(0, 0)
      }
   }
}

Compose modifiers 항목들

개인적으로 자주 쓰일 것으로 보이는 항목만 남김

Action

  • anchoredDraggable : 정의 상태에서의 드래그 제스처를 사용하도록 설정
  • clickable : 입력/접근성 “클릭” 이벤트를 통해 클릭을 수신하는 Component
  • draggable : 단일 방향에서 UI element에 대한 터치 드래그 구성
  • swipeable : 정의 상태에서의 스와이프 동작 활성화

Alignment

Animation

Border : element의 border 적용

Drawing

  • alpha : 알파 적용 (0 ~ 1)
  • background : 콘텐츠 뒤에 단색으로 도형 그리기
  • clip : 콘텐츠를 clip 처리
  • drawBehind : 콘텐츠 뒤에 있는 캔버스에 그리기
  • drawWithCache : Draw 영역의 크기가 동일하거나 읽은 상태 개체가 변경되지 않는 한 Draw 호출 간에 유지되는 콘텐츠. DrawScope에 그리기를 한다.
  • drawWithContent : 콘텐츠 앞/뒤에 그릴 수 있는 DrawModifier를 생성
  • shadow : 그림자를 그리는 graphicsLayer 생성

Graphics

  • graphicsLayer : 콘텐츠를 draw layer에 그리도록 만드는 Modifier.Element

Layout

  • layoutId : element에 layoutId를 태그하여 부모 내의 element를 식별
  • layout : wrapping된 element의 측정/배치 방식을 변경할 수 있는 LayoutModifier를 생성
  • onGloballyPositioned : 콘텐츠의 Global position이 변경되었을 수 있는 경우, element의 레이아웃 좌표와 함께 onGloballyPositioned를 호출

Padding

  • padding : 콘텐츠의 각 가장자리(시작, 위쪽, 끝, 아래쪽)에 추가 공간을 적용
  • imePadding : ime insets을 위한 패딩을 추가
  • navigationBarsPadding : navigation bars insets을 위한 패딩을 추가
  • statusBarsPadding : status bars insets을 위한 패딩을 추가
  • systemBarsPadding : system bars insets을 위한 패딩을 추가 (statusBars/captionBar/navigationBars 포함)

Position

  • offset : 콘텐츠 x/y 좌표를 오프셋
  • TabRowDefaults > tabIndicatorOffset : TabRow 내에서 사용 가능한 모든 너비를 차지하는 Modifier. currentTabPosition에 따라 적용되는 indicator 오프셋을 애니메이션화

Semantics

  • semantics : 테스트, 접근성에 사용할 수 있도록 레이아웃 노드에 시맨틱 키/값 쌍을 추가

Scroll

Size

Testing

  • testTag : element를 테스트에서 찾을 수 있도록 태그를 적용

Transformations

Other

  • blur : 지정된 반경으로 콘텐츠를 흐리게 그리기

Pager

콘텐츠를 좌우/상하로 넘기려면 HorizontalPager/VerticalPager Composable을 사용. ViewPager와 유사

  • 각 페이지는 필요할 때만 느리게 구성/배치
  • beyondBoundsPageCount : 더 많은 페이지를 로드하려면 값 수정

Pager는 아직 실험적 기능

HorizontalPager

val pagerState = rememberPagerState(pageCount = {
   10
})
HorizontalPager(state = pagerState) { page ->
   Text(
      text = "Page: $page",
      modifier = Modifier.fillMaxWidth()
   )
}

VerticalPager

val pagerState = rememberPagerState(pageCount = {
   10
})
VerticalPager(state = pagerState) { page ->
   Text(
      text = "Page: $page",
      modifier = Modifier.fillMaxWidth()
   )
}

특정 항목으로 스크롤

  1. rememberPagerState()PagerState 객체 생성 후, Pager의 state로 전달
  2. PagerState#scrollToPage 함수로 원하는 페이지 번호 전달
  3. (애니메이션) PagerState#animateScrollToPage() 함수 사용
val pagerState = rememberPagerState(pageCount = {
   10
})
HorizontalPager(state = pagerState) { page ->
   Text(
      text = "Page: $page",
      modifier = Modifier
         .fillMaxWidth()
         .height(100.dp)
   )
}

// scroll to page
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
   coroutineScope.launch {
      // Call scroll to on pagerState
      pagerState.scrollToPage(5)
   }
}, modifier = Modifier.align(Alignment.BottomCenter)) {
   Text("Jump to Page 5")
}

페이지 상태 변경에 대한 알림 받기

PagerState 속성

  • currentPage: snap position에 가장 가까운 페이지. 기본적으로 snap position는 레이아웃 시작 부분
  • settledPage: 애니메이션/스크롤이 되지 않을 때의 페이지 번호.
    • 페이지가 snap 위치에 충분히 가까우면 currentPage는 즉시 업데이트.
    • settledPage는 모든 애니메이션 실행이 완료될 때까지 동일하게 유지
  • targetPage: 스크롤 동작에 대해 제안된 정지 위치

snapshotFlow 함수를 사용하여 변수의 변경 사항을 관찰하여 대응 가능

val pagerState = rememberPagerState(pageCount = {
   10
})

LaunchedEffect(pagerState) {
   // currentPage를 읽는 snapshotFlow에서 수집
   snapshotFlow { pagerState.currentPage }.collect { page ->
      // 페이지시 특정 작업을 실행
      Log.d("Page change", "Page changed to $page")
   }
}

VerticalPager(
   state = pagerState,
) { page ->
   Text(text = "Page: $page")
}

page indicator

PageState를 사용해 page indicator 그리는데 사용한다

val pagerState = rememberPagerState(pageCount = {
   4
})
/** HorizontalPager */

// 커스텀 page indicator
Row(...) {
   repeat(pagerState.pageCount) { iteration ->
      val color = if (pagerState.currentPage == iteration) Color.DarkGray else Color.LightGray
      Box(...)
   }
}

스크롤 효과 적용

PagerState.currentPageOffsetFraction : 현재 선택된 페이지에서 얼마나 멀리 떨어져 있는지 알려줌 (-0.5 ~ 0.5)

val pagerState = rememberPagerState(pageCount = {
   4
})
HorizontalPager(state = pagerState) { page ->
   Card(
      Modifier
         .size(200.dp)
         .graphicsLayer {
            // 스크롤 위치에서 현재 페이지의 절대 오프셋을 계산
            // 양방향의 모든 효과를 미러링 할 수 있는 절대값을 사용
            val pageOffset = (
               (pagerState.currentPage - page) + pagerState.currentPageOffsetFraction
            ).absoluteValue

            // 알파를 50% ~ 100% 사이로 애니메이션 처리
            alpha = lerp(
               start = 0.5f,
               stop = 1f,
               fraction = 1f - pageOffset.coerceIn(0f, 1f)
            )
         }
   ) {
      // Card content
   }
}

커스텀 페이지 크기

기본 각 페이지는 전체 너비/높이를 차지.

  • pageSize 변수를 Fixed, Fill(기본값) 또는 커스텀으로 설정
val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    pageSize = PageSize.Fixed(100.dp)
) { page ->
    // page content
}

ViewPort 기준으로 페이지 크기를 조정

  • 예) Custom PageSize 객체를 만들고, 항목 사이의 간격을 고려하여 availableSpace를 3으로 나누기
private val threePagesPerViewport = object : PageSize {
   override fun Density.calculateMainAxisPageSize(
      availableSpace: Int,
      pageSpacing: Int
   ): Int {
      return (availableSpace - 2 * pageSpacing) / 3
   }
}

콘텐츠 padding

콘텐츠의 padding 변경을 지원

val pagerState = rememberPagerState(pageCount = {
   4
})
HorizontalPager(
   state = pagerState,
   contentPadding = PaddingValues(horizontal = 32.dp),
) { page ->
   // page content
}

커스텀 스크롤 동작

pagerSnapDistance 또는 flingBehaviour와 같은 기본값을 변경 가능

Snap distance

기본적으로 fling 제스처로 스크롤할 수 있는 최대 페이지 수를 한 번에 한 페이지로 설정.

val pagerState = rememberPagerState(pageCount = { 10 })

val fling = PagerDefaults.flingBehavior(
   state = pagerState,
   pagerSnapDistance = PagerSnapDistance.atMost(10)
)

Column(modifier = Modifier.fillMaxSize()) {
   HorizontalPager(
      state = pagerState,
      pageSize = PageSize.Fixed(200.dp),
      beyondBoundsPageCount = 10,
      flingBehavior = fling
   ) {
      PagerSampleItem(page = it)
   }
}

Flow layouts

FlowRow/FlowColumn는 아직 실험적 기능

@Composable
private fun FlowRowSimpleUsageExample() {
   FlowRow(modifier = Modifier.padding(8.dp)) {
      ChipItem("Price: High to Low")
      ChipItem("Avg rating: 4+")
      ChipItem("Free breakfast")
      ChipItem("Free cancellation")
      ChipItem("£50 pn")
   }
}

첫 번째 행에 더 이상 공간이 없을 때, 자동으로 다음 행으로 이동하는 UI

flow layout의 특징

main axis 배열

main axis는 항목이 배치되는 축이며 배열을 다양하게 설정 가능

  • FlowColumn: Arrangement.Top이 기본값

예) FlowRow의 일부 배열 케이스

FlowRow에 설정된 가로 배열 Result
Arrangement.Start (Default)
Arrangement.Center
Arrangement.spacedBy(8.dp)

Cross axis 배열

Cross axis는 main axis와 반대 방향의 축

  • FlowColumn의 기본 cross axis는 Arrangement.Start

예) FlowRow의 일부 케이스

FlowRow에 설정된 세로 배열 Result
Arrangement.Top (Default)
Arrangement.Center

개별 항목 정렬

row 내에서 개별 항목을 서로 다른 정렬로 배치할 때 Modifier.align()을 사용하여 적용

  • FlowColumn의 기본 정렬은 Alignment.Star
Vertical alignment set on FlowRow Result
Alignment.Top (Default)
Alignment.CenterVertically

행 또는 열의 최대 항목 수

maxItemsInEachRow/maxItemsInEachColumn 파라미터로 main axis에서 한 줄에 허용할 수 있는 최대 항목을 정의. 기본값은 Int.MAX_INT

기본 값 maxItemsInEachRow = 3
No max set on flow row Max items set on flow row

지연 로딩 flow items

ContextualFlowRow/ContextualFlowColumn은 FlowRow/FlowColumn의 콘텐츠를 지연 로드할 수 있는 특수 버전

  • 항목 위치(인덱스, 행 번호 및 사용 가능한 크기)도 제공
  • maxLines 파라미터 : 표시되는 row의 수를 제한
  • overflow 파라미터 : 항목이 넘칠 때 표시할 항목을 지정하여 커스텀 expandIndicator/collapseIndicator를 지정 가능

예) ‘+(남은 항목 수)’ 또는 ‘Less’ 버튼을 표시

val totalCount = 40
var maxLines by remember {
   mutableStateOf(2)
}

val moreOrCollapseIndicator = @Composable { scope: ContextualFlowRowOverflowScope ->
   val remainingItems = totalCount - scope.shownItemCount
   ChipItem(if (remainingItems == 0) "Less" else "+$remainingItems", onClick = {
      if (remainingItems == 0) {
         maxLines = 2
      } else {
         maxLines += 5
      }
   })
}
ContextualFlowRow(
   ...
   maxLines = maxLines,
   overflow = ContextualFlowRowOverflow.expandOrCollapseIndicator(
      minRowsToShowCollapse = 4,
      expandIndicator = moreOrCollapseIndicator,
      collapseIndicator = moreOrCollapseIndicator
   ),
   itemCount = totalCount
) { index ->
   ChipItem("Item $index")
}

아이템 가중치

항목의 갯수와 사용 가능한 공간에 따라 항목을 늘린다.

각 항목 너비 : 가중치 * (남은 공간 / 총 가중치)

Modifier.weight와 최대 항목 수를 조합하여 Grid 레이아웃을 만들 수 있다.

val rows = 3
val columns = 3
FlowRow(
   ...
   maxItemsInEachRow = rows
) {
   val itemModifier = Modifier
      .padding(4.dp)
      .height(80.dp)
      .weight(1f)
      .clip(RoundedCornerShape(8.dp))
      .background(MaterialColors.Blue200)
   repeat(rows * columns) {
      Spacer(modifier = itemModifier)
   }
}

위의 예시에서 10개의 항목이라면, 전체 행의 총 가중치가 1f가 되므로 마지막 항목이 마지막 열 전체를 차지한다

부분 크기 조정

Modifier.fillMaxWidth(fraction)를 사용하면 항목이 차지할 크기를 지정 가능

  • 전체 컨테이너 너비가 아닌 남은 너비의 백분율을 차지

예) FlowRow/Row를 사용할 때 다른 결과의 모습

FlowRow(
    modifier = Modifier.padding(4.dp),
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    maxItemsInEachRow = 3
) {
    val itemModifier = Modifier
        .clip(RoundedCornerShape(8.dp))
    Box(modifier = itemModifier.height(200.dp).width(60.dp).background(Color.Red))
    Box(modifier = itemModifier.height(200.dp).fillMaxWidth(0.7f).background(Color.Blue))
    Box(modifier = itemModifier.height(200.dp).weight(1f).background(Color.Magenta))
}
FlowRow: 전체 컨테이너 너비의 0.7분의 1을 차지하는 중간 항목 Fractional width with flow row
Row: 중간 항목이 남은 행 너비의 0.7%를 차지 Fractional width with row

fillMaxColumnWidth()/fillMaxRowHeight()

같은 Column/Row의 항목이 Column/Row에서 가장 큰 항목과 동일한 너비/높이를 가지도록 함

각 항목에 적용된 Modifier.fillMaxColumnWidth()

Custom layouts

UI 트리에 각 노드를 배치하는 작업은 3단계 프로세스. 각 노드는 다음을 수행해야 함

  1. 하위 element 측정
  2. 자체 크기 결정
  3. 하위 element 배치

Compose UI는 복수 pass 측정을 허용하지 않음. 즉, 레이아웃 element는 다른 측정 구성을 시도하기 위해 하위 element를 두 번 이상 측정할 수 없다.

Scope의 사용 여부에 따라 하위 element를 측정/배치할 수 있는 시기가 결정됨.

  • 레이아웃 측정은 측정 및 레이아웃 패스 중에만 가능
  • 하위 element는 레이아웃 패스 중에만(그리고 측정이 끝난 후에만) 배치 가능

layout modifier 사용

  • layout Modifier를 사용하여 element의 측정/배치 방식을 수정
  • layout은 람다이며, measurable로 전달되는 측정 가능한 element와 constraints로 전달되는 해당 Composable의 제약 조건이 파라미터로 포함
fun Modifier.customLayoutModifier() =
   layout { measurable, constraints ->
      // ...
   }

(이미지 샘플 예시)

화면에 텍스트를 표시, 텍스트의 첫 번째 줄의 상단에서 baseline까지의 거리를 제어. layout modifier를 사용하여 composable을 수동으로 배치

  1. measurable 람다 파라미터에서 measurable.measure(constraints)를 호출하여 측정 가능한 파라미터로 표시되는 Text를 측정
  2. layout(width, height) 메서드를 호출하여 컴포저블의 크기를 지정. 크기는 마지막 기준선과 추가된 상단 패딩 사이의 높이
  3. placeable.place(x, y)를 호출하여 화면에 래핑된 요소를 배치. element를 배치하지 않으면 표시되지 않습니다. (y 위치는 상단 패딩 - 텍스트의 첫 번째 기준선 위치)
fun Modifier.firstBaselineToTop(
   firstBaselineToTop: Dp
) = layout { measurable, constraints ->
   // composable 측정
   val placeable = measurable.measure(constraints)

   // composable에 FirstBaseline이 있는지 체크
   check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
   val firstBaseline = placeable[FirstBaseline]

   // padding이 포함된 composable의 높이 - firstBaseline
   val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
   val height = placeable.height + placeableY
   layout(placeable.width, height) {
      // composable이 배치되는 위치
      placeable.placeRelative(0, placeableY)
   }
}

// 다음과 같이 Modifier를 사용
@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
   MyApplicationTheme {
      Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
   }
}

커스텀 layout 만들기

layout modifier는 호출하는 Composable만 변경. 여러 Composable을 측정하고 배치에 사용.

하위 element를 수동으로 측정/배치 가능.

row/column과 같은 상위 수준 레이아웃은 Laytout composable로 만든다

// 예) 기본적인 버전의 Column
@Composable
fun MyBasicColumn(
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit
) {
   Layout(
      modifier = modifier,
      content = content
   ) { measurables, constraints ->
      // 여기에서 제약 조건 로직이 주어진 하위 element를 측정하고 배치
      // 측정된 하위 element 목록
      val placeables = measurables.map { measurable ->
         // 하위 element 측정
         measurable.measure(constraints)
      }

      // 레이아웃의 크기를 최대한 크게 설정
      layout(constraints.maxWidth, constraints.maxHeight) {
         // 하위 element를 최대로 배치한 Y 좌표를 추적
         var yPosition = 0

         // 부모 layout에 하위 element 배치
         placeables.forEach { placeable ->
            // 화면에서 item 위치 지정
            placeable.placeRelative(x = 0, y = yPosition)

            // 배치된 Y 좌표를 업데이트
            yPosition += placeable.height
         }
      }
   }
}
@Composable
fun CallingComposable(modifier: Modifier = Modifier) {
   MyBasicColumn(modifier.padding(8.dp)) {
      Text("MyBasicColumn")
      Text("places items")
      Text("vertically.")
      Text("We've done it by hand!")
   }
}

레이아웃 방향

Composable의 레이아웃 방향은 LocalLayoutDirection CompositionLoal로 변경하면 바꿀 수 있다.

화면에 수동으로 배치하는 경우, LayoutDirection은 layout modifier 또는 Layout composable의 LayoutScope에 포함됨

layoutDirection 사용 시 Composable을 배치할 때에는 place를 사용.

placeRelative 메소와 달리 place는 레이아웃 방향(왼쪽에서 오른쪽 또는 오른쪽에서 왼쪽)에 따라 변경되지 않음

Alignment lines

paddingFrom : 라이브러리에서 alignment line을 기준으로 padding을 지정 가능

커스텀 alignment lines 만들기

커스텀 Layout Composable 또는 커스텀 LayoutModifier를 만들 때 커스텀 alignment lines을 제공하면 다른 부모 Composable이 이를 사용하여 하위 element를 정렬/배치할 수 있다.

차트의 최대/최소 데이터 값에 맞춰 정렬할 수 있도록 두 개의 Alignment lines인 MaxChartValue와 MinChartValue를 노출하는 커스텀 BarChart Composable. 세로로 정렬하는 데 사용되므로 HorizontalAlignmentLine 유형을 사용한다.

/** BarChart의 최대 값으로 정의된 AlignmentLine */
private val MaxChartValue = HorizontalAlignmentLine(merger = { old, new ->
   min(old, new)
})

/** BarChart의 최소 값으로 정의된 AlignmentLine */
private val MinChartValue = HorizontalAlignmentLine(merger = { old, new ->
   max(old, new)
})
@Composable
private fun BarChart(
   dataPoints: List<Int>,
   modifier: Modifier = Modifier,
) {
   val maxValue: Float = remember(dataPoints) { dataPoints.maxOrNull()!! * 1.2f }

   BoxWithConstraints(modifier = modifier) {
      val density = LocalDensity.current
      with(density) {
         // ...
         // Calculate baselines
         val maxYBaseline = // ...
         val minYBaseline = // ...
         Layout(
            content = {},
            modifier = Modifier.drawBehind {
               // ...
            }
         ) { _, constraints ->
            with(constraints) {
               layout(
                  width = if (hasBoundedWidth) maxWidth else minWidth,
                  height = if (hasBoundedHeight) maxHeight else minHeight,
                  // 커스텀 AlignmentLines이 설정.
                  // 직간접 부모 Composable에 전파된다
                  alignmentLines = mapOf(
                     MinChartValue to minYBaseline.roundToInt(),
                     MaxChartValue to maxYBaseline.roundToInt()
                  )
               ) {}
            }
         }
      }
   }
}

이 Composable의 직간접 Composable이 AlignmentLines을 사용할 수 있다. 그다음 Composable은 두 개의 텍스트 슬롯과 데이터 포인트를 파라미터로 사용하여 두 텍스트를 최대/최소 차트 데이터 값에 맞춰 정렬하는 커스텀 레이아웃을 만든다.

@Composable
private fun BarChartMinMax(
   dataPoints: List<Int>,
   maxText: @Composable () -> Unit,
   minText: @Composable () -> Unit,
   modifier: Modifier = Modifier,
) {
   Layout(
      content = {
         maxText()
         minText()
         // BarChart 고정 크기 설정
         BarChart(dataPoints, Modifier.size(200.dp))
      },
      modifier = modifier
   ) { measurables, constraints ->
      check(measurables.size == 3)
      val placeables = measurables.map {
         it.measure(constraints.copy(minWidth = 0, minHeight = 0))
      }

      val maxTextPlaceable = placeables[0]
      val minTextPlaceable = placeables[1]
      val barChartPlaceable = placeables[2]

      // BarChart에서 alignment lines을 가져와 텍스트의 위치를 지정
      val minValueBaseline = barChartPlaceable[MinChartValue]
      val maxValueBaseline = barChartPlaceable[MaxChartValue]
      layout(constraints.maxWidth, constraints.maxHeight) {
         maxTextPlaceable.placeRelative(
            x = 0,
            y = maxValueBaseline - (maxTextPlaceable.height / 2)
         )
         minTextPlaceable.placeRelative(
            x = 0,
            y = minValueBaseline - (minTextPlaceable.height / 2)
         )
         barChartPlaceable.placeRelative(
            x = max(maxTextPlaceable.width, minTextPlaceable.width) + 20,
            y = 0
         )
      }
   }
}
@Preview
@Composable
private fun ChartDataPreview() {
   MaterialTheme {
      BarChartMinMax(
         dataPoints = listOf(4, 24, 15),
         maxText = { Text("Max") },
         minText = { Text("Min") },
         modifier = Modifier.padding(24.dp)
      )
   }
}

Intrinsic measurements

Compose 규칙 중 하나는 하위 element를 한 번만 측정해야 한다는 것이다. 두 번 측정하면 런타임 예외가 발생한다. 하지만 하위 element 측정하기 전에 하위 element에 관한 정보가 필요한 경우도 있다.

Intrinsics을 사용하면 하위 element가 측정되기 전에 하위 element를 쿼리할 수 있다.

Composable에 intrinsicWidth/intrinsicHeight를 요청

  • (min|max)IntrinsicWidth: 콘텐츠를 적절하게 그릴 수 있는 최소/최대 너비
  • (min|max)IntrinsicHeight: 콘텐츠를 적절하게 그릴 수 있는 최소/최대 높이

width가 무한대인 TextminIntrinsicHeight를 요청하면, 텍스트가 한 줄에 그려진 것처럼 Textheight가 반환

Intrinsics 실제 사례

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
   // IntrinsicSize.Min : 최소 고유 높이만큼 하위 element들의 크기를 강제로 조절
   Row(modifier = modifier.height(IntrinsicSize.Min)) {
      Text(
         modifier = Modifier
            .weight(1f)
            .padding(start = 4.dp)
            .wrapContentWidth(Alignment.Start),
         text = text1
      )
      Divider(
         color = Color.Black,
         modifier = Modifier.fillMaxHeight().width(1.dp)
      )
      Text(
         modifier = Modifier
            .weight(1f)
            .padding(end = 4.dp)
            .wrapContentWidth(Alignment.End),
         text = text2
      )
   }
}

// @Preview
@Composable
fun TwoTextsPreview() {
   MaterialTheme {
      Surface {
         TwoTexts(text1 = "Hi", text2 = "there")
      }
   }
}

Row의 modifier를 기본값을 사용한다면 Divider의 fillMaxHeight() 조건으로 부모가 가지는 높이만큼 채우게 된다.

Divider 요소의 minIntrinsicHeight는 제약 조건이 주어지지 않으면 공간을 차지하지 않으므로 0이다. Row 요소의 height 제약 조건은 Text의 최대 minIntrinsicHeight입니다. 그런 다음 DividerheightRow가 지정한 height 제약 조건으로 확장한다.

커스텀 layout의 내장 기능

커스텀 Layout 또는 layout modifier를 만들 때 근사치를 기반으로 측정값이 자동으로 계산된다. 모든 레이아웃이 계산이 정확하지 않다. API를 사용해서 재정의 가능하다

Layout의 MeasurePolicy 인터페이스를 재정의하면 된다.

  • minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth 및 maxIntrinsicHeight

커스텀 layout modifier은 LayoutModifier 인터페이스를 재정의 구현하면 된다.

ConstraintLayout

기존 Viw 시스템에서 ConstraintLayout은 크고 복잡한 레이아웃을 만드는 데 권장된 이유는 flat View 계층 구조가 중첩된 View보다 성능 면에서 더 좋았기 때문이다. 그러나, 깊은 레이아웃 계층 구조를 효율적으로 처리할 수 있는 Compose에서는 문제가 되지 않는다.

ConstraintLayout 시작하기

implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"

Compose의 ConstraintLayout은 DSL 방식을 사용

  • createRefs() 또는 createRefFor()를 사용하여 ConstraintLayout의 각 Composable에 대한 reference를 생성
  • 제약 조건은 참조를 파라미터로 받아, 본문 람다에서 해당 제약 조건을 지정할 수 있는 constrainAs() modifier를 사용하여 제공
  • 제약 조건은 linkTo()/기타 메소드를 사용하여 지정
  • 부모는 ConstraintLayout composable에 대한 제약 조건을 지정하는 데 사용할 수 있는 기존 참조

@Composable
fun ConstraintLayoutContent() {
   ConstraintLayout {
      // 제약할 composable에 대한 레퍼런스를 생성
      val (button, text) = createRefs()

      Button(
         onClick = { /* Do something */ },
         // Button composable에 참조 "button"을 할당,
         // ConstraintLayout의 상단으로 제약 추가
         modifier = Modifier.constrainAs(button) {
            top.linkTo(parent.top, margin = 16.dp)
         }
      ) {
         Text("Button")
      }

      // Text composable에 참조 "text"를 할당,
      // Buton composable의 하단에 제약 추가
      Text(
         "Text",
         Modifier.constrainAs(text) {
            top.linkTo(button.bottom, margin = 16.dp)
         }
      )
   }
}

API 분리

ConstraintLayout의 ConstraintSet과 layoutId를 분리하여 지정 가능

  1. ConstraintSet을 파라미터로 ConstraintLayout에 전달
  2. layoutId modifier를 사용하여 ConstraintSet에 생성된 참조를 Composable에 할당
@Composable
fun DecoupledConstraintLayout() {
   BoxWithConstraints {
      val constraints = if (minWidth < 600.dp) {
         decoupledConstraints(margin = 16.dp) // 세로 제약
      } else {
         decoupledConstraints(margin = 32.dp) // 가로 제약
      }

      ConstraintLayout(constraints) {
         Button(
            onClick = { /* Do something */ },
            modifier = Modifier.layoutId("button")
         ) {
            Text("Button")
         }

         Text("Text", Modifier.layoutId("text"))
      }
   }
}

private fun decoupledConstraints(margin: Dp): ConstraintSet {
   return ConstraintSet {
      val button = createRefFor("button")
      val text = createRefFor("text")

      constrain(button) {
         top.linkTo(parent.top, margin = margin)
      }
      constrain(text) {
         top.linkTo(button.bottom, margin)
      }
   }
}

ConstraintLayout 개념

Guidelines

특정 dp/percentage로 배치하는 데 사용

ConstraintLayout {
   // Composable width의 10%에서 parent start부터 가이드라인 생성
   val startGuideline = createGuidelineFromStart(0.1f)
   // parent end에서 Composable width의 10%에 가이드라인 생성
   val endGuideline = createGuidelineFromEnd(0.1f)
   // parent top의 16dp에서 가이드라인 생성
   val topGuideline = createGuidelineFromTop(16.dp)
   // parent bottom에서 16dp에서 가이드라인 생성
   val bottomGuideline = createGuidelineFromBottom(16.dp)
}

Row/Column에서 Spacer composable을 사용하면 비슷한 효과가 가능

Barriers

여러 composable을 참조하여 지정된 쪽의 가장 극단적인 가상 guideline을 생성

ConstraintLayout {
   val constraintSet = ConstraintSet {
      val button = createRefFor("button")
      val text = createRefFor("text")

      val topBarrier = createTopBarrier(button, text)
   }
}

Row/Column에서 Intrinsic을 사용하면 비슷한 효과가 가능

Chains

단일 축(가로/세로)에서 group과 같은 동작을 제공

ConstraintLayout {
   val constraintSet = ConstraintSet {
      val button = createRefFor("button")
      val text = createRefFor("text")

      val verticalChain = createVerticalChain(button, text, chainStyle = ChainStyle.Spread)
      val horizontalChain = createHorizontalChain(button, text)
   }
}

지원하는 ChainStyles

  • ChainStyle.Spread : 첫 번째 composable 앞과 마지막 composable 뒤의 여유 공간을 포함하여 모든 composable에 공간을 균등하게 분배
  • ChainStyle.SpreadInside : 첫 번째 composable 앞이나 마지막 composable 뒤에 여유 공간 없이 모든 composable에 공간을 균등하게 분배
  • ChainStyle.Packed : 첫 번째 composable 앞과 마지막 composable 뒤에 공간이 분배되며, composable 사이에 공백 없이 함께 패킹

Row/Column에서 Arrangements을 사용하면 비슷한 효과가 가능

comments powered by Disqus

Currnte Pages Tags

Android AndroidX Compose

About

Pluu, Android Developer Blog Site

이전 블로그 링크 :네이버 블로그

Using Theme : SOLID SOLID Github

Social Links