N가지 방향의 스크롤뷰에서 영상 재생하기

벼리·2025년 6월 8일
2

들어가며

HorizontalPager, Recyclerview, 디바이스의 길이를 넘어가는 Column 등 스크롤의 형태는 다양합니다. 또한 현업에서는 compose와 xml을 혼용하여 사용하는 경우가 많기 때문에 각 상황에 대해 모두 대처해야 합니다.

그렇다면, 이렇게 다양한 상황 속에서 어떻게 영상 재생 로직을 구현할 수 있을까요?
상황별 문제들을 그룹화하여, 어떻게 영상 재생 로직을 구현했는지에 대한 저의 경험을 공유하고자 합니다.

사전 지식

다음 사전 지식을 알고 있다면 글을 이해하는데 더욱 도움이 됩니다

  • Jetpack Media3 라이브러리
  • Compose
  • Recyclerview

1️⃣ LazyColumn의 경우

LazyColumn의 경우 LazyListState 를 관찰하여 현재 보이는 리스트 아이템의 LazyListItemInfo 를 알 수 있습니다. 해당 정보를 이용해, 리스트에서 어떤 index의 video를 재생할지 결정할 수 있습니다.

예를 들어, 현재 화면에 보이는 아이템의 개수가 최대 3개일 때 다음과 같은 로직을 구현할 수 있습니다.

  • 현재 보이는 아이템의 개수가 1개인 경우 → 첫 번째 index의 video를 재생
  • 현재 보이는 아이템의 개수가 2개인 경우 → height의 70%가 보여지는 아이템 중에서, 첫 번째 요소를 재생
  • 현재 보이는 아이템의 개수가 3개인 경우 → 가운데 video를 재생

위의 로직을 코드로 구현하면 다음과 같습니다.

 val playVideoIndex by remember {
        derivedStateOf {
            val visibleItems = lazyListState.layoutInfo.visibleItemsInfo
            return when (visibleItems.size) {
                1 -> visibleItems.first().index
                2 -> {
                    visibleItems.firstOrNull {
                        it.offset + it.size >= it.size * 0.7f
                    }?.index ?: visibleItems.first().index
                }
                3 -> {
                    visibleItems[1].index
                }
                else -> visibleItems.firstOrNull()?.index ?: 0
            }
        }
    }

playVideoIndex에 해당하는 player를 재생하도록 구현하면, 다음과 같이 스크롤에 따라 자동으로 영상이 재생되도록 구현할 수 있습니다.

2️⃣ Recyclerview의 경우

RecyclerView에서는 ViewHolder가 화면에 표시되거나 사라질 때 호출되는 콜백 메서드를 제공합니다.

  • onViewAttachedToWindow는 ViewHolder가 Window에 붙을 때, 즉 화면에 보이기 시작할 때 호출됩니다.
  • 반대로 onViewDetachedFromWindow는 ViewHolder의 itemView가 Window에서 분리될 때, 즉 화면에서 사라질 때 호출됩니다.

이 두 메서드를 활용하여, 화면에 보이지 않을 때 비디오 재생을 중단하고, 다시 나타날 때 재생을 재개하는 로직을 구현할 수 있습니다.

override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
    super.onViewAttachedToWindow(holder)
    holder.replayPlayer()
}

override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
    super.onViewDetachedFromWindow(holder)
    if (holder is VideoViewHolder) {
        holder.clearPlayer()
    }
}

class VideoViewHolder() : RecyclerView.ViewHolder(binding.root) { 
		fun replayPlayer() {
				// video 재생
		}

		fun pausePlayer() {
				// video 중지
		}
}

3️⃣ 디바이스 화면을 넘어가는 Composable

디바이스 화면을 넘어가는 Composable 내부에 특정 컴포넌트만 Video일 경우, 스크롤을 함에 따라 영상 재생이 중단되어야 합니다.

이 경우, 현재 컴포넌트가 화면에 보이는지 여부를 판단하여 영상 재생 로직을 구현해야 합니다.

우선 이를 확인하기 위해 현재의 displayMatrix를 가져옵니다.

val context = LocalContext.current
val displayMetrics = context.resources.displayMetrics

여기서 widthPixels, heightPixels은 전체 화면 해상도(px)를 나타냅니다.

이후 Video 컴포넌트가 실제 레이아웃에 배치된 이후의 정보가 필요합니다. 이를 위해 onGloballyPositioned 를 사용합니다.

Box(
    modifier = modifier
        .onGloballyPositioned { coordinates ->
						// ..
		}

onGloballyPositioned컴포저블이 실제로 레이아웃에 배치된 후의 위치 정보 (LayoutCoordinates)를 제공해주는 콜백입니다. 레이아웃 배치가 끝난 뒤에 컴포넌트의 위치를 측정해야하기 때문에 onGloballyPositioned 콜백 내부에서 가시성을 확인합니다.

val currentlyVisible = componentRect.overlaps(
    Rect(
        left = 0f,
        top = 0f,
        right = displayMetrics.widthPixels.toFloat(),
        bottom = displayMetrics.heightPixels.toFloat()
    )
)

이후 Video 컴포넌트의 현재 위치와 크기를 기준으로 한 화면 상(Window 기준)의 위치 정보를 가져옵니다. 이렇게 화면 상의 컴포넌트 위치와, 전체 화면의 위치를 겹침 여부를 확인하여 Video 컴포넌트의 가시성을 확인할 수 있습니다. 이제 currentlyVisible 을 이용해 비디오 재생을 제어하여 로직을 구현할 수 있습니다.

전체 코드는 다음과 같습니다.

val context = LocalContext.current
val displayMetrics = context.resources.displayMetrics

Box(
    modifier = modifier
        .onGloballyPositioned { coordinates ->
            val componentRect = coordinates.boundsInWindow()
            val currentlyVisible = componentRect.overlaps(
                Rect(
                    left = 0f,
                    top = 0f,
                    right = displayMetrics.widthPixels.toFloat(),
                    bottom = displayMetrics.heightPixels.toFloat()
                )
            )
            // currentlyVisible을 이용해 비디오 재생 제어
				
       }
)
profile
코딩일기

0개의 댓글