ViewPager2

한 화면에서 다른 화면으로 슬라이드해서 전환하게 도와주는 객체이다. viewPager

build.gradle.kts (module 수준)

    // viewPager2
    implementation ("com.google.accompanist:accompanist-pager:0.20.1") // 슬라이드 가능하게 하는 뷰페이저
    implementation ("com.google.accompanist:accompanist-pager-indicators:0.20.1") // 뷰 페이저 인디케이터, 하단의 동그라미

HorizontalPager

HorizontalPager은 수평 방향으로 슬라이드를 가능하게 한다.

@Composable
fun PagerExample() {
    val state = rememberPagerState(initialPage = 0)
    val contentPadding = PaddingValues(50.dp)

    HorizontalPager(
        state = state,
        modifier = Modifier
            .weight(.7f)
            .padding(
                top = 32.dp, start = 16.dp, end = 16.dp
            )
            .background(color = Color.LightGray),
        count = 3,
        itemSpacing = 20.dp,
        contentPadding = contentPadding,
    ) {
        // pagerContent
    }
}

HorizontalPager() 컴포저블에 각 페이지의 상태를 알 수 있는 PagerState 객체를 추가한다.

Modifier.padding과 contentPadding의 차이를 많이 헷갈려한다. 그림으로 알아보자.

📌 참고로 content는 page와 같은 의미이다. 참고하자


itemSpacing는 content들 간의 간격을 설정할 수 있다. 그림으로 알아보자.
❗️ itemSpacing 값을 음수로 설정하면 위의 사진 처럼 content끼리 겹치게 설정할 수 있다.


content에 크기 및 그림자 설정

Card() 컴포저블을 이용해서 content에 모양, 투명도, 크기, 애니메이션 효과를 적용해보겠다.

Card(
    Modifier
        .graphicsLayer {
            val pageOffset = calculateCurrentOffsetForPage(page).absoluteValue

            // We animate the scaleX + scaleY, between 85% and 100%
            lerp(
                0.85f,
                1f,
                1f - pageOffset.coerceIn(0f, 1f)
            ).also { scale ->
                scaleX = scale
                scaleY = scale
                Log.d(TAG,"page: ${page}\nfraction: ${pageOffset}\n scale: $scale")
            }

            // We animate the alpha, between 50% and 100%
            alpha = lerp(
                0.5f,
                1f,
                1f - pageOffset.coerceIn(0f, 1f)
            )
        }
    ) {
    Box(
        modifier = Modifier.fillMaxSize()
    ) {
        Text(text = page.toString(), modifier = Modifier.align(Alignment.Center))
    }
}

graphicsLayer{}는 애니메이션을 설정하는 메서드.
calculateCurrentOffsetForPage(page)는 현재 페이지의 Offset 정보를 반환해주고 .absoluteValue을 통해 Offset을 절대값으로 설정한다.
lerp()은 content의 애니메이션 적용하는 메서드이다. also{}를 통해 크기를 설정하고 있다.
scaleXscaleY는 width와 height를 설정하는 프로퍼티이다. scale은 0.85f에서 1f까지 Offset의 값에 따라 변경된다.
pageOffset.coerceIn(0f, 1f)은 Offset의 값이 0f ~ 1f 사이에서만 왔다갔다 하게 설정하는 메서드이다.

alpha는 투명도를 설정하는 프로퍼티이다.lerp()를 통해 0.5f에서 1f까지 투명도가 애니메이션으로 적용되어진다.



❗️즉, lerp()메서드의 매개변수인 fraction의 값을 계산해 return 값으로 크기를 변경한다.
예를 들어 설명하면 pageOffset이 0f인 경우(content가 중앙에 위치한 경우) fraction의 값은 1f, return 값은 1f, 따라서 크기가 원래크기의 1f 만큼의 크기로 바뀐다.
pageOffset이 1f인 경우(content가 중앙이 아닌 옆에 위치한 경우) fraction의 값은 0f, return 값은 0.85f, 따라서 크기가 원래크기의 0.85f 만큼의 크기로 바뀐다.

Offset, fraction 이게 뭔데?

앞서 설명한 코드에 Offset, fraction이 자주 등장했다. 많이 궁금했을텐데 지금부터 알아보려고 한다. Offsetfraction은 같은 의미로 보면 된다.
Offset은 보통 위치의 의미로 많이 쓰이고 ViewPager2에서 마찬가지로 위치의 의미로 사용되어진다. 아래의 사진을 보자.

image.jpg2

image.jpg2

❗️즉, 중앙에 있는 page의 fraction은 0, 가장자리에 있는 page의 fraction은 1이다. 중앙에서 가장자리로 이동될 때 fraction은 증가된다.
중앙에 있는 page로부터 떨어져있을 때 fraction는 page 1개당 1씩 증가된다.
fraction 관련 이해를 도울 수 있는 링크

페이지 변경에 반응

PagerState.currentPage는 page가 변경될 때마다 속성이 업데이트된다. snapshotFlow 함수를 사용해서 흐름의 변경 사항을 관찰할 수 있다.

LaunchedEffect(state) {
    snapshotFlow { state.currentPage }.collect { page ->
            Log.d(TAG, "page: $page")
    }
}

Indicator

Indicator를 통해 페이지의 위치를 나타내어 사용자의 만족도를 높일 수 있다. 페이지가 얼만큼 남았는지 알 수 있기에 편리하다. ViewPager 밑에 있는 작은 동그라미 여러개가 Indicator이다. 아래의 사진으로 살펴보자.


현재 페이지의 상태를 알기 위한 pagerState를 매개변수로 넣어준다. activeColor는 Indicator의 동그라미 생삭을 변경할 수 있게 도와준다.

HorizontalPagerIndicator(pagerState = state, activeColor = Color.Blue)

전체 코드

@Composable
fun ViewPagerExample() {
    val state = rememberPagerState(initialPage = 0)
    val contentPadding = PaddingValues(50.dp)

    LaunchedEffect(state) {
        // Collect from the pager state a snapshotFlow reading the currentPage
        snapshotFlow { state.currentPage }.collect { page ->
             Log.d(TAG, "page: $page")
        }
    }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        HorizontalPager(
            state = state,
            modifier = Modifier
                .weight(.7f)
                .padding(
                    top = 32.dp, start = 16.dp, end = 16.dp
                )
                .background(color = Color.LightGray),
            count = 3,
            itemSpacing = (-70).dp,
            contentPadding = contentPadding,
        ) {page ->
            Card(
                Modifier
                    .graphicsLayer {

                        val pageOffset = calculateCurrentOffsetForPage(page).absoluteValue
//                        val pageOffset = calculateCurrentOffsetForPage(page)

                        // We animate the scaleX + scaleY, between 85% and 100%
                        lerp(
                            0.85f,
                            1f,
                            1f - pageOffset.coerceIn(0f, 1f)
                        ).also { scale ->
                            scaleX = scale
                            scaleY = scale
                            Log.d(TAG,"page: ${page}\nfraction: ${pageOffset}\n scale: $scale")
                        }

                        // We animate the alpha, between 50% and 100%
                        alpha = lerp(
                            0.5f,
                            1f,
                            1f - pageOffset.coerceIn(0f, 1f)
                        )
                    }
            ) {
                Box(
                    modifier = Modifier.fillMaxSize()
                ) {
                    Text(text = page.toString(), modifier = Modifier.align(Alignment.Center))
                }
            }

        }
        Spacer(modifier = Modifier.height(16.dp))
        HorizontalPagerIndicator(pagerState = state, activeColor = Color.Blue)
    }
}

Reference

이해하기 쉬운 viewpager 블로그
compose viewpager
custom Indicator 라이브러리
직접 custom하는 Indicator