📱 안드로이드 Android ~ Kotlin

[Android/Kotlin] ViewPager2 를 이용한 무한 스크롤(Infinite Scroll)/자동 스크롤(Auto Scroll)

핑크빛연어 2022. 10. 31. 23:26

 

ViewPager2

기존 ViewPager 라이브러리의 개선된 버전으로, ViewPager 를 사용 시 발생하는 일반적인 문제를 해결해줍니다.

Android 공식 문서에서도 ViewPager2 사용을 권장하고 있습니다.

 

 

  🚨 ViewPager2 의 이점  

- 세로 방향 지원 (Orientation 속성을 활용하여 Horizontal Paging 에서 Vertical Paging 도 지원)

- 오른쪽에서 왼쪽 지원 (LayoutDirection 속성을 활용하여 RT(Right To Left) 페이징 지원)

- 수정 가능한 프래그먼트 컬랙션 (notifyDatasetChanged() 를 호출하여 UI 업데이트 지원)

- DiffUtil (RecyclerView 기반으로 빌드되므로 DiffUtil 유틸리티 클래스 액세스 가능. 애니메이션 활용 가능)

 

https://developer.android.com/training/animation/vp2-migration?hl=ko 

 

ViewPager에서 ViewPager2로 이전  |  Android 개발자  |  Android Developers

ViewPager에서 ViewPager2로 이전 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. ViewPager2는 ViewPager 라이브러리의 개선된 버전으로, 향상된 기능을 제공하며 ViewPag

developer.android.com

 

 

🚨 ViewPager2 사용하기

https://developer.android.com/guide/navigation/navigation-swipe-view-2?hl=ko 

 

ViewPager2를 사용하여 탭으로 스와이프 뷰 만들기  |  Android 개발자  |  Android Developers

ViewPager2를 사용하여 탭으로 스와이프 뷰 만들기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 스와이프 뷰를 사용하면 손가락의 가로 동작이나 스와이프

developer.android.com

 

 

💡 작성한 파일 목록 입니다.

     1. build.gradle(:app)

     2. SliderEntity.kt

     3. activity_slider_main.xml

     4. SliderMainActivity.kt

     5. fragment_slider.xml

     6.  SliderRecyclerAdapter.kt
     7. SliderPagerAdapter.kt
     8. SliderPagerFragment.kt

 

 

결과 화면

ViewPager2 를 이용한 가로 방향 페이징세로방향 페이징 결과 화면입니다.

 

 

1. build.gradle(:app)

dependencies 안에 ViewPager2 에 대한 의존성을 추가해줍니다.

implementation 'androidx.viewpager2:viewpager2:1.0.0'  //viewPager2 추가

...

dependencies {
    ...
    //viewPager2
    implementation 'androidx.viewpager2:viewpager2:1.0.0'
    ...
}

 

 

2. SliderEntity.kt

ViewPager 에 표시할 리스트의 data model 클래스입니다.

package com.eun.myappkotlin02.function

data class SliderEntity (
    var imgSrc: Int?,
    var imgName: String?,
)

 

 

3. activity_slider_main.xml

SliderMainActivity.kt 에 대한 레이아웃 리소스입니다.

가로방향 슬라이딩과 세로방향 슬라이딩을 모두 표시하도록 구현하였습니다.

 

아이템 리스트가 있는 경우 ViewPager2 인 view_pager_horizontal, view_pager_vertical 를 표시하도록,

리스트가 없는 경우 tv_empty_horizontal, view_pager_vertical 를 표시하도록 하였습니다.

<?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" >

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:layout_marginBottom="10dp"
        android:text="ViewPager Activity"
        android:textSize="25sp"
        android:textColor="@color/black"
        android:textStyle="bold"
        android:gravity="center"
        android:background="@color/amber_200"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@id/tv_ori_horizontal"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/tv_ori_horizontal"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:layout_marginHorizontal="10dp"
        android:text="방향 : 가로"
        android:textSize="20sp"
        android:textStyle="bold"
        android:gravity="center"
        android:background="@drawable/bg_custom_button_purple"
        app:layout_constraintTop_toBottomOf="@id/tv_title"
        app:layout_constraintBottom_toTopOf="@id/view_pager_horizontal"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/view_pager_horizontal"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginBottom="10dp"
        android:layout_marginHorizontal="15dp"
        android:background="@color/blue_200"
        android:orientation="horizontal"
        app:layout_constraintTop_toBottomOf="@id/tv_ori_horizontal"
        app:layout_constraintBottom_toTopOf="@id/tv_ori_vertical"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/tv_empty_horizontal"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_marginBottom="10dp"
        android:layout_marginHorizontal="15dp"
        android:gravity="center"
        android:background="@color/blue_200"
        android:text="가로방향 ViewPager2 리스트가 존재하지 않습니다."
        app:layout_constraintTop_toBottomOf="@id/tv_ori_horizontal"
        app:layout_constraintBottom_toTopOf="@id/tv_ori_vertical"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/tv_ori_vertical"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:layout_marginHorizontal="10dp"
        android:text="방향 : 세로"
        android:textSize="20sp"
        android:textStyle="bold"
        android:gravity="center"
        android:background="@drawable/bg_custom_button_purple"
        app:layout_constraintTop_toBottomOf="@id/view_pager_horizontal"
        app:layout_constraintBottom_toTopOf="@id/view_pager_vertical"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/view_pager_vertical"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginBottom="10dp"
        android:layout_marginHorizontal="15dp"
        android:background="@color/green_200"
        android:orientation="vertical"
        app:layout_constraintTop_toBottomOf="@id/tv_ori_vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/tv_empty_vertical"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_marginBottom="10dp"
        android:layout_marginHorizontal="15dp"
        android:gravity="center"
        android:background="@color/green_200"
        android:text="세로방향 ViewPager2 리스트가 존재하지 않습니다."
        app:layout_constraintTop_toBottomOf="@id/tv_ori_vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

 </androidx.constraintlayout.widget.ConstraintLayout>

 

 

4. SliderMainActivity.kt

가로방향으로 슬라이드되는 형태는 RecyclerView.Adapter 를 사용한 ViewPager2 로 구현(initViewForHorizontal) 하였고,

세로방향으로 슬라이드되는 형태는 FragmentStateAdapter 를 사용한 ViewPager2 로 구현(initViewForVertical) 하였습니다.

 

자동 슬라이딩을 위해 handler 와 runnable 을 사용하였습니다.

package com.eun.myappkotlin02.function

import android.os.Bundle
import android.os.Handler
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.viewpager2.widget.ViewPager2
import com.eun.myappkotlin02.R
import com.eun.myappkotlin02.databinding.ActivitySliderMainBinding


class SliderMainActivity : AppCompatActivity() {

    lateinit var binding: ActivitySliderMainBinding
    private lateinit var mSliderPagerAdapter: SliderPagerAdapter
    private lateinit var mSliderRecyclerAdapter: SliderRecyclerAdapter

    private var sliderList: MutableList<SliderEntity> = mutableListOf()

    /* viewPagerHorizontal 에 사용돠는 handler, runnable */
    private val hHandler = Handler()
    private val hRunnable =
        Runnable {
            binding.viewPagerHorizontal.currentItem = binding.viewPagerHorizontal.currentItem + 1
        }

    /* viewPagerVertical 에 사용돠는 handler, runnable */
    private val vHandler = Handler()
    private val vRunnable =
        Runnable {
            binding.viewPagerVertical.currentItem = binding.viewPagerVertical.currentItem + 1
        }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivitySliderMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        initView()
    }


    private fun initView() {
        // 리스트 데이터 세팅
        sliderList.add(0, SliderEntity(R.drawable.img_blue, "img_blue"))
        sliderList.add(1, SliderEntity(R.drawable.img_mint, "img_mint"))
        sliderList.add(2, SliderEntity(R.drawable.img_mix, "img_mix"))
        sliderList.add(3, SliderEntity(R.drawable.img_pink, "img_pink"))
        sliderList.add(4, SliderEntity(R.drawable.img_purple, "img_purple"))
        sliderList.add(5, SliderEntity(R.drawable.img_skyblue, "img_skyblue"))
        sliderList.add(6, SliderEntity(R.drawable.img_white, "img_white"))
        sliderList.add(7, SliderEntity(R.drawable.img_yellow, "img_yellow"))

        initViewForHorizontal()
        initViewForVertical()
    }


    /**
     * initViewForHorizontal()
     * RecyclerView.Adapter 를 사용한 ViewPager2 구현
     */
    private fun initViewForHorizontal() {
        if(sliderList.size > 0) {
            binding.viewPagerHorizontal.isVisible = true
            binding.tvEmptyHorizontal.isGone = true

            mSliderRecyclerAdapter = SliderRecyclerAdapter()

            binding.viewPagerHorizontal.adapter = mSliderRecyclerAdapter
            binding.viewPagerHorizontal.orientation = ViewPager2.ORIENTATION_HORIZONTAL
            mSliderRecyclerAdapter.setSliderList(this, binding.viewPagerHorizontal, sliderList)

            binding.viewPagerHorizontal.registerOnPageChangeCallback(object :
                ViewPager2.OnPageChangeCallback() {
                override fun onPageSelected(position: Int) {
                    super.onPageSelected(position)
                    hHandler.removeCallbacks(hRunnable)
                    hHandler.postDelayed(hRunnable, 2000) // Slide duration 2 seconds
                }
            })

        } else {  // sliderList 가 없는 경우 viewPager2 hidden 처리
            binding.viewPagerHorizontal.isInvisible = true
            binding.tvEmptyHorizontal.isVisible = true
        }
    }


    /**
     * initViewForVertical()
     * FragmentStateAdapter 를 사용한 ViewPager2 구현
     */
    private fun initViewForVertical() {
        if(sliderList.size > 0) {
            binding.viewPagerVertical.isVisible = true
            binding.tvEmptyVertical.isGone = true

            mSliderPagerAdapter = SliderPagerAdapter(this)

            binding.viewPagerVertical.adapter = mSliderPagerAdapter
            binding.viewPagerVertical.orientation = ViewPager2.ORIENTATION_VERTICAL
            mSliderPagerAdapter.setSliderList(sliderList)

            binding.viewPagerVertical.registerOnPageChangeCallback(object :
                ViewPager2.OnPageChangeCallback() {
                override fun onPageSelected(position: Int) {
                    super.onPageSelected(position)
                    vHandler.removeCallbacks(vRunnable)
                    vHandler.postDelayed(vRunnable, 2000) // Slide duration 2 seconds
                }
            })

        } else {  // sliderList 가 없는 경우 viewPager2 hidden 처리
            binding.viewPagerVertical.isInvisible = true
            binding.tvEmptyVertical.isVisible = true
        }
    }


    override fun onResume() {
        super.onResume()

        hHandler.postDelayed(hRunnable, 2000)  // viewPagerHorizontal 2초마다 Slide
        vHandler.postDelayed(vRunnable, 2000)  // viewPagerVertical 2초마다 Slide
    }


    override fun onPause() {
        super.onPause()

        hHandler.removeCallbacks(hRunnable)  // viewPagerHorizontal 2초마다 Slide
        vHandler.removeCallbacks(vRunnable)  // viewPagerVertical 2초마다 Slide
    }


}

 

 

5. fragment_slider.xml

SliderRecyclerAdapter.kt 와 SliderPagerFragment.kt 에 대한 레이아웃 리소스입니다.

<?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"
    android:padding="10dp"
    android:background="@android:color/transparent" >

    <ImageView
        android:id="@+id/item_img"
        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:padding="10dp"
        android:gravity="center"
        tools:src="@drawable/ic_launcher_bear"
        android:background="@color/orange_200"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@id/item_name"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintVertical_chainStyle="packed"/>

    <TextView
        android:id="@+id/item_name"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:text="1. yellow"
        android:textSize="15sp"
        android:gravity="center"
        android:background="@color/pink_200"
        app:layout_constraintTop_toBottomOf="@id/item_img"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

6. SliderRecyclerAdapter.kt

가로방향으로 슬라이드되는 형태는 RecyclerView.Adapter 를 사용한 ViewPager2 로 구현하였습니다.

package com.eun.myappkotlin02.function

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.bumptech.glide.Glide
import com.eun.myappkotlin02.R
import com.eun.myappkotlin02.databinding.FragmentSliderBinding


class SliderRecyclerAdapter : RecyclerView.Adapter<SliderRecyclerAdapter.ViewHolder>() {

    companion object {
        const val TAG = "ViewPagerAdapter"
    }

    private lateinit var context: Context
    private lateinit var mViewPager2: ViewPager2
    var pagerItems: MutableList<SliderEntity> = mutableListOf()


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        context = parent.context
        return ViewHolder(FragmentSliderBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }


    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        if(pagerItems.size < 0) {
            // SliderMainActivity 에서 hidden 처리되어 여기까지 안넘어옴
        } else {
            holder.bind(pagerItems[position], position)
            if(pagerItems.size > 1) {  // size 가 1보다 큰 경우(2 이상) 무한 슬라이딩
                if(position == (pagerItems.size-1)) {  // 무한 슬라이딩되도록 함
                    mViewPager2.post(runnable)
                }
            }
        }
    }


    override fun getItemCount(): Int = pagerItems.size


    inner class ViewHolder(private val viewBinding: FragmentSliderBinding): RecyclerView.ViewHolder(viewBinding.root) {
        fun bind(item: SliderEntity, position: Int) {
            Glide.with(context).load(item.imgSrc)  // 이미지
                .placeholder(R.drawable.ic_launcher_bear)
                .error(R.drawable.ic_launcher_bear)
                .into(viewBinding.itemImg)
            viewBinding.itemName.text = "${position+1}. ${item.imgName}"
        }
    }


    fun setSliderList(context: Context, viewPager2: ViewPager2, pagerItems: MutableList<SliderEntity>) {
        this.context = context
        this.mViewPager2 = viewPager2
        this.pagerItems = pagerItems
        notifyDataSetChanged()
    }

    
    private val runnable = Runnable {
        pagerItems.addAll(pagerItems)
        notifyDataSetChanged()
    }

}

 

 

7. SliderPagerAdapter.kt

세로방향으로 슬라이드되는 형태는 FragmentStateAdapter 를 사용한 ViewPager2 로 구현하였습니다.

package com.eun.myappkotlin02.function

import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter


class SliderPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {

    private lateinit var dataList: MutableList<SliderEntity>
    private var dataSize: Int = 0


    override fun getItemCount(): Int = dataSize


    override fun createFragment(position: Int): Fragment {
        return SliderPagerFragment().setFragment(position, dataList)
    }


    fun setSliderList(paramList: MutableList<SliderEntity>) {
        this.dataList = paramList
        this.dataSize = paramList.size
    }

}

 

 

8. SliderPagerFragment.kt

SliderPagerAdapter 의 Fragment 입니다.

package com.eun.myappkotlin02.function

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.bumptech.glide.Glide
import com.eun.myappkotlin02.R
import com.eun.myappkotlin02.databinding.FragmentSliderBinding


class SliderPagerFragment: Fragment() {

    lateinit var binding: FragmentSliderBinding

    private var position: Int = 0
    private var dataList : MutableList<SliderEntity> = mutableListOf()


    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        binding = FragmentSliderBinding.inflate(inflater, container, false)
        return binding?.root
//        return inflater.inflate(R.layout.item_viewpager, container, false)
    }


    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        Glide.with(this).load(dataList[position].imgSrc)  // 이미지
            .placeholder(R.drawable.ic_launcher_bear)
            .error(R.drawable.ic_launcher_bear)
            .into(binding.itemImg)

        binding.itemName.text = "${position+1}. ${dataList[position].imgName}"
    }


    fun setFragment(paramPosition: Int, paramList: MutableList<SliderEntity>) : Fragment {
        val fragment = SliderPagerFragment()

        fragment.position = paramPosition
        fragment.dataList = paramList
        return fragment
    }


    override fun onDestroyView() {
        super.onDestroyView()
    }

}

 

 

 

감사합니다 🙆🏻‍♀️

 

728x90
반응형