이번엔 로또 번호 생성기를 만들어 볼 예정이다.
사용자는 번호를 선택할 수 있고, 버튼을 누르면 선택하지 않은 번호만큼 로또 번호를 랜덤으로 추천 해 준다.
우선 번호를 선택할 넘버 피커를 만들어 보자.
컴포즈에선 넘버 피커를 지원해주지 않는다.
하지만 커스텀 해서 만들 수 있는데,
스택오버플로우에서 좋은 자료가 있어서 참고해서 만들었다.
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를 아이템에 넣어준다.
다음으론 선택된 로또 번호를 표기해줄 뷰를 만들어줄 거다.
횡렬로 늘어져 있고 아이템이 선택할때 마다 변하니 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,
)
}
}
간단한 기능이지만 연습이 많이 된 것 같다.