[Compose] NumberPicker 구현, 로또 번호 생성기

KSang·2024년 4월 17일
0

TIL

목록 보기
85/101

이번엔 로또 번호 생성기를 만들어 볼 예정이다.

사용자는 번호를 선택할 수 있고, 버튼을 누르면 선택하지 않은 번호만큼 로또 번호를 랜덤으로 추천 해 준다.

넘버피커

우선 번호를 선택할 넘버 피커를 만들어 보자.

컴포즈에선 넘버 피커를 지원해주지 않는다.

하지만 커스텀 해서 만들 수 있는데,

스택오버플로우에서 좋은 자료가 있어서 참고해서 만들었다.
https://gist.github.com/inidamleader/7bcc273afe6b885738556d190582a815

fun Picker(
    modifier: Modifier = Modifier,
    items: List<String>,
    state: PickerState = rememberPickerState(),
    startIndex: Int = 0,
    visibleItemCount: Int = 5,
    textModifier: Modifier = Modifier,
    dividerColor: Color = LocalContentColor.current,
    content: @Composable (String) -> Unit
) {

컴포저블의 파라 미터를 이렇게 구성했다.

넘버 피커의 크기를 담당하는 modifier

목록에 사용된 아이템들을 받는 items

state는 선택된 아이템을 나타낸다.

PickerState 클래스를 따로 만들어 줬는데,

class PickerState {
    var selectedItem by mutableStateOf("")
}

@Composable
fun rememberPickerState() = remember {
    PickerState()
}

클래스 내에 상태를 선언해주고 컴포저블로 만들어서 기본값으로 넣어 줬다.


) {
    val visibleItemsMiddle = visibleItemCount / 2
    val listScrollCount = Int.MAX_VALUE
    val listScrollMiddle = listScrollCount / 2
    val listStartIndex =
        listScrollMiddle - listScrollMiddle % items.size - visibleItemsMiddle + startIndex

    fun getItem(index: Int) = items[index % items.size]

    val scrollState = rememberLazyListState(initialFirstVisibleItemIndex = listStartIndex)
    val flingBehavior = rememberSnapFlingBehavior(lazyListState = scrollState)

    var itemHeightPixel by remember { mutableIntStateOf(0) }
    val itemHeightToDp = pixelsToDp(pixels = itemHeightPixel)
    
        LaunchedEffect(scrollState) {
        snapshotFlow { scrollState.firstVisibleItemIndex }
            .map { index -> getItem(index + visibleItemsMiddle) }
            .distinctUntilChanged()
            .collect { item ->
                state.selectedItem = item
            }

이전에 뷰페이저로 무한 스크롤을 표현한 방법 중 MAX_VALUE를 사용해 유사 무한 스크롤을 구현한 적이 있는대, 그와 비슷하게 구현했다.

rememberLazyListState는 LazyColumn 같은 스크롤 가능한 컴포저블에서 현재 스크롤 위치와 상태를 나타낸다.

rememberSnapFlingBehavior 는 스크롤 동작을 관리하는데, 사용자가 스크롤을 빠르게 할 때 아이템이 자연스럽게 정렬되로록 도와주는 snap효과를 구현한다.

사용자가 스크롤을 멈추면, 가장 가까운 아이템이 화면 중앙이나 명시적으로 지정된 위치에 오도록 조정한다.

scrollState를 참조하게 만들어 snap효과를 줬다.

    val fadingEdgeGradient = remember {
        Brush.verticalGradient(
            0f to Color.Transparent,
            0.5f to Color.Black,
            1f to Color.Transparent
        )
    }
    
    private fun Modifier.fadingEdge(brush: Brush) = this
    .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
    .drawWithContent {
        drawContent()
        drawRect(brush = brush, blendMode = BlendMode.DstIn)
    }

그라데이션을 표현 할 수도 있는데, 피커의 상하 부분을 투명하게 가운데 부분을 검은색으로 구성했다.

graphicsLayer를 통해서 그래픽 처리를 위한 설정을 해준다.

compositingStrategy 속성은 CompositingStrategy.Offscreen으로 설정되어 있는걸 볼 수 있는데, 그리기 연산을 오프스크린 버퍼에 먼저 수행하고, 최종 결과를 화면에 합성하게 한다.


    Box(modifier = modifier) {
        LazyColumn(
            state = scrollState,
            flingBehavior = flingBehavior,
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier
                .fillMaxWidth()
                .height(itemHeightToDp * visibleItemCount)
                .fadingEdge(fadingEdgeGradient)
        ) {
            items(listScrollCount) { index ->
                Text(
                    text = getItem(index),
                    textAlign = TextAlign.Center,
                    fontSize = 16.sp,
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                    modifier = Modifier
                        .onSizeChanged { size -> itemHeightPixel = size.height }
                        .then(textModifier)
                )

            }
        }
        HorizontalDivider(
            color = dividerColor,
            modifier = Modifier.offset(y = itemHeightToDp * visibleItemsMiddle)
        )
        HorizontalDivider(
            color = dividerColor,
            modifier = Modifier.offset(y = itemHeightToDp * (visibleItemsMiddle + 1))
        )
    }
    content(state.selectedItem)

이제 피커가 들어갈 박스를 만들면된다.

위에서 정의한 scrollState를 LazyColumn에 연결해주고 위 에 정의 한 int.MAX_VALUE를 아이템에 넣어준다.

ball

다음으론 선택된 로또 번호를 표기해줄 뷰를 만들어줄 거다.

횡렬로 늘어져 있고 아이템이 선택할때 마다 변하니 LazyRow를 사용해 구현해 보자.

@Composable
fun LottoBox(
    selectedItems: List<String>
) {
    fun getItem(index: Int) = selectedItems[index]
    LazyRow(
        modifier = Modifier
            .fillMaxWidth()
            .height(80.dp)
            .padding(8.dp)
            .background(LottoBoxColor),
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically
    ) {
        items(selectedItems.size) { index ->
            LottoBall(item = getItem(index).toInt())
            Spacer(modifier = Modifier.size(8.dp))
        }
    }
}

@Composable
fun LottoBall(
    item: Int,
) {
    val ballColor = when (item) {
        in 1..10 -> Color.Yellow
        in 11..20 -> Color.Blue
        in 21..31 -> Color.Red
        in 31..40 -> Color.Gray
        else -> Color.Green
    }
    Box(
        modifier = Modifier
            .clip(CircleShape)
            .size(32.dp)
            .background(ballColor),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = item.toString(),
            color = Color.White,
            fontWeight = FontWeight.Bold,
        )
    }
}

간단한 기능이지만 연습이 많이 된 것 같다.

https://github.com/Guri999/ComposeLotto

0개의 댓글