본문 바로가기
⛏️ | 개발 기록/⏰ | Schedule Planner

[Android, Kotlin] Compose Lazycolumn의 scroll state 관리하기 (6)

by immgga 2023. 10. 21.

이번에 Planner v2를 개발하면서 Plan(main) screen에서의 add schedule icon이 너무 사용자들에게 불편할 수도 있다는 느낌을 받았었다.

기존 화면

일정 header 부분에 + icon은 사용자들이 실제로 사용한다면 icon의 크기가 작아서 클릭이 힘들어 보일 것 같았다.(크기를 키우기에는 header 크기도 같이 커지기에 애매했다.)

그래서 기존의 icon을 LazyColumn의 맨 아래에 card로 만들어서 생성할 수 있도록 만들기로 했다.

그렇게 만들면 일정을 많이 생성하는 사용자들이 스크롤을 한참 해야 맨 아래로 도달해서 불편할 것 같았기에 따로 맨 아래, 맨 위로 스크롤 해주는 작은 button도 만들어 보기로 했다.

 

1. 자동 Scroll Button 만들기

button의 ui 구성은 간단하게 둥근 버튼에 화살표를 달아서 만들어 주기로 했다.

버튼 ui(?)

LazyColumn의 scroll을 관리하기 위해서는 scrollState가 필요한데, LazyColumn에서는 rememberScrollState()를 사용할 수 없지만 다른 방법으로 사용 가능하다.

val scrollState = rememberLazyListState()
val scope = rememberCoroutineScope()
var scrollIsLastState by remember { mutableStateOf(false) }     // 스크롤을 더 이상 할수 없는가?(마지막 스크롤인가?)

rememberLazyListState()를 이용해 LazyColumn의 scroll state를 가져올 수 있다.

coroutineScope랑 scroll이 가장 아래로 스크롤되어 있는지 확인하는 remember 변수도 만들어 주었다.

 

state도 만들어 주었으니 바로 compose로 만들어 보자.

IconButton(
    modifier = Modifier
        .clip(CircleShape)
        .align(Alignment.BottomEnd)
        .padding(end = 20.dp, bottom = 20.dp),
    onClick = {
        scope.launch {
            if (scrollIsLastState) scrollState.animateScrollToItem(index = 0)
            else scrollState.animateScrollToItem(index = list.lastIndex)
        }
    },
    colors = IconButtonDefaults.iconButtonColors(
        containerColor = Color(0xFF6EC4A7)
    )
) {
    Icon(
        modifier = Modifier
            .padding(10.dp)
            .rotate(if (scrollIsLastState) 180f else 0f),
        painter = painterResource(id = R.drawable.ic_scroll_arrow),
        tint = Color.White,
        contentDescription = "down arrow"
    )
}

IconButton Composable로 만들어 주었고, onClick 부분에서는 scrollIsLastState 변수의 상태에 따라서 LazyColumn이 scroll 되도록 하는 로직을 구현해 보았다.

 

scrollIsLastState의 상태를 변화시키기 위해서는 LazyColumn의 scroll이 처음 또는 마지막일 때, 상태가 변하게 해야 하기 때문에 LaunchedEffect Composable을 이용해야 한다. 그러기 위해서는 scroll이 처음 또는 마지막일 때를 체크할 수 있는 변수들이 필요하다.

val cantScrollForward = !scrollState.canScrollForward       // 앞으로는 더 스크롤할 수 없음
val cantScrollBackward = !scrollState.canScrollBackward     // 뒤로는 더 스크롤할 수 없음

위 두 개의 변수를 만들어서 LaunchedEffect()의 key로 활용해 보겠다.

LaunchedEffect(key1 = cantScrollForward, key2 = cantScrollBackward) {
    if (cantScrollForward) scrollIsLastState = true
    if (cantScrollBackward) scrollIsLastState = false
}

이렇게 구현하면 cantScrollForward(Backward)의 상태 변화에 따른 scrollIsLastState의 상태를 변화시킬 수 있다.

 

2. add schedule card 만들기

다음으로는 add schedule card를 만들어 보겠다.

card에는 border를 추가할 것이다. 그 대신 border를 점선으로 만들어 보겠다.

add schedule card ui(?)

바로 만들어 보자.

Stroke()를 이용해 점선의 border를 만들 수 있다.

val stroke = Stroke(width = 2f, pathEffect = PathEffect.dashPathEffect(intervals = floatArrayOf(10f, 10f), phase = 10f))

width와 pathEffect를 설정한다. PathEffect.dashPathEffect()가 점선으로 설정하는 기능인 것 같다.

dashPathEffect() 안에서 점선을 커스터마이징 할 수 있다.

 

점선을 만들었으니 바로 적용을 해보자.

Box(
    modifier = Modifier
        .fillMaxWidth()
        .padding(vertical = 7.dp, horizontal = 10.dp)
        .clip(RoundedCornerShape(8.dp))
        .drawBehind {
            drawRoundRect(
                color = Color.LightGray,
                style = stroke,
                cornerRadius = CornerRadius(8.dp.toPx())
            )
        }
        .clickable { onCardClick() },
    contentAlignment = Alignment.Center
) {
    Text(
        modifier = Modifier.padding(vertical = 15.dp),
        text = "클릭해서 일정 추가하기...",
        color = Color.LightGray,
        fontSize = 20.sp
    )
}

Box Composable 안의 Modifier.drawBehind에서 점선을 적용할 수 있다.

drawRoundRect()로 모서리가 둥근 테두리를 설정할 수 있다.

color에서 자신이 원하는 점선의 color, style에서 이전에 설정한 Stroke()를, cornerRadius에서 radius를 설정한다.

 

3. 전체 코드

val scrollState = rememberLazyListState()
val scope = rememberCoroutineScope()
var scrollIsLastState by remember { mutableStateOf(false) }     // 스크롤을 더 이상 할수 없는가?(마지막 스크롤인가?)

val cantScrollForward = !scrollState.canScrollForward       // 앞으로는 더 스크롤할 수 없음
val cantScrollBackward = !scrollState.canScrollBackward     // 뒤로는 더 스크롤할 수 없음
LaunchedEffect(key1 = cantScrollForward, key2 = cantScrollBackward) {
    if (cantScrollForward) scrollIsLastState = true
    if (cantScrollBackward) scrollIsLastState = false
}

// change firebase date
val list = listOf(1,2,3,4,5,6,7,8,9,10)
val checkBoxList = listOf(true, false, false, false, false, false, false, false, false, true)

Box(modifier = Modifier.fillMaxSize()) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        state = scrollState
    ) {
        item {
            AndroidView(
                modifier = Modifier.fillMaxWidth(),
                factory = { CalendarView(it) }
            ) { calendarView ->
                val selectedDate = "${yearState}-${monthState}-${dayState}"
                calendarView.date = formatter.parse(selectedDate)!!.time

                calendarView.setOnDateChangeListener { _, year, month, day ->
                    yearState = year.toString()
                    monthState = (month + 1).toString()
                    dayState = day.toString()
                }
            }
        }

        stickyHeader { ScheduleHeader(month = monthState, day = dayState) }

        items(list) {
            ScheduleItem(
                checked = checkBoxList[it - 1],
                onCheckBoxClick = {

                }
            )

            Divider(
                modifier = Modifier
                    .height(1.dp)
                    .padding(horizontal = 15.dp)
            )
        }

        item {
            AddScheduleCard {}
        }
    }

    IconButton(
        modifier = Modifier
            .clip(CircleShape)
            .align(Alignment.BottomEnd)
            .padding(end = 20.dp, bottom = 20.dp),
        onClick = {
            scope.launch {
                if (scrollIsLastState) scrollState.animateScrollToItem(index = 0)
                else scrollState.animateScrollToItem(index = list.lastIndex)
            }
        },
        colors = IconButtonDefaults.iconButtonColors(
            containerColor = Color(0xFF6EC4A7)
        )
    ) {
        Icon(
            modifier = Modifier
                .padding(10.dp)
                .rotate(if (scrollIsLastState) 180f else 0f),
            painter = painterResource(id = R.drawable.ic_scroll_arrow),
            tint = Color.White,
            contentDescription = "down arrow"
        )
    }
}

LazyColumn의 state 파라미터를 이용해 현재 scroll 상태를 저장해 주는 기능도 추가했다.

 

4. 구현 영상

IconButton을 화면 하단 오른쪽에, Add Schedule Card를 LazyColumn의 맨 아래에 놓은 모습이다.

 

정리

  • 이번에 위 2개의 기능을 추가로 구현하면서 scroll state의 활용 방법을 많이 알게 되었던 시간이었다.
  • 이번에 UI 교체로 사용자한테 편안한 UI에 대해 생각해 보는 시간이 되었다.
728x90

댓글