본문 바로가기

안드로이드/[Kotlin]

[Kotlin] ViewPager2 & TabLayout (Feat. FragmentStateAdatper, TabLayoutMediator)

 

 

음.. 위의 화면에서 Banner부분의 이미지는 Swipe 방식으로 좌우로 움직이면서 이미지가 바뀐다. 저런건 어떻게 구현하지,,?  AndroidX에서 제공 해 주는 ViewPager2 라는 아주 유용한 위젯이 있다.

 

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

 

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

ViewPager2를 사용하여 탭으로 스와이프 뷰 만들기 스와이프 뷰를 사용하면 손가락의 가로 동작이나 스와이프로 탭과 같은 동위 화면 간을 탐색할 수 있습니다. 이러한 탐색 패턴을 가로 페이징이

developer.android.com

 

 

 

전체적인 흐름은 아래와 같다.

  1. 메인 레이아웃에 ViewPager2 추가 (fragment_home.xml)
  2. ViewPager2 에 하위 뷰를 삽입하기 위해 FragmentStateAdapter 에 연결 (HomeBannerVpAdapter.kt)
  3. ViewPager2 의 하위 뷰에 삽입될 Fragment 생성 (HomeBannerFragment.kt , fragment_home_banner.xml)
  4. 메인 Fragment에서 binding을 통한 Adapter 연결 (HomeFragment.kt)

 

 

1. 메인 레이아웃에 ViewPager2 추가

//fragment_home.xml 

<androidx.viewpager2.widget.ViewPager2
            android:id="@+id/home_banner_viewpager"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@id/home_today_music_scroll_view"/>

 

간단하게 xml 코드에 추가하면 된다. 여기서 추가적으로 ViewPager2 의 기본 Swipe 방향은 좌우 이다. 따라서 orientation의 default 값은 horizontal 이다. Swipe 방향을 상하로 하고 싶다면 android:orientation="vertical" 속성을 추가하면 된다.

 

 

 

2. FragmentStateAdapter 에 연결 (Adapter 생성)

ViewPager2 는 화면이 Swipe 되면서 하위 뷰 (Fragment) 들이 보여지는 위젯이다. 각 페이지를 나타내는 하위 뷰를 삽입하려면 이 레이아웃을 FragmentStateAdapter에 연결해야 한다.  직접 Adapter class 를 만들어서  FragmentStateAdapter 를 상속받으면 된다.  여기서 getItemCountcreateFragment 는 필수 메소드이다.

//HomeBannerVpAdapter.kt


package com.example.flo

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

class HomeBannerVpAdapter(fragment : Fragment) :FragmentStateAdapter(fragment) {
    private var fragmentlist : ArrayList<Fragment> = ArrayList()
    override fun getItemCount(): Int = fragmentlist.size

    override fun createFragment(position: Int): Fragment = fragmentlist[position]

    fun addFragment(fragment: Fragment) {
        fragmentlist.add(fragment)
        notifyItemInserted(fragmentlist.size-1)
    }
}

 우리는 이 글 최상단의 gif 파일에서 보다시피 Home 화면의 배너에 사진이 Swipe 되면서 보여지는 형식이다. 이 사진은 갯수가 정해진 것이 아니라 추가적으로 add 될 수 있으므로 fragmentlist 를 직접 만들어서 사진이 들어가는 Fragment 를 담을것이다.

 위에서 우리가 직접 만든 addFragment 메소드는 Banner에 새로운 사진파일이 추가될 때 마다 호출이 되는 메소드이다. fragmentlist.add(fragment) 를 통해 리스트에 add 해 주고, notifyItemInserted 를 통해 해당 Adapter 클래스에 새로운 fragment 가 add 되었다고 꼭 알려줘야 한다.

 

 

 

3. ViewPager2 의 하위 뷰에 삽입될 Fragment 생성

레이아웃 코드는 위와 같다. 그냥 주의해야 할 거는 height를 match_parent 가 아닌 wrap_content 로 하는 거 밖에 없다.

 

 

//HomeBannerFragment.kt

package com.example.flo

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.example.flo.databinding.FragmentHomeBannerBinding

class HomeBannerFragment(val imgres : Int) : Fragment() {
    lateinit var binding : FragmentHomeBannerBinding
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentHomeBannerBinding.inflate(inflater, container, false)
        binding.homeBannerImgIv.setImageResource(imgres)


        return binding.root
    }
}

위의 Fragment는 ViewPager2 가 Swipe 되면서 보여지게 될 하위 뷰이다. 우리가 만들어야 할 Banner는 크기와 위치가 모두 일정하고 그냥 사진 src만 변경해 주면 되므로 Fragment 파일은 하나만 만들어도 무방하다.  따라서 위의 HomeBannerFragment.kt 는 imgres 라는 사진파일 src 를 인자로 받게된다. 사진소스의 형식은 Int 이다.  그 뒤에 그냥 뷰 바인딩을 이용해서 (binding.homeBannerImgIv.setImageResource(imgres) ) 사진파일을 변경 해 주면 된다. 그렇다면 사진은 src는 어디서 쏴(?) 주느냐???  바로바로

.

.

.

.

 

 

4. 메인 Fragment에서 binding을 통한 Adapter 연결

그렇다. 여기서 사진 src를 쏴(?) 준다

 

val bannerAdapter = HomeBannerVpAdapter(this)

bannerAdapter.addFragment(HomeBannerFragment(R.drawable.img_home_viewpager_exp))
bannerAdapter.addFragment(HomeBannerFragment(R.drawable.img_home_viewpager_exp))
bannerAdapter.addFragment(HomeBannerFragment(R.drawable.img_home_viewpager_exp))
bannerAdapter.addFragment(HomeBannerFragment(R.drawable.img_home_viewpager_exp))
bannerAdapter.addFragment(HomeBannerFragment(R.drawable.img_home_viewpager_exp))

binding.homeBannerViewpager.adapter = bannerAdapter

아주 무식해 보이는 코드다.  아까 생성한 HomeBannerVpAdapter 클래스의 인스턴스를 생성 한 뒤에 사진 src 를 보내주면서 Fragment를 add 하는 방식이다. 마지막 줄에서 adapter 연결도 꼭 빠짐없이 하도록 하자.

 

 

 


 

 

 

다음으로는 아래와 같은 화면의 레이아웃을 짜야한다.

 

자세히 보면 전체의 큰 Fragment (AlbumFragment.kt) 에서 아래부분을 보면   [수록곡 , 상세정보 , 영상]  이 swipe 됨과 동시에 바뀌면서 아래부분의 뷰 전체가 바뀌는 것을 볼 수 있다. 사실 처음에 난 처음에 그냥 [수록곡 , 상세정보 , 영상] 에 해당하는 Fragment를 만든 뒤에 버튼을 누를때마다 Fragment가 바뀌는 식으로 코드를 짰다. 그런데 이 부분에도 위의 예시와 마찬가지로 ViewPager2 를 적용할 수 있다.

 

 

 

아래 그림을 보면 알 수 있다시피 Swipe를 통한 화면 전환에 유용한 ViewPager2 와, 가로의 탭을 추가할 수 있는 TabLayout을 활용해서 레이아웃 틀을 구성 해 보았다.

 

 

첫번째 예시에서는 이미지 src만 바뀌는 경우여서 ViewPager2 하위 뷰의 Fragment 를 하나만 생성했다. 하지만 이 경우엔  [수록곡 , 상세정보 , 영상]  세가지 다 다른 화면이다. 따라서 3개의 Fragment를 각각 만들어 줘야 한다. 그리고 첫번째 경우에는 Banner에 사진이 계속해서 add 될 수도 있으므로 하위 뷰의 수가 가변적이였다. 하지만 이 경우는 3개로 고정이다. 이 점을 염두에 두고 코드를 살펴보자.

 

//AlbumVpAdapter.kt

package com.example.flo

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

class AlbumVpAdapter (fragment:Fragment) : FragmentStateAdapter(fragment) {

    override fun getItemCount(): Int = 3

    override fun createFragment(position: Int): Fragment {
        return when(position) {
            1 -> AlbumDetailFragment()
            2 -> AlbumVideoFragment()
            else -> AlbumIncludedFragment()
        }
    }

}

 

필수메소드 getItemCount 와 createFragment 는 여전히 존재한다. createFragment 에서 position 값이 바뀜에 따라 하위 뷰들의 Fragment 가 정해지는 방식이다. 

 

다음은 메인 코드의 Adapter 연결 부분을 보자.

val albumVpAdapter = AlbumVpAdapter(this)

val tabTextList = arrayListOf("수록곡", "상세정보", "영상")
	TabLayoutMediator(binding.albumTabLayout, binding.albumViewPager) { tab, position ->
	tab.text = tabTextList[position]
}.attach()

binding.albumViewPager.adapter = albumVpAdapter

뭔가 첫번째 경우랑 다르다.  새롭게 눈에 띄는놈이 있다. 바로 TabLayoutMediator. TabLayout를 ViewPager2에 연결하고 첨부하는 역할을 한다. TabLayout은 Tab 메뉴들을 담는 큰 틀이고, tabTextList 라는 리스트를 생성해서 각각의 탭에 대한 정보를 넣었다. 이 tabTextList 를 TabLayout에 연결한다. 코드는 그냥 스윽 봐도 직관적으로 이해가 가능하다.

 

 


 

 

 

추가적으로 하나 더 기록 하겠다. 상단이 Swipe 되는건 ViewPager2 를 통해 구현했다. 그런데 아래 TabLayout이 각각 Tab의 text 는 없고, Indicator 가 dot 형식으로 되어있다. 음... 어떻게 만들지??

 

 

https://stackoverflow.com/questions/38459309/how-do-you-create-an-android-view-pager-with-a-dots-indicator

 

How do you create an Android View Pager with a dots indicator?

Probably many of you (as me), have problem with creating ViewPager with bottom dots, like this: How do you create such an Android ViewPager?

stackoverflow.com

모를땐 역시 stackoverflow...!!!!

 

drawable 폴더에 home_top_selected_dot.xml ,home_top_default_dot.xml , home_top_tab_selector.xml 파일을 추가해준다.

 

<!-- home_top_selected_dot.xml  -->

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape
            android:shape="ring"
            android:innerRadius="0dp"
            android:thickness="4dp"
            android:useLevel="false">
            <solid android:color="#3F51B5"/>
        </shape>
    </item>
</layer-list>
<!-- home_top_default_dot.xml  -->

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape
            android:shape="ring"
            android:innerRadius="0dp"
            android:thickness="4dp"
            android:useLevel="false">
            <solid android:color="#C3C3C3"/>
        </shape>
    </item>
</layer-list>
<!-- home_top_tab_selector.xml  -->

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/home_top_selected_dot"
        android:state_selected="true"/>

    <item android:drawable="@drawable/home_top_default_dot"/>
</selector>

 

그리고 xml Layout에서 TabLayout에 아래 속성들을 추가 해 줘야한다.

app:tabBackground="@drawable/tab_selector"
app:tabGravity="center"
app:tabIndicatorHeight="0dp"

 

방법은 간단하다. <selector> 태그와 그 안에 2개의 <item> 태그를 활용해서 selected 되었을때와, default일때의 경우를 나눠서 처리하는 방식(?) 에 익숙해 지고 훈련하자. 느낌있다.