프로그래밍/Android

[안드로이드] 부채꼴 카드처럼 돌아가는 Pager 만들기 (with Jetpack Compose Horizontal Pager)

Lou Park 2023. 8. 26. 17:34

완성된 UI, 오타는 신경쓰지 마세요...ㅋㅋㅋ

오랜만의 Android 포스팅이다...ㅋㅋㅋ

도전적인 UI를 받아볼때 머리아프면서도 신나는 그런게 있다.

이번에 만들어본 건 손에 쥔 카드처럼 돌아가는 Pager다. (뭐라고 해야할까..? 용어를 아시는분은 댓글!)

 

 

Jetpack Compose를 사용한지 3개월 남짓이라 숙련도가 다소 낮았기 때문에 간단한 이해부터 하고 작업에 들어갔다. “Jetpack Compose Pager Animation” 키워드로 검색해서 나오는 글들 중에 개인적으로 가장 깔끔했던 이 글의 설명을 빌려 Page Offset을 계산하는 방식을 후술해보려 한다.

Page Offset 계산하기

Pager State 에는 currentPageOffsetFraction이라는 멤버변수가 제공된다. 이름에서 알 수 있듯이, 현재 페이지에 대한 offset을 제공한다. offset은 -0.5 ~ 0.5 사이의 값으로 나타내어진다.

  • -0.5: 현재 페이지가 왼쪽 가장자리의 오른쪽 50%
  • 0.0: 현재 페이지가 왼쪽 가장자리를 따라 배치됨
  • 0.5: 현재 페이지가 왼쪽 가장자리의 왼쪽 50%

데엠 무슨 말인지 모르겠다…….. 그래서 이미지를 캡쳐해서 확인해보았다.

  • 초록색 선이 왼쪽 가장자리(Left Edge)
  • 주황색 선이 현재 페이지의 왼쪽 끝이다.

쉽게 얘기하면 왼쪽 끝을 현재 페이지가 얼마나 지나쳤으냐 정도가 될 것 같다.

이것만으로 충분할 수도 있겠지만, 이를 이용해서

한 페이지가 완전히 왼쪽으로 사라졌을때는 1이되고,

반대로 완전히 오른쪽에 있을때는 -1인 값들은 다음과 같이 얻을 수 있다.

@OptIn(ExperimentalFoundationApi::class)
fun PagerState.offsetForPage(page: Int) = 
    (currentPage - page) + currentPageOffsetFraction

https://www.sinasamaki.com/pager-animations/

이것으로 Page Offset 계산이 끝났다! 많은 부분이 해결되었다.

이제 Page Offset을 따라 카드를 회전시켜주기만 하면된다.

계산식도 pageOffset * -각도 로 아주 간단하다.

 

양 사이드의 페이지가 보이게하기

아직 코드에는 문제가 많다. 그중에서 하나는 바로 좌/우 페이지가 보이지 않는다는 것인데,

Page Size와 Content Padding 설정으로 해결할 수 있다.

꽉 차면안되는데!

val widthWeight = 0.6F
val configuration = LocalConfiguration.current
val pageSize = PageSize.Fixed(pageSize = (configuration.screenWidthDp * widthWeight).dp)
val horizontalContentPadding = (configuration.screenWidthDp * (1F - widthWeight) / 2).dp

HorizontalPager(
    modifier = Modifier.fillMaxSize().background(Color.LightGray),
    pageCount = pageCount,
    state = pagerState,
    pageSize = pageSize,
    contentPadding = PaddingValues(horizontal = horizontalContentPadding),
) {
    // ...
}

현재 화면의 가로 사이즈를 얻어서, 한 페이지가 화면의 60%를 채우게 하고, 나머지 좌우 공간 20%씩을 content padding으로 설정한다. 설정을 끝내면 이 상태가 된다.

 

 

물론 아직 만족스럽지 않다… 카드끼리 너무 딱 붙어있다는 문제가 존재한다.

HorizontalPager에는 페이지 간격을 설정할 수 있는 pageSpacing이라는 파라미터 또한 제공한다.

val pageSpacing = 40.dp

HorizontalPager(
        ...
    pageSpacing = pageSpacing,
)

우리는 카드를 회전시켜야 하므로 40dp라는 과한 수치를 줘야 카드끼리 좀 떨어진 느낌이 든다.

혹은 카드 각도(rotateDegree) 값을 줄이는 방법을 쓰거나~

 

카드 높이 맞추기

여전히 만족스럽지 못한건, 카드의 높이가 뒤죽박죽이어 보인다는 것이다. (그야 가운데 축을 중심으로 회전만 시켜버리니 정리된 느낌이 안날 수 밖에…!)

이렇게 하려면 양 옆 카드의 translationY 를 높여, 아래로 쳐져보이게 해야 완벽한 부채꼴이 된다.

그러면 얼마만큼 아래로 내려야하는가?는 삼각함수를 이용하면 쉽게 구할 수 있다. (지구와 달 그리기 포스팅 참조)

💡 P(x1, y1)에서 x1 = cos(θ) * r, y1 = sin(θ) * r

 

우리가 구현한 카드 페이저를 이에 대입시켜보면 이런 그림이 된다.

P(x1, y1)에서 우리가 구하고 싶은건 파란색 선으로 표시한 y1이다.

  • R = 화면의 절반 - pageSpacing
  • θ = 회전각도(rotateDegree)의 라디안 값
  • y1 = sin(θ) * R

 

그리고 구해진 y1만큼을 translationY에 더해주면 된다.

양쪽 카드 모두에 동일한 높이만큼 적용해야하므로 pageOffeset.absoluteValue로 절대값을 얻어낸다.

파란색 카드정도 위치로 옮기면 돼!

Card(
    modifier = Modifier
    ...
    .graphicsLayer {
            // ...
        val distance = (configuration.screenWidthDp.dp / 2) - pageSpacing
        val height = sin(Math.toRadians(rotateDegree.toDouble())) * distance.toPx()

        translationY = (height * pageOffset.absoluteValue).toFloat()
    }
)

완 - 성!

이제 꽤나 볼만해진 모습이다. 부채꼴 애니메이션 가이드는 여기서 끝났다!

하지만 조금 더 심화하여 양 옆 카드가 살짝~(alpha = 0.8F) 투명해지는 효과도 줘보겠다.

 

lerp()를 이용하여 양 옆 카드를 살짝 투명하게 만들기

androidx.compose.ui.util 에 있는 lerp() 함수는 애니메이션을 할때 꽤나 유용하다.

그래서 같이 소개하고 싶었는데, fraction을 따라서 start에서 시작하고 stop에서 선형적으로 보간하고 멈춘다. util 라이브러리를 사용하고 있지 않다면 아래 함수만 추가해주면된다.

/**
 * Linearly interpolate between [start] and [stop] with [fraction] fraction between them.
 */
fun lerp(start: Float, stop: Float, fraction: Float): Float {
    return (1 - fraction) * start + fraction * stop
}

이렇게 하면 양 옆을 약간 투명하게하고 currentPage는 불투명하게 할 수 있다.

alpha = lerp(
    start = 0.8F,
    stop = 1F,
    fraction = 1F - pageOffset.absoluteValue.coerceIn(0F, 1F)
)

무한 Pager를 만드는 방법은 "무한 페이저 만들기" 포스팅을 참조하면 된다.

 

전체 코드

도움이 되셨다면 공감 한번 눌러주세요~!