본문 바로가기

Android

[Android] Compose Scrollable Column에서 처음 보여지는 타이밍 캐치하기

들어가는 글

신규 기능으로 매출 분석을 차트, 막대 그래프 등 시각적으로 표현하는 기능을 개발하게 되었습니다.

평소 토스의 인터랙션과 애니메이션에 관심이 많았기에 소비 분석 화면을 참고해보기로 했습니다.

소비 분석엔 카테고리별 소비, 일별 소비, 월별 소비, 고정 지출 등 다양한 분석이 Column에 나열되어 있습니다.

막대그래프의 높이 애니메이션, 원그래프의 애니메이션은 Column을 스크롤해 화면에 표시되는 순간 시작하게 됩니다.

토스처럼 Scrollable Column에서 화면에 보여졌을 때 애니메이션이 시작되길 원했습니다.

처음에는 LazyColumn을 사용해봤지만 하단을 갔다가 상단을 갔을 때 애니메이션이 다시 시작되는 문제가 있었고, debug 환경에서 프레임이 떨어지는 현상도 있어 일반 Column을 활용해 보기로 했습니다.

개발하려는 화면이 10개 미만의 Composable로 나눠 Column에 배치할 수 있어 Column 사용이 문제가 되진 않았습니다.

구현

Composable이 화면에 보여지는 타이밍을 잡기 위해 Modifier.onGlobalPositioned extension과 LayoutCoordinates.boundsInWindow extension을 활용했습니다.

onGlobalPositioned extension은 Layout의 Global Position이 변경되었을 때 호출되면 이때 Parameter로 넘어온 LayoutCoordinates 객체는 레이아웃의 Size, Offset와 같은 정보와 다양한 Function, Extension을 제공하고 있습니다.

그 Extension 중 boundsInWindow()는 레이아웃이 Window(전체화면)에서 차지하는 영역 Rect를 계산해주는데요.

이때 화면을 벗어나 보여지지 않고 있는 경우 Zero Rect를 반환해줍니다.

이를 활용해 화면에 처음 보여지는 타이밍을 캐칭하는 Modifier.onFirstVisible Extension을 개발했습니다.

import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned

@Stable
fun Modifier.onFirstVisible(action: () -> Unit): Modifier = composed {
    // 한 번만 호출하기 위한 Flag Boolean
    var flag by remember {
        mutableStateOf(false)
    }
    onGloballyPositioned {
        if (flag.not() && it.boundsInWindow() != Rect.Zero) {
            flag = true
            action()
        }
    }
}

활용

저 같은 경우엔 onFirstVisible callback이 호출되면 Composable의 MutableState(Boolean) 값을 true로 변경해

LaunchEffect가 실행되어 Animataion이 시작되도록 하거나 Composition되는 순간 애니베이션이 시작되는 Composable이 표시되도록 했습니다.

@Composable
fun AnimationView(modifier: Modifier = Modifier, idx: Int) {
    var isShown by remember {
        mutableStateOf(false)
    }
    val progress = remember {
        Animatable(0f)
    }
    if (isShown)
        LaunchedEffect(Unit) {
            progress.animateTo(1f, animationSpec = tween(1000))
        }
    Row(modifier = modifier.onFirstVisible {
        isShown = true
    }) {
        Text(text = idx.toString())
        LinearProgressIndicator(modifier = Modifier.weight(1f), progress = { progress.value })
    }
}
Column(
    modifier = Modifier
        .padding(innerPadding)
        .verticalScroll(rememberScrollState())
) {
    repeat(100) {
        AnimationView(
            modifier = Modifier
                .fillMaxWidth()
                .height(40.dp),
            idx = it
        )
    }
}

결과

주의할 점으로는 Modifier에 padding을 먼저 적용하고 onFirstVisible을 뒤에 적용할 경우 padding 영역을 제외한 부분이 표시되었을 때 callback이 호출되게 됩니다. 이와 관련해선 Modifier 순서 공식문서를 참고하면 좋습니다.

https://developer.android.com/develop/ui/compose/layouts/constraints-modifiers?hl=ko

 

제약 조건 및 수정자 순서  |  Jetpack Compose  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 제약 조건 및 수정자 순서 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose에서는 여러 수정자를

developer.android.com

이를 활용해 처음 보여지는 타이밍 뿐만 아니라 보여지고 사라지는 타이밍에 대한 Extension도 개발해봤습니다.

@Stable
fun Modifier.onVisibleChanged(action: (visible: Boolean) -> Unit): Modifier = composed {
    var flag by remember {
        mutableStateOf(false)
    }
    onGloballyPositioned {
        if (flag.not() && it.boundsInWindow() != Rect.Zero) {
            flag = true
            action(true)
        } else if (flag && it.boundsInWindow() == Rect.Zero) {
            flag = false
            action(false)
        }
    }
}

개선이 필요한 부분이나 더 좋은 방법이 있다면 댓글 부탁드립니다!

참고

https://github.com/bjj3036/OnFirstVisible

 

GitHub - bjj3036/OnFirstVisible

Contribute to bjj3036/OnFirstVisible development by creating an account on GitHub.

github.com

https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/OnGloballyPositionedModifier

 

OnGloballyPositionedModifier  |  Android Developers

androidx.compose.desktop.ui.tooling.preview

developer.android.com

 

https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/LayoutCoordinates#(androidx.compose.ui.layout.LayoutCoordinates).boundsInWindow()

 

LayoutCoordinates  |  Android Developers

androidx.compose.desktop.ui.tooling.preview

developer.android.com