Android

[Android/Kotlin] Compose Layout(1)

YusAOS 2024. 11. 9. 22:16

함수 뜯어보고 탐구하기



Compose Pager

화면 내에서 페이지를 좌우 또는 상하로 스크롤하여 콘텐츠를 탐색할 수 있도록 돕는 Components입니다.

종류는 다음과 같습니다.

  • HorizontalPager
  • VerticalPager

이 컴포넌트들은 콘텐츠를 지연 로딩하여 필요할 때만 페이지를 생성함으로써 성능을 최적화 합니다.

( 사용자가 페이지를 스크롤하면 컴포저블은 더 이상 필요하지 않은 페이지를 삭제합니다. 기본적으로 페이저는 화면에 표시되는 페이지만 로드합니다. 화면 외부에 더 많은 페이지를 로드하려면 beyondBoundsPageCount를 0 보다 큰 값을 설정해야 합니다.)

HorizontalPager는 화면의 전체 너비를 차지하고, VerticalPager는 전체 높이를 차지하며 페이저는 한 번에 한 페이지만 플링합니다.

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

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

 

페이저에서 특정 페이지로 스크롤하려면 아래와 같은 rememberPagerState()를 사용하여 PagerState 객체를 만들고

 이를 페이저에 state 매개변수로 전달해야 합니다. 이 상태에서 CoroutineScope 내에서 PagerState.scrollToPage() 함수를 선언해줘야 합니다.

Scroll시 Animation을 추가하려면 animateScrollToPage로 설정하여 적용시킵니다.

val coroutineScope = rememberCoroutineScope()
Button(onClick = {
    coroutineScope.launch {
        // Call scroll to on pagerState
        pagerState.scrollToPage(5)
        // Animation
        pagerState.animateScrollToPage(5)
    }
    })

 

페이지 상태 변경에 대한 알림 받기를 할려면

PagerState에 페이지에 관한 정보가 포함된 세가지 속성을 활용 해야 합니다.

  • currentPage: 스냅 위치에 가장 가까운 페이지입니다. 기본적으로 스냅 위치는 레이아웃의 시작 부분에 있습니다.
  • settledPage: 애니메이션이나 스크롤이 실행되지 않는 페이지 번호입니다. 이는 currentPage 속성과는 다릅니다. currentPage는 페이지가 스냅 위치에 충분히 가까우면 즉시 업데이트되지만 settledPage는 모든 애니메이션이 실행을 완료할 때까지 동일하게 유지됩니다.
  • targetPage: 스크롤 동작의 제안된 중지 위치입니다.

snapshotFlow 함수를 사용하여 이러한 변수의 변경사항을 관찰하고 이에 반응할 수 있습니다. 예를 들어 각 페이지 변경 시 애널리틱스 이벤트를 전송하려면 다음을 수행합니다.

 

LaunchedEffect(pagerState) {
    // Collect from the a snapshotFlow reading the currentPage
    snapshotFlow { pagerState.currentPage }.collect { page ->
        // Do something with each page change, for example:
        // viewModel.sendPageSelectedEvent(page)
        Log.d("Page change", "Page changed to $page")
    }
}

 

페이지 표시기 추가하기 위해선 pagerState.currentPage를 사용하여 활용합니다.

repeat(pagerState.pageCount) { iteration ->
        if (pagerState.currentPage == iteration){
        // Something
        }
    }

 

페이지가 현재 선택된 페이지에서 얼마나 멀리 떨어져 있는지 확인하려면 PagerState.currentPageOffsetFraction를 사용해야 한다. 항목의 불투명도를 조정할려면, Modifier.graphicsLayer를 사용하여 alpha를 변경합니다.

Modifier
            .size(200.dp)
            .graphicsLayer {
                // Calculate the absolute offset for the current page from the
                // scroll position. We use the absolute value which allows us to mirror
                // any effects for both directions
                val pageOffset = (
                    (pagerState.currentPage - page) + pagerState
                        .currentPageOffsetFraction
                    ).absoluteValue

                // We animate the alpha, between 50% and 100%
                alpha = lerp(
                    start = 0.5f,
                    stop = 1f,
                    fraction = 1f - pageOffset.coerceIn(0f, 1f)
                )
            }

 

pageSize의 변수를 활용하여 fixed, fill(default) 또는 맞춤 크기를 갖도록 설정할 수 있습니다.

HorizontalPager(
    state = pagerState,
    pageSize = PageSize.Fixed(100.dp)
) 

 

기본적으로 플링 동작으로 한 번에 스크롤할 수 있는 최대 페이지 수를 1페이지로 설정합니다. 이를 변경하려면, flingBehavior에서 pagerSnapDistance를 설정해야합니다.

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

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

 

Flow Layout

FlowRow  FlowColumn Row  Column와 유사한 컴포저블이지만 컨테이너의 공간이 부족하면 항목이 다음 줄로 이동한다는 점에서 다릅니다. 그러면 여러 행 또는 열이 생성됩니다. 

maxItemsInEachRow 또는 maxItemsInEachColumn를 설정하여 줄에 있는 항목 수를 제어할 수도 있습니다. FlowRow  FlowColumn를 사용하여 반응형 레이아웃을 빌드하는 경우가 많습니다.

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

ContextualFlowRow  ContextualFlowColumn는 흐름 행 또는 열의 콘텐츠를 지연 로드할 수 있는 FlowRow  FlowColumn의 특수 버전입니다. 또한 항목이 첫 번째 행에 있는지 여부와 같은 항목 위치(색인, 행 번호, 사용 가능한 크기)에 관한 정보도 제공합니다. 이는 대규모 데이터 세트와 항목에 관한 문맥 정보가 필요한 경우에 유용합니다.

maxLines 매개변수는 표시되는 행 수를 제한하며 overflow 매개변수는 항목 오버플로에 도달했을 때 표시할 항목을 지정하므로 커스텀 expandIndicator 또는 collapseIndicator를 지정할 수 있습니다.

 

*  maxLines는 FlowRow 및 FlowColumn에서 사용할 수 있지만, ContextualFlowRow 및 ContextualFlowColumn는 하위 컴포지션을 사용하여 레이아웃 정보를 결정하므로 모든 단계에서 shownItemCount와 같은 레이아웃 정보에 액세스할 수 있습니다. 그리기 단계에 있는 경우에만 FlowRow에서 shownItemCount에 액세스할 수 있습니다. 이 정보가 필요한지 여부와 언제 필요한지 고려하여 두 가지 중에서 선택하세요.

 

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(
    modifier = Modifier
        .safeDrawingPadding()
        .fillMaxWidth(1f)
        .padding(16.dp)
        .wrapContentHeight(align = Alignment.Top)
        .verticalScroll(rememberScrollState()),
    verticalArrangement = Arrangement.spacedBy(4.dp),
    horizontalArrangement = Arrangement.spacedBy(8.dp),
    maxLines = maxLines,
    overflow = ContextualFlowRowOverflow.expandOrCollapseIndicator(
        minRowsToShowCollapse = 4,
        expandIndicator = moreOrCollapseIndicator,
        collapseIndicator = moreOrCollapseIndicator
    ),
    itemCount = totalCount
) { index ->
    ChipItem("Item $index")
}

 

 

그리고 항상 크기가 일정하게 설정하기 위해서는 fillMaxColumnWidth()  fillMaxRowHeight()의 설정이 필수적입니다.