관리 메뉴

나만을 위한 블로그

[Android] 코틀린과 ViewPager2로 미리보기 + 자동 무한 스크롤 기능 구현하는 법 본문

Android

[Android] 코틀린과 ViewPager2로 미리보기 + 자동 무한 스크롤 기능 구현하는 법

참깨빵위에참깨빵 2022. 6. 8. 23:45
728x90
반응형

뷰페이저도 만들고 나면 까먹기 딱 좋은 소재라서 구현한 김에 적어둔다.

 

먼저 이미지 슬라이더를 만들 거니까 필요한 이미지를 준비한다. 대충 무료 이미지 검색해서 찾은 아래 이미지를 써도 되지만 네 귀퉁이가 둥근 이미지가 좀 더 보기 이쁘다. 아래 이미지는 구현을 위해 준비한 것이니 각자 사정에 맞게 적당한 이미지를 준비하자.

 

 

그리고 res 안의 values 폴더를 우클릭해서 dimens.xml 파일을 만들고 거기에 dp값 2개를 써둔다.

이 값들은 나중에 액티비티 코틀린 파일 안에서 사용할 것이다.

 

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="pageMargin">100dp</dimen>
    <dimen name="pageWidth">300dp</dimen>
</resources>

 

그리고 XML을 만든다.

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".viewpager.ViewPagerActivity">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/image_viewpager"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_marginStart="20dp"
        android:layout_marginEnd="20dp"
        android:layout_marginTop="26dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

 

뷰페이저를 만들었으면 안에서 보여줄 아이템의 XML도 만들어줘야 한다.

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/slide_imageview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

 

아이템의 XML을 만들었다면 뷰페이저2에 붙일 어댑터를 만든다.

 

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.example.kotlinprac.R

class ViewPagerAdapter(
    private val sliderItems: MutableList<Int>,
    private val viewPager2: ViewPager2
): RecyclerView.Adapter<ViewPagerAdapter.ViewPagerViewHolder>() {

    inner class ViewPagerViewHolder(view: View): RecyclerView.ViewHolder(view) {
        private val imageView = view.findViewById<ImageView>(R.id.slide_imageview)

        fun onBind(image: Int) {
            imageView.setImageResource(image)
        }
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): ViewPagerAdapter.ViewPagerViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.slide_item_container, parent, false)
        return ViewPagerViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewPagerAdapter.ViewPagerViewHolder, position: Int) {
        holder.onBind(sliderItems[position])
        if (position == sliderItems.size - 1) {
            viewPager2.post(runnable)
        }
    }

    override fun getItemCount(): Int = Int.MAX_VALUE

    private val runnable = Runnable { sliderItems.addAll(sliderItems) }
}

 

getItemCount()를 보면 Int의 최대값(약 21억)을 리턴하게 해놨는데, 이렇게 하면 리스트 안의 이미지를 모두 표시하는 걸 한 사이클이라고 할 경우 총 21억 사이클만큼 사용자에게 반복해서 보여줄 수 있다. 무한 스크롤 뷰페이저라고 하지만 당연히 사실 무한은 아니고 처음부터 끝까지 엄청나게 뺑뺑이 돌려서 보여주는 것이다.

그리고 onBindViewHolder() 안에선 뷰페이저 내 아이템 위치가 마지막에 다다랐을 경우 Runnable 프로퍼티를 실행하는데, Runnable 프로퍼티는 리스트에 다시 어댑터 생성자로 받은 이미지가 담긴 리스트를 담는다. 즉 같은 이미지를 다시 리필한다고 생각하면 된다.

그 외에는 딱히 볼 것 없는 평범한 뷰페이저2 어댑터 코드다.

 

이제 마지막으로 액티비티 파일이다. 아직 소스코드 분석이 완벽하게 되지 않아서 설명이 많이 미흡하니 참고하자.

 

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.example.kotlinprac.R
import com.example.kotlinprac.databinding.ActivityViewPagerBinding

class ViewPagerActivity : AppCompatActivity() {

    private lateinit var binding: ActivityViewPagerBinding

    private val sliderImageHandler: Handler = Handler()
    private val sliderImageRunnable = Runnable { binding.imageViewpager.currentItem = binding.imageViewpager.currentItem + 1 }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityViewPagerBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val imageList = arrayListOf<Int>().apply {
            for (i in 0..2) {
                add(R.drawable.image)
            }
        }

        val pageMarginPx = resources.getDimensionPixelOffset(R.dimen.pageMargin)
        val pagerWidth = resources.getDimensionPixelOffset(R.dimen.pageWidth)
        val screenWidth = resources.displayMetrics.widthPixels
        val offsetPx = screenWidth - pageMarginPx - pagerWidth

        binding.imageViewpager.apply {
            adapter = ViewPagerAdapter(imageList, binding.imageViewpager)
            offscreenPageLimit = 1
            getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
            registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
                override fun onPageSelected(position: Int) {
                    super.onPageSelected(position)
                    sliderImageHandler.removeCallbacks(sliderImageRunnable)
                    sliderImageHandler.postDelayed(sliderImageRunnable, 1000)
                }
            })
            setPageTransformer { page, position ->
                page.translationX = position * -offsetPx
            }
        }
    }

    override fun onResume() {
        super.onResume()
        sliderImageHandler.postDelayed(sliderImageRunnable, 1000)
    }

    override fun onPause() {
        super.onPause()
        sliderImageHandler.removeCallbacks(sliderImageRunnable)
    }

}

 

데이터바인딩을 쓰긴 했지만 굳이 안 써도 된다. 난 쓰는 게 편해서 썼다.

하나씩 보자. 먼저 전역 private 프로퍼티로 핸들러와 Runnable을 만들었다. Runnable은 뷰페이저에 표시되는 이미지를 다음 이미지로 보여주는 처리를 수행한다.

 

imageList에 맨 위에 걸어놓은 이미지를 2번 넣는데, 한 번만 넣을 경우 어댑터에서 아래와 같은 에러가 발생한다.

 

java.lang.IndexOutOfBoundsException: Index: 1, Size: 1

 

확실하진 않지만 이미지를 하나만 넣을 경우 옆에 표시되는 미리보기 이미지를 만들지 못해서 이런 에러가 나는 게 아닌가 싶다.

그 아래의 pageMarginPx 프로퍼티부터 offsetPx 프로퍼티까지는 핸드폰의 가로 길이에서 뷰페이저 길이와 페이지 마진값을 뺀 만큼 이미지를 얼마나 이동시킬건지 계산하는 부분이다. 이 로직이 없으면 어떻게 되는지 이 부분을 주석치고 확인해보자.

그 다음 apply 범위 지정 함수를 써서 뷰페이저에 적용할 각종 처리들을 모두 담아놨다. 아래에 간단하게 내가 이해한 내용을 써놓긴 했는데 이것 역시 설명이 미흡하니 공식문서의 설명 원문을 보고 이해하는 게 좋다.

 

  • 어댑터를 붙인다. 이 때 생성자로 이미지들이 담긴 리스트와 뷰페이저의 id값을 넘긴다
  • offscreenPageLimit : 현재 보이는 페이지의 양 옆에 유지해야 하는 페이지 수를 의미한다. 미리보기 기능은 처음 표시되는 이미지의 경우 오른쪽 1장만, 그 다음 이미지부턴 양 옆에 하나씩 표시돼야 하니 1을 넣으면 된다
  • getChildAt(0).overScrollMode : 이 코드가 없으면 첫 번째 이미지를 왼쪽으로 쭉 슬라이드할 경우 물결 모양이 생기는데 이걸 없애는 코드다. 있어도 상관없다면 이 코드는 없애도 된다
  • registerOnPageChangeCallback : 이 함수의 설명 원문을 보면 페이지가 바뀌거나 스크롤될 때 호출되는 콜백이라고 한다. 즉 지금 보는 이미지에서 다음 이미지로 바뀔 때 호출된다는 건데, 잠깐 멈춰서 일정 시간 동안 이미지를 보여주고 다음 이미지로 넘기는 처리를 구현하려면 이 함수를 구현하고 그 안에서 onPageSelected 함수를 재정의해야 한다. 실제로 onPageSelected() 내부에는 이런 처리가 구현돼 있다
  • setPageTransformer : 이 함수의 설명 원문을 보면 스크롤 위치가 바뀔 때마다 호출되는 함수로, 스크롤 동작을 재정의해서 각 페이지에 커스텀 속성 변환을 적용할 수 있다고 한다. 가로로 일정 거리만큼 이동하는 것이니 translationX 값을 조작하면 된다
  • 다른 페이지로 이동하면 onPause()가 호출되는데 이 때도 자동 무한 스크롤이 작동할 이유는 없기 때문에 스크롤을 중단하고, 다시 돌아왔을 경우 onResume()이 호출되니 이 때 중지된 지점부터 다시 자동 무한 스크롤을 작동시킨다

 

여차여차 해서 앱을 빌드하면 아래처럼 나온다.

 

 

3장 정도 넘어갔을 때 홈 버튼을 눌러 바탕화면으로 나간 다음 실행한 앱 목록을 열어 다시 열면 3장째부터 다시 시작되는 걸 볼 수 있다. 가끔 사진이 빠르게 넘어가는 부분은 내가 마우스로 직접 넘겨서 그런 것이다. 직접 넘기길 멈추고 가만 있으면 정상적으로 다시 움직이는 걸 볼 수 있다.

 

참고한 사이트)

 

https://todaycode.tistory.com/26

 

코틀린 viewPager2 : 사용법, 애니메이션 등

1. viewPager2  1-1. viewPager란?  1-2. viewPager의 활용 2. 사용 방법  2-1. 기본 사용법  2-2. 애니메이션 설정  2-3. 여백 설정 1. viewPager2 1-1. viewPager란? 페이지를 넘기듯이 이렇게 슉-슉- 넘..

todaycode.tistory.com

 

반응형
Comments