달달한 스토리

728x90
반응형

출처 핀터레스트

 

컴포즈를 공부하는 중이라 내용이 정확하지 않고,

 

부실할 수 있다는 점 양해 바랍니다.

 

오늘은 간단히 compose에서 사용하는

 

viewPager 기능과 유사한 기능을 사용함을 써,

 

앱을 처음 시작할 때 사용자에게 소개하는 가이드 화면을 예시로 만들어 보려고 합니다.

 

 

예시로 검은색 화면을 두었습니다. 가이드 화면이 있고,

 

옆으로 슬라이드 하게 되면

 

아래 왼쪽에 큰 하얀색점이 작아지고,

 

오른쪽 점이 커지게 하여, 가이드 페이지의 위치와 개수를 표현하게 됩니다.

 

 

사실 여러 viewPager를 찾는 중에 첫 번째 방법은

 

HorigontalPager라는 페이저 라이브러리였는데,

 

이러한 편리한 라이브러리도 있구나 정도로 생각을 했고,

 

두 번째 방법은 굳이 라이브러리를 사용하지 않고도,

 

만들 수 있는 법이 있었습니다.

 

아래 코드를 보겠습니다.

 

@Composable
fun OnboardingScene(
    modifier: Modifier = Modifier,
    imageList: List<Int> = MainActivity.imageList,
) {
    // 스크롤의 position의 상태를 저장.
    val lazyListState = rememberLazyListState()

    //페이지의 스크롤 되는중인지, 어떤페이지인지에 대한 정보(PagerSnapState) 즉 페이지 상태를 반환하는 메서드이다.
    val state = rememberPagerSnapState()

    //해당 composable의 lifecycle과 같은 lifecycle을 가집니다.
    val scope = rememberCoroutineScope()

    val configuration = LocalConfiguration.current
    val screenWidth = configuration.screenWidthDp.dp
    val widthPx = with(LocalDensity.current) {
        screenWidth.roundToPx()
    }


    val connection = remember(state, lazyListState) {
        PagerSnapNestedScrollConnection(state, lazyListState) {

            val firstItemIndex = state.firstVisibleItemIndex.value
            val firstItemOffset = kotlin.math.abs(state.offsetInfo.value)

            val position = when {
                firstItemOffset <= widthPx.div(2) -> firstItemIndex
                else -> firstItemIndex.plus(1)
            }

            scope.launch {
                state.scrollItemToSnapPosition(lazyListState, position)
            }

        }
    }

    val padding = 16.dp
    Box(
        modifier = modifier
            .nestedScroll(connection = connection)
            .fillMaxWidth()
            .padding(horizontal = padding)
            .padding(top = padding, bottom = padding)
    ) {
        LazyRow(
            state = lazyListState,
            horizontalArrangement = Arrangement.spacedBy(padding),
        ) {

            items(imageList.size) { ind ->
                Image(
                    painter = painterResource(imageList[ind]),
                    contentDescription = "OnBoarding Images",
                    modifier = Modifier
                        .fillParentMaxWidth()
                        .fillParentMaxHeight(),
                    contentScale = ContentScale.FillBounds,
                )
            }
        }
        LazyRow(
            Modifier
                .align(Alignment.BottomCenter)
                .padding(bottom = 8.dp)
                .wrapContentWidth(),
            horizontalArrangement = Arrangement.spacedBy(2.dp)
        ) {
            items(imageList.size) { ind ->
                //하나는 투명하게 하나는 진하게
                if (ind == lazyListState.firstVisibleItemIndex) {
                    Text(
                        text = ".",
                        color = Color.White,
                        fontSize = 80.sp,
                        fontWeight = FontWeight.Bold
                    )
                } else {
                    Text(
                        text = ".",
                        modifier = Modifier
                            .alpha(.5f),
                        color = Color.White,
                        fontSize = 80.sp,
                        fontWeight = FontWeight.Light
                    )
                }
            }
        }
    }
}

사실 라이브러리를 쓰는 편이 덜 복잡할지 모르겠습니다,..

 

저도 아직 컴포즈는 초보라 많이 복잡해 보입니다.

 

// 스크롤의 position의 상태를 저장.
val lazyListState = rememberLazyListState()

//페이지의 스크롤 되는중인지, 어떤페이지인지에 대한 정보(PagerSnapState) 즉 페이지 상태를 반환하는 메서드이다.
val state = rememberPagerSnapState()

//해당 composable의 lifecycle과 같은 lifecycle을 가집니다.
val scope = rememberCoroutineScope()

val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp.dp
val widthPx = with(LocalDensity.current) {
    screenWidth.roundToPx()
}

 

우선 스크롤하게 될 페이지의 포지션을 저장할 rememberLazyListState()

 

현재 페이지가 스크롤되는 중인지, 어떤 페이지인지에 대한 정보 등, 페이지의 상태를 반환하는

 

rememberPagerSnapState()

 

라이프사이클을 얻기 위한 rememberCoroutineScope로 스코프를 반환받고,

 

각각의 변수에 담아줍니다.

 

 

rememberPagerSnapState()는 페이지의 상태를 알기 위해 커스텀해서 만든 메서드입니다.

 

아래 코드와 같습니다.

 

@Composable
fun rememberPagerSnapState(): PagerSnapState {
    return remember {
        PagerSnapState()
    }
}

 

PagerSnapState클래스를 반환합니다.

 

class PagerSnapState {

    val isSwiping = mutableStateOf(false)

    val firstVisibleItemIndex = mutableStateOf(0)

    val offsetInfo = mutableStateOf(0)

    internal fun updateScrollToItemPosition(itemPos: LazyListItemInfo?) {
        if (itemPos != null) {
            this.offsetInfo.value = itemPos.offset
            this.firstVisibleItemIndex.value = itemPos.index
        }
    }

    internal suspend fun scrollItemToSnapPosition(listState: LazyListState, position: Int) {
        listState.animateScrollToItem(position)
    }
}

 

isSwiping -> 다른 페이지로 스와이프 하여 완료되었는지 아닌지에 대한 상태가 업데이트되는 mutablestate 변수입니다.

 

firstVisibleItemIndex -> 현재 표시된 페이지 인덱스 값이다.

 

offsetInfo -> 지연의 의한 offset의 값이다. 쉽게 말해 페이징을 할 때 움직일 때 얻는 간격에 대한 값이다.

 

updateScrollToItemPosition() -> 스크롤 후에 호출되는 메서드로써, firstVisibleItemIndex값과 offsetInfo의 값을 경신해준다.

 

scrollItemToSnapPostion() -> 스크롤 시 애니메이션을 적용하기 위한 메서드이다. suspend

 

 

val connection = remember(state, lazyListState) {
    PagerSnapNestedScrollConnection(state, lazyListState) {

        val firstItemIndex = state.firstVisibleItemIndex.value
        val firstItemOffset = kotlin.math.abs(state.offsetInfo.value)
        

        val position = when {
            firstItemOffset <= widthPx.div(2) -> firstItemIndex
            else -> firstItemIndex.plus(1)
        }

        scope.launch {
            state.scrollItemToSnapPosition(lazyListState, position)
        }

    }
}

이 값들을 isSwaping의 값을 경신하며, 스크롤할 때마다 실질적으로 위에 pagerSnapState들의 변수를 갱신하는

 

트리거 역할을 해주는 PagerSnapNestedScrollConnection 클래스의 scrollTo라는 함수로 매개변수를 넘겨준다.

 

//페이지가 스크롤될때 호출되는 리스너를 담은 클래스
class PagerSnapNestedScrollConnection(
    private val state: PagerSnapState,
    private val listState: LazyListState,
    private val scrollTo: () -> Unit
) : NestedScrollConnection {

    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset =
        when (source) {
            NestedScrollSource.Drag -> onScroll()
            else -> Offset.Zero
        }

    override fun onPostScroll(
        consumed: Offset,
        available: Offset,
        source: NestedScrollSource
    ): Offset = when (source) {
        NestedScrollSource.Drag -> onScroll()
        else -> Offset.Zero
    }

    private fun onScroll(): Offset {
        state.isSwiping.value = true
        return Offset.Zero
    }

    override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity = when {
        state.isSwiping.value -> {

            state.updateScrollToItemPosition(listState.layoutInfo.visibleItemsInfo.firstOrNull())

            scrollTo()

            Velocity.Zero
        }
        else -> {
            Velocity.Zero
        }
    }.also {
        state.isSwiping.value = false
    }

}

 

만약 손가락을 떼지 않고 스크롤을 하면 onPreScroll -> onPostScroll 순서로 (손가락을 떼기 전까지) 계속해서

 

호출될 것이다.

 

그러면서 

NestedScrollSource.Drag 

 

드래그 중인 것을 감지하여 onScroll() 메서드를 호출하여,

 

isSwiping의 값은 true값을 유지하게 된다.

 

그래서 손을 떼고 나서(스크롤 종료),

 

onPostFling이 호출되게 된다.

 

현재에 포지션이 갱신되고, 

 

scrollTo 함수가 실행된다.

 

그리고

 

마지막에는 isSwiping 값이 false가 된다.

 

마지막으로,

 

맨 아래에 있는 두 개의 점표 시를 보도록 하자

 

val padding = 16.dp
Box(
    modifier = modifier
        .nestedScroll(connection = connection)
        .fillMaxWidth()
        .padding(horizontal = padding)
        .padding(top = padding, bottom = padding)
) {
    LazyRow(
        state = lazyListState,
        horizontalArrangement = Arrangement.spacedBy(padding),
    ) {

        items(imageList.size) { ind ->
            Image(
                painter = painterResource(imageList[ind]),
                contentDescription = "OnBoarding Images",
                modifier = Modifier
                    .fillParentMaxWidth()
                    .fillParentMaxHeight(),
                contentScale = ContentScale.FillBounds,
            )
        }
    }
    LazyRow(
        Modifier
            .align(Alignment.BottomCenter)
            .padding(bottom = 8.dp)
            .wrapContentWidth(),
        horizontalArrangement = Arrangement.spacedBy(2.dp)
    ) {
        items(imageList.size) { ind ->
            //하나는 투명하게 하나는 진하게
            if (ind == lazyListState.firstVisibleItemIndex) {
                Text(
                    text = ".",
                    color = Color.White,
                    fontSize = 80.sp,
                    fontWeight = FontWeight.Bold
                )
            } else {
                Text(
                    text = ".",
                    modifier = Modifier
                        .alpha(.5f),
                    color = Color.White,
                    fontSize = 80.sp,
                    fontWeight = FontWeight.Light
                )
            }
        }
    }
}

 

스크롤에 적합한 LazyRow를 사용하였고,

 

현재의 포지션 값을 저장하고 있는 lazyListState값을 넘겨줌으로써,

 

현재 포지션을 인식한다.

 

이미지에 대한 LazyRow와 

 

아래 현재 위치를 표시하는 점에 대한 LazyRow,

 

두 개에 대한 코드이다.

 

 

지금까지 Compose로 온보딩 화면을 만드는 법을 알아보았다..

 

설명이 난해하지만, 컴포즈를 더 공부하여 세세히 정리해보겠다. 

 

(깃헙에 전체코드가 있습니다.)

 

https://github.com/qjsqjsaos/ComposeUses

 

GitHub - qjsqjsaos/ComposeUses: Compose 공부자료

Compose 공부자료. Contribute to qjsqjsaos/ComposeUses development by creating an account on GitHub.

github.com

728x90
반응형

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading