안드로이드

[Kotlin/Android] ViewPager2 (1) Adapter(RecyclerView.Adapter, FragmentStateAdapter)

란서 2022. 2. 4. 12:32

Swipe View

Swipe View는 손가락의 가로 동작이나 탭, Swipe를 이용하여 동위 화면 간에 탐색(가로 페이징 Horizontal Paging)을 할 수 있는 뷰를 의미한다.

 

Swipe View를 생성하기 위해서 ViewPager2를 사용한다.


ViewPager2

 

  • Gradle dependency

        //viewPagar2
        implementation "androidx.viewpager2:viewpager2:1.0.0"​
  • <ViewPager2> Layout 요소 추가.

    SwipeView가 들어갈 UI xml 파일에 ViewPage2 layout을 추가한다.

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".swipeview.SwipeViewFragment">
    
            <androidx.viewpager2.widget.ViewPager2
                android:id="@+id/viewPager"
                bindData="@{viewModel.tmpList}"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                />
        </FrameLayout>​
     

 

ViewPager2.Adatper

 

다음은 ViewPager2 의 adapter를 설정하기 위한 2가지 방법이다.

 

  • RecyclerView.Adapter
  • FragmentStateAdapter

기존 RecyclerView의 Adapter를 설정해줄 때와 같이 RecyclerView.Adapter를 상속한 클래스를 설정해줄 수 있다. 또는 프래그먼트를 Swipe View의 구성요소로 넣고 싶다면 FragmentStateAdapter를 상속한 클래스를 설정해줄 수 있다.

그런데 FragmentStateAdapter도 RecyclerView.Adapter의 하위 클래스기 때문에 ListAdatper 역시 adapter로 사용가능하다. 

따라서 ListAdapter를 활용한 ViewPager2, 그리고 FragmentStateAdapter를 활용한 ViewPager2 두 가지 케이스를 정리해보겠다.


ViewPager2.adapter = ListAdapter

 

우선 ListAdapter를 상속하는 클래스를 정의한다. 이 방법은 블로그에도 포스팅 되어있다.

https://ranseo.tistory.com/246

 

[Kotlin/Android] RecyclerView (2) DiffUtil & ListAdapter

DiffUtil & ListAdapter DiffUtil 탄생(?)배경  이전에 리사이클러뷰가 보여주는 data 목록에 (추가, 삭제, 변경) 이 발생하면 notifyDataSetChanged() 함수를 사용하여 리사이클러뷰에 알리도록 했다. val data..

ranseo.tistory.com

 

 

 

 

  • class ViewPagerAdapter : ListAdapter
class ViewPagerAdapter : ListAdapter<Tmp, ViewPagerAdapter.ViewHolder>(tmpDiffCallback()){

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder.from(parent)
    }


    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val tmp = getItem(position)
        holder.bind(tmp)
    }



    class ViewHolder(val binding: TmpListItemBinding) : RecyclerView.ViewHolder(binding.root){
        fun bind(
            tmp: Tmp?
        ) {
            binding.item = tmp
        }

        companion object{
            fun from(parent: ViewGroup): ViewHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val binding = TmpListItemBinding.inflate(layoutInflater, parent, false)
                return ViewHolder(binding)
            }

        }

    }

}


class tmpDiffCallback() : DiffUtil.ItemCallback<Tmp>() {
    override fun areItemsTheSame(oldItem: Tmp, newItem: Tmp): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: Tmp, newItem: Tmp): Boolean {
        return oldItem == newItem
    }

}

 

  • ViewHolder와 연동할 List_Item_View
<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>
        <variable
            name="item"
            type="com.example.viewpager2recyclerviewadapter.data.Tmp" />

    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/text"
            android:text="@{item.text}"
            android:textSize="32sp"
            android:layout_gravity="center"
            android:textAlignment="center"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            tools:text="안녕하세요"/>

        <TextView
            android:layout_width="24dp"
            android:layout_height="52dp"
            android:layout_gravity="center|right"
            android:background="@color/black"
            />

        <TextView
            android:layout_width="24dp"
            android:layout_height="52dp"
            android:layout_gravity="center|left"
            android:background="@color/black"
            />

    </FrameLayout>
</layout>

※주의 : SwipeView에서 쓰이는 ListItemView의 width, height는 반드시 화면 전체를 채워야 한다. 따라서 match_parent로 정의 되어야 한다. 그렇지 않을 경우 오류.

  • In SwipeViewFragment
        val adapter = ViewPagerAdapter()

        binding.viewPager.adapter = adapter

 

 

 

ViewPager2.adapter = FragmentStateAdapter

 

SwipeView에 단순 view가 아닌 Fragment 자체를 활용하기 위해 사용되는 Adapter.

 

FragmentStateAdapter는 현재 adapter가 있는 프래그먼트를 사용해 초기화할 수 있다. 따라서 해당 클래스를 상속하는 하위 클래스는 생성 시 Fragment 매개변수를 부모 클래스에 넘겨주어야 한다.

 

  • SwipeView에 페이징 되는 뷰로 활용될 Fragment 생성 - TmpObjFragment

    class TmpObjFragment() : Fragment() {
    
        private lateinit var binding: FragmentTmpObjBinding
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            binding = DataBindingUtil.inflate<FragmentTmpObjBinding>(inflater, R.layout.fragment_tmp_obj, container, false)
            return binding.root
        }
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            arguments?.takeIf { it.containsKey(KEY)}?.apply {
                binding.item = getParcelable(KEY)
            }
        }
    
        companion object {
            val KEY = "object"
        }
    }​

    - arguments?.takeIf{it.containsKey()} 를 이용하여 프래그먼트가 활동주기가 초기화 될 때  (생성되기 직전) 전해 받은 인자가 있다면 이를 사용.

    -TmpObjFragment 의 layout 
    <layout 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">
    
        <data>
            <variable
                name="item"
                type="com.example.viewpager2fragmentstateadapter.data.Tmp" />
    
        </data>
    
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".swipeview.TmpObjFragment">
    
            <TextView
                android:id="@+id/text"
                android:text="@{item.text}"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:textAlignment="center"
                android:textSize="32sp"
                tools:text="하이요" />
    
        </FrameLayout>
    </layout>​


  • FragmentStateAdapter 생성 - TmpViewPagerAdapter

    class TmpViewPagerAdapter(fr: Fragment) : FragmentStateAdapter(fr) {
        var data : List<Tmp> = listOf()
            set(value) {
                field = value
                notifyDataSetChanged()
            }
    
        override fun getItemCount(): Int = data.size
    
        override fun createFragment(position: Int): Fragment {
            val fragment = TmpObjFragment()
            fragment.arguments = Bundle().apply {
                putParcelable(KEY, data[position])
            }
            return fragment
    
        }
    
        companion object {
    
            const val KEY = "object"
        }
    }​

    - FragmentStateAdapter(fr) 상속 = fr :Fragment를 매개변수로 넘겨주어 부모 클래스 초기화

    - getItemCount() = view의 개수. ->data의 개수로 설정해주면 된다.

    - createFragment() = position에 따라 fragment를 생성해서 리턴하는 함수. 해당 함수 내에서 Fragment를 생성하고 argument를 설정하여 생성된 프래그먼트 내에서 해당 arg를 사용할 수 있다.

  • In SwipeViewFragment

            val adapter = TmpViewPagerAdapter(this)
            binding.viewPager.adapter = adapter​



swipe 되는 모습.