Android/Jetpack Compose

[Android Compose] Scroll과 imePadding을 같이 쓰면 제대로 동작을 안한다구요?

chattymin 2025. 1. 31. 15:14
728x90

문제상황

Column에 ScrollState를 넣어주고, 내부에 TextField를 여러개 넣어뒀다.

그 후 키보드 높이 조정을 위해 adjustResize 설정을 한 후 imePadding을 넣어줬지만, 제대로 동작하지 않는다.

문제 영상

 

 

정확히는 아래에 있는 TextField가 보일 정도로 올라오긴 하지만, 내가 원하는 만큼 더 위로 올라오진 않는다.

 

찾아보니 LazyColumn, Column에 verticalScroll을 넣어준다면 이런 오류가 존재한다는 것을 찾았다.

https://issuetracker.google.com/issues/229628557

 

Google Issue Tracker

 

issuetracker.google.com

22년부터 이렇다는데 좀 고쳐주지 참...

 

 

뭐 구글이 이러는게 하루이틀이 아니니 직접 해결해보자.

 

 

 

어떻게 해결할 수 있을까?

1. TextField에 focus가 들어온다면 Scroll을 바닥으로 박아버리자

imePadding을 넣는다면 화면 아래에 키보드 높이만큼 자연스럽게 패딩이 생긴다

그렇기에 화면을 아래로 Scroll을 박아버린다면 자연스럽게 TextField가 전체적으로 다 보이지 않을까 했다.

 

TextField에 focus가 들어온다면 imePadding이 생길 것이다. 그렇기에 imeHeight를 구해서 이 값이 새로 생긴다면 스크롤을 조정하도록 코드를 작성해보았다.

 

    ...
    val imeInsets = WindowInsets.ime
    val imeHeight = imeInsets.getBottom(LocalDensity.current)

    LaunchedEffect(imeHeight) {
        if (imeHeight > 0) {
            scrollState.scrollTo(scrollState.maxValue)
        }
    }
    ...

 

 

우선 결과적으로 말하면 동작은 잘 됐다.

하지만, 해당 화면에서 TextField가 여러개이기 때문에 제일 위의 TextField를 클릭해도 바닥으로 Scroll이 박혀버린다는게 문제였다.

 

하나의 TextField가 바닥에 하나만 존재한다면 적용할만한 방법인 것 같다.

 

 

2. 해당 컴포넌트의 position으로 화면을 scroll을 이동하자!

이 아이디어는 iOS의 Swift UI에서 영감을 받았다.

 

Swift UI에서는 Scroll을 하기 위한 anchor가 존재한다.

특정 컴포넌트에 anchor를 만들어 준다면, 해당 anchor로 scroll하도록 설정이 가능하다

https://developer.apple.com/documentation/swiftui/view/scrollposition(_:anchor:)

 

이 기능을 직접 구현하여 문제를 해결해보고자 했다.

 

첫번째, Modifier 확장함수로 구현하자.

컴포넌트 내부에 기능을 넣는다면 추후 구현하게될 모든 TextField에 구현해야 한다.

그렇다면 너무나 중복 코드가 생기기에 Modifier 확장함수로 구현하고자 했다.

 

그렇게 초기 틀을 잡을 수 있었다.

fun Modifier.animateScrollAroundItem(): Modifier = composed {
   // Something To do
}

 

 

두번째, Item의 Position을 구하자

내가 이동하고자 하는 item의 위치를 구하고 Scroll시켜보자.

item의 위치의 경우 onGloballyPositioned라는 확장함수를 사용하면 구할 수 있다.

 

또한 화면을 이동시켜야 하기에 ScrollState를 매개변수로 받아왔다.

 

마지막으로, 해당 아이템이 포커스 되었을 때. 즉, TextField를 클릭했을 때 화면이 이동해야 하기에 onFucusEvent 내부에 스크롤을 이동시키는 코드를 넣어줬다.

fun Modifier.animateScrollAroundItem(
    scrollState: ScrollState
): Modifier = composed {
    var scrollToPosition by remember {
        mutableFloatStateOf(0f)
    }
    val coroutineScope = rememberCoroutineScope()

    this
        .onGloballyPositioned { coordinates ->
            scrollToPosition = coordinates.positionInParent().y
        }
        .onFocusEvent {
            if (it.isFocused) {
                coroutineScope.launch {
                    scrollState.animateScrollTo(scrollToPosition.toInt())
                }
            }
        }
}

 

 

이렇게 코드를 작성하고 기쁘게 실행했더니...

 

 

 

 

positon의 y좌표가 해당 item의 시작점이었다. 

즉, 해당 item의 제일 윗부분이 키보드의 시작점과 겹쳐져 정확히 다 가려지는 문제가 있었다.

 

이를 해결해야 했다.

 

세번째, position에서 해당 item의 높이를 더해주자

코드는 간단하다. 이전에 구현해두었던 onGloballyPositioned에서 해당 컴포넌트의 height를 구해주면 된다.

fun Modifier.animateScrollAroundItem(
    scrollState: ScrollState
): Modifier = composed {
    var scrollToPosition by remember {
        mutableFloatStateOf(0f)
    }
    val coroutineScope = rememberCoroutineScope()

    this
        .onGloballyPositioned { coordinates ->
            val itemTop = coordinates.positionInParent().y
            val itemHeight = coordinates.size.height

            scrollToPosition = itemTop + itemHeight
        }
        .onFocusEvent {
            if (it.isFocused) {
                coroutineScope.launch {
                    scrollState.animateScrollTo(scrollToPosition.toInt())
                }
            }
        }
}

이렇게 구현한다면 해당 itm의 높이까지 반영되어 내가 원하는 위치로 scroll되게 된다.

 

 

 

그렇게 원하는대로 동작이 되었고, 끝난줄 알았다...

 

 

네번째, 해당 아이템의 높이에서 커스텀을 해보자

완성하고 PR을 올린 후 곰곰히 생각해보니 우리 서비스에서는 해당 TextField 밑에 TextField를 추가하는 버튼이 있었다.

그렇다면 이게 가려지는게 맞는 UX일까? 사용자는 TextField를 추가해주기 위해서 스크롤을 필수로 해야하는게 맞을까?

 

그렇기에 디자이너 선생님들께도 질문을 했다.

 

디자이너 선생님에게도 후자가 더 낫다는 말을 들었고, 이를 위해 y position의 가중치를 넣을 수 있게 슬롯을 뚫어주기로 했다.

 

그 결과 최종 코드는 아래와 같다.

fun Modifier.animateScrollAroundItem(
    scrollState: ScrollState,
    verticalWeight: Int = 0
): Modifier = composed {
    var scrollToPosition by remember {
        mutableFloatStateOf(0f)
    }
    val coroutineScope = rememberCoroutineScope()

    this
        .onGloballyPositioned { coordinates ->
            val itemTop = coordinates.positionInParent().y
            val itemHeight = coordinates.size.height

            scrollToPosition = itemTop + itemHeight + verticalWeight
        }
        .onFocusEvent {
            if (it.isFocused) {
                coroutineScope.launch {
                    scrollState.animateScrollTo(scrollToPosition.toInt())
                }
            }
        }
}

 

verticalWeight에 원하는 값을 넣어준다면 해당 아이템의 밑으로 그 값만큼 더 내려가서 스크롤이 멈추게 된다.

 

 

결과

꽤나 확장성 있는 확장함수가 만들어진 것 같다.

scrollState를 넣어주는 것 정도는 해당 화면이 Scroll된다는 가정 하에 만든 확장함수이고, 내부에서 Scroll시키는 로직이 있기에 필수적인 매개변수라 생각한다.

또한 가중치는 기본값으로 0을 넣어두었기에 필요하다면 넣어주고, 필요없으면 안넣어줘도 되도록 구현했다.

 

onGloballyPositioned는 신이다. 뭐낙 헷갈리면 저거 쓰면 기능 많이 구현할 수 있더라.

728x90