깨알 개념/Android

[Android] Pointer input - Scroll

interfacer_han 2025. 2. 17. 23:54

#1 개요

 

스크롤  |  Jetpack Compose  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 스크롤 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 참고: 항목 목록을 표시하려면 이러한 API 대신

developer.android.com

위 공식 문서를 나의 언어로 정리했다.

 

#2 스크롤'되는' Modifier

#2-1 Modifier.verticalScroll()

fun Modifier.verticalScroll(
    state: ScrollState,
    enabled: Boolean = true,
    flingBehavior: FlingBehavior? = null,
    reverseScrolling: Boolean = false
): Modifier

당신이 현재 보고 있는 이 페이지는 모니터보다 클 것이다. 그래서 당신은 마우스휠로 이 페이지를 스크롤하고 있다. verticalScroll()은 어떤 콘텐츠가 해당 콘텐츠가 담긴 컨테이너보다 (세로 방향으로) 클 때, 본 함수를 통해 컨테이너를 수직 스크롤하게 만들어준다. 반대로 verticalScroll()이 부재하다면, 컨테이너보다 콘텐츠가 큼에도 스크롤할 수 없다.

 

#2-2 Modifier.horizontalScroll()

fun Modifier.horizontalScroll(
    state: ScrollState,
    enabled: Boolean = true,
    flingBehavior: FlingBehavior? = null,
    reverseScrolling: Boolean = false
): Modifier

verticalScroll()의 수평 버전.

 

#3 스크롤 '가능한' Modifier

#3-1 Modifier.scrollable()

fun Modifier.scrollable(
    state: ScrollableState,
    orientation: Orientation,
    enabled: Boolean = true,
    reverseDirection: Boolean = false,
    flingBehavior: FlingBehavior? = null,
    interactionSource: MutableInteractionSource? = null
): Modifier

이름 한 번 못 지었다. verticalScroll()과 늬앙스가 너무나도 겹치지 않는가? 명백하게 다른 동작을 하는 함수임에도 말이다. "'아' 다르고 '어' 다르다"라는 말을 들어본 적이 있을 것이다. 그 말만큼 이 함수를 잘 설명하는 말은 찾기 어려울 것이다. 결론부터 말하자면 scrollable() 함수는,

 

어떤 컨테이너를 스크롤 가능케 만들지만, 그렇다고 스크롤이 되게 만들진 않는 함수

다. 이 정의가 혼란스러울 수밖에 없을 것이다. 이건 예시 코드와 그 예시 코드가 동작하는 GIF 파일을 보는 수밖에 없다. 바로 아래에서 이어진다.

 

#3-2 예시 코드와 그 결과

코드

@Composable
private fun ScrollableSample() {
    // 합산 스크롤량을 저장할 State
    var offset by remember { mutableStateOf(0f) }
    Box(
        Modifier
            .size(150.dp)
            .scrollable(
                orientation = Orientation.Vertical,
                // 매 스크롤마다 호출되는 콜백 함수
                state = rememberScrollableState { delta ->
                    offset += delta // 수직 스크롤 변화량(delta)를 offset에 더해나감
                    delta
                }
            )
            .background(Color.LightGray),
        contentAlignment = Alignment.Center
    ) {
        Text(offset.toString())
    }
}

Box()에 scrollable()을 적용했다. 이제 이 Box()는 위에서 말한대로, '스크롤이 가능하지만 그렇다고 스크롤이 되지는 않아야' 한다.

 

결과 그리고 목적

https://developer.android.com/develop/ui/compose/touch-input/pointer-input/scroll#scrollable-modifier

난 GIF 이미지를 보고 나서야 비로소, 스크롤 가능하다했지 된다고는 안 했다는 말의 늬앙스를 알게 됐다. 또, scrollable()의 목적도 직관적으로 느껴진다. 바로 스크롤 측정이다. 측정의 전제 조건은, 스크롤이 가능(able)해야 한다는 것이다.

 

#3-3 ScrollableState

scrollable()의 매개변수 중엔 ScrollableState가 있다. 스크롤(Scroll) 가능한(able) 상태(State)라니. 바로 위에서 언급했던 scrollable()의 목적에 기반해 생각해 보면 ScrollableState는 스크롤 측정과 관련된 객체임은 분명하다. verticalScroll()의 ScrollState가 현재 어느 정도 스크롤 되었는지의 값을 보유하는 것과 달리, ScrollableState는 그런 값 대신 어떤 콜백 함수 하나만을 저장한다. 그래서 이 클래스의 인스턴스를 만들려면 생성자에 콜백 함수를 전달해 줘야 한다.

 

#3-4 ScrollableState의 콜백 함수

스크롤 측정의 방식을 정의한다. '측정'이라는 단어를 문장으로 바꾸면 '얼마나 스크롤되었는가?'다. 그렇다면 '측정의 방식'은 "측정을 어떻게 편향시킬 것인가?"가 된다. 이는 마치 중립을 지키지 않는 언론사가 실제 모습과 보도 내용을 상이하게 편향시키는 것과 같다.

 

ScrollalbeState가 보유하는 콜백 함수는 Float형 숫자 하나를 매개변수로 받고, Float형 숫자를 반환한다. 매개변수 Float는 스마트폰 사용자가 실제로 화면에 손가락을 실제로 문지른 길이값이다. 그리고 반환하는 Float는 '편향'시킨 정도다. 사용자의 실제 스크롤 길이의 30%를 삭감해서 버리고 싶은 상황이라고 가정한다. 이 경우 콜백 함수가 100.0F를 매개변수로 받아도 30.0F을 반환하게 만드는 식으로, 프로그래머가 사용자의 실제 스크롤을 '편향'시킬 수 있는 것이다.

 

주의할 점은, 반환되는 Float를 30.0F아니라 무심코 70.0F라고 쓰지 않아야 한다는 것이다. '편향이 가해진 원본'이 아니라 '편향시킨 정도'를 반환하는 것이다.

 

#3-5 메소드 체이닝 1

스크롤이 되게 하면서 delta(움직인 거리)를 추적하려면 어떻게 해야 할까? 바로 scrollable()과 verticalScroll() (또는 horizontalScroll())를 연달아 메소드 체이닝 하면 된다.

 

val scrollableState = rememberScrollableState { delta ->
    println("delta: $delta") // ← 대충 delta를 추척하는 코드라고 가정한다
    delta * 9 / 10 // 실제 스크롤의 90%를 삭감했다. 굉장히 느리게 스크롤될 것이다.
}
val scrollState = rememberScrollState()

Column(
    modifier = Modifier
        .fillMaxWidth()
        .verticalScroll(
            state = scrollState
        )
        .scrollable(
            state = scrollableState,
            orientation = Orientation.Vertical
        )
) {
    val textCount = 20
    repeat(textCount) {
        Text(
            text = "(${it + 1} / ${textCount})",
            fontSize = 32.sp,
        )
        Spacer(modifier = Modifier.height(24.dp))
    }
}

콜백 함수가 매개변수로 받은 delta의 100%가 아닌 90% 소비하게 만들었다. 사용자는 꽤 힘들 것이다. 왜냐하면 같은 양을 스크롤 하기 위해서 평소보다 손가락을 10배나 더 움직여야 하기 때문이다.

 

#3-6 메소드 체이닝 2

val scrollableState = rememberScrollableState { delta ->
    println("delta: $delta") // ← 대충 delta를 추척하는 코드라고 가정한다
    delta * 9 / 10 // 실제 스크롤의 90%를 삭감했다. 굉장히 느리게 스크롤될 것이다.
}
val scrollState = rememberScrollState()

Column(
    modifier = Modifier
        .fillMaxWidth()
        .scrollable(
            state = scrollableState,
            orientation = Orientation.Vertical
        )
        .verticalScroll(
            state = scrollState
        )
) {
    val textCount = 20
    repeat(textCount) {
        Text(
            text = "(${it + 1} / ${textCount})",
            fontSize = 32.sp,
        )
        Spacer(modifier = Modifier.height(24.dp))
    }
}

#3-5와 메소드 체이닝 순서를 제외하면 완전히 동일한 코드다. 하지만 스크롤이 평범하게 잘 된다. 왜냐하면 scrollableState의 콜백 함수가 적용되지 않았기에 그렇다. Modifier에서 이벤트 전파 방향은 (메소드 체이닝 순서의) 역방향이라는 것을 기억하자. 실제로 스크롤이 되게 하는 verticalScroll() 입장에서는 편향되기 전의 스크롤 이동량(delta)을 받아본 셈이니 스크롤이 정상적으로 멀쩡히 돼버린 것이다.

 

#4 요약

verticalScroll()과 scrollable() 둘 다 컴포저블을 스크롤 가능케 만든다. 그러나, 스크롤 되게 만드는 건 verticalScroll() 뿐이다.

 

#5 이어지는 글

 

[Android] Pointer input - Nested Scroll

#1 이전 게시글 [Android] Pointer input - Scroll#1 개요 스크롤  |  Jetpack Compose  |  Android Developers이 페이지는 Cloud Translation API를 통해 번역되었습니다. 스크롤 컬렉션을 사용해 정리하기 내 환경설정

kenel.tistory.com