[Jetpack Compose] Scroll Picker 만들기

2024. 12. 26. 16:18·Jetpack Compose

Scroll Picker

 

 

사용화면

해당 휠 피커는 연, 월, 일을 선택하는 휠 피커이며 원하는 항목을 사용하도록 커스텀할 수 있다. 예를 들어 맑음, 흐림, 비, 눈, 강풍처럼 날씨를 선택하는 휠 피커를 만들 수도 있다.

 

 

코드 설명

- 변수

// 초기값(2025년 3월)
val yearMonth: YearMonth by remember { mutableStateOf(YearMonth.now()) }

// ListState
val yearListState = rememberLazyListState(yearMonth.year - 1900)
val monthListState = rememberLazyListState(yearMonth.monthValue - 1)

// 변수
val density = LocalDensity.current
val threshold = remember { density.run { 20.dp.toPx() } } // 한 칸 높이의 절반
val year by remember { derivedStateOf { (yearListState.firstVisibleItemIndex + if (yearListState.firstVisibleItemScrollOffset >= threshold) 1901 else 1900) } }
val month by remember { derivedStateOf { (monthListState.firstVisibleItemIndex + if (monthListState.firstVisibleItemScrollOffset >= threshold) 2 else 1) } }

• date : 초기값 설정을 위한 변수로써 예시로 사용하는 값은 2025년 3월이다.

 

- 변수 클래스는 연월을 포함하고 있는 YearMonth를 사용했으며 보여줄 옵션들을 포함하는 클래스면 아무 클래스나 사용하면 된다.

 

• yearListState : 연도는 선택지를 1900부터 제공할 예정이기 때문에 초기값에서 1900을 빼서 초기 인덱스를 계산해 입력해 준다. 따라서 2025에서 1900을 뺀 125가 초기 인덱스가 되며 인덱스는 0부터 시작되므로 126번째 항목인 2025부터 보이게 된다.

 

• monthListState : 월은 선택지를 1부터 제공하므로 초기값 3에서 1을 빼 2번째 항목인 3부터 보이게 설정한다.

 

• density, threshold : 현재 선택된 항목을 변경할 때 사용되는 기준이다.

 

- 한 칸의 높이를 40.dp로 설정하였기 때문에 그 절반인 20.dp로 설정한다.

- firstVisibleItemScrollOffset이 flaot 타입이므로 density를 사용해 float 타입으로 변환해 둔다.

 

• year, month : 현재 선택된 옵션을 저장하는 변수이다. 화면에서 다른 색으로 표시할 때 사용하며 매우 빠르게 업데이트될 수 있는 값이므로 derivedStateOf를 사용하여 최적화해 준다.

 

- 연도의 초기 인덱스는 125였으므로 1900을 더해 2025가 year에 저장되고 스크롤해서 화면에 보이는 첫 번째 아이템의 인덱스가 변할 때마다 새로운 값이 계산되어 저장된다. 

- 선택된 항목을 변경하는 기준이 높이의 절반이므로 위의 gif나 영상을 보면 항목 높이의 절반만큼 스크롤하면 선택된 항목이 변경되는 것을 확인할 수 있다.

 

- Composable

나머지 부분은 디자인이고 주목해서 볼 부분은 LazyColumn이다. state를 선언할 때 LazyList에 대한 state를 선언했으므로 LazyColumn을 사용해 수직 피커로 만들 수도 있고 LazyRow를 사용해 수평 피커도 제작할 수도 있다.

Box(
    contentAlignment = Alignment.Center,
    modifier = Modifier
        .fillMaxWidth()
        .padding(horizontal = 20.dp)
) {
    Row(
        modifier = Modifier
            .fillMaxWidth(0.75f)
    ) {
        LazyColumn(
            state = yearListState,
            contentPadding = PaddingValues(16.dp, 80.dp),
            flingBehavior = rememberSnapFlingBehavior(yearListState),
            modifier = Modifier
                .weight(1f)
                .height(200.dp)
        ) {
            items((1900..2100).toList()) { int ->
                val textColor by animateColorAsState(if (int == year) Color.Black else Color(0xFFD0D0D0))

                Box(
                    contentAlignment = Alignment.Center,
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(40.dp)
                ) {
                    BasicText(
                        text = "${int}년",
                        style = LocalTextStyle.current.copy(
                            fontSize = 23.sp,
                            fontWeight = FontWeight.Normal
                        ),
                        color = { textColor }
                    )
                }
            }
        }
        LazyColumn(
            state = monthListState,
            contentPadding = PaddingValues(16.dp, 80.dp),
            flingBehavior = rememberSnapFlingBehavior(monthListState),
            modifier = Modifier
                .weight(1f)
                .height(200.dp)
        ) {
            items((1..12).toList()) { int ->
                val textColor by animateColorAsState(if (int == month) Color.Black else Color(0xFFD0D0D0))

                Box(
                    contentAlignment = Alignment.Center,
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(40.dp)
                ) {
                    BasicText(
                        text = "${int}월",
                        style = LocalTextStyle.current.copy(
                            fontSize = 23.sp,
                            fontWeight = FontWeight.Normal
                        ),
                        color = { textColor }
                    )
                }
            }
        }
    }
}

• contentPadding : 현재 선택된 항목의 위아래에 어떤 항목이 있는지 보여주기 위해서 상하단에 공간을 마련해야 한다. 필자의 한 칸 높이는 40.dp이고 각각 두 개씩 보이게 하기 위해 상하단에 80.dp의 패딩을 주었다.

 

• flingBehavior : rememberSnapFlingBehavior를 사용해서 스크롤하다 놓으면 가장 가까운 값으로 자동으로 스냅 되게 하였다. 이 함수는 필자가 제작한 함수가 아닌 compose foundation에 포함된 함수이다.

 

• modifier : 상단 2칸 중앙 1칸 하단 2칸 해서 총 5칸이므로 높이를 200.dp로 설정한다.

 

• items : 옵션으로 보여줄 항목을 리스트로 만들어 List 버전의 items의 파라미터로 입력해 준다.

 

• textColor, BasicText : 색상 변환을 부드럽게 해주는 방법이며 animateColorAsState를 사용할 때 BasicText와 함께 사용하면 재구성 최적화에 도움이 된다. 본인 취향에 맞게 사용하면 된다.

 

- 연과 일은 항목 수가 변하지 않으므로 직접 리스트로 만들어서 입력해도 되지만 일은 월에 따라 항목 수가 변하므로 lengthOfMonth 함수를 사용해서 계산해서 입력해 준다.

 

 

마치며

최대한 발생할 수 있는 상황을 예측하면서 예외 처리를 하였지만 미처 예상하지 못한 상황이 있을 수 있기에 그런 상황을 발견한다면 말씀해 주시면 감사하겠습니다. 그 외에도 궁금한 점이나 피드백은 댓글이나 카톡으로 연락해 주시면 최대한 빠르게 답변해 드리겠습니다. 

https://open.kakao.com/o/syg9ngUf

'Jetpack Compose' 카테고리의 다른 글
  • [Jetpack Compose] Navigation Indicator 만들기
  • [Jetpack Compose] Color Picker 만들기
  • [Jetpack Compose] 버전 문제로 인한 오류, 안정화 버전으로 해결하기
  • [Jetpack Compose] BasicTextField(TextFieldState) 사용하기
브애애앳
브애애앳
  • 브애애앳
    디벨로퍼즐
    브애애앳
  • 전체
    오늘
    어제
    • 분류 전체보기 (21)
      • Jetpack Compose (15)
      • Kotlin (3)
      • Android Studio (2)
      • Tistory (1)
  • 인기 글

  • hELLO· Designed By정상우.v4.10.0
브애애앳
[Jetpack Compose] Scroll Picker 만들기
상단으로

티스토리툴바