본문 바로가기

TIL

[TIL] Kotlin ViewPager2 [ 1 ] Fragment 전환 하기, 선택된 탭 아이콘 색상이 바뀌지 않는 오류

[ 1 ] View Pager2 개념

  • ViewPager2 는 RecyclerView에 기반하여 만들어진 것이다. ViewPager2도 ViewHolder 패턴을 사용하여 화면에 보이지 않는 View를 재사용하고, 데이터만 업데이트하여 효율적인 메모리 사용과 스무스한 스크롤이 가능하게 한다. 또한 Adapter 인터페이스를 사용하여 Adapter 인터페이스에서 데이터와 View 생성을 관리하도록 한다. 이를 통해 데이터 세트의 크기, View 생성 및 Binding, ViewHolder 생성등을 처리할 수 있다.
  • RecyclerView의 주 역할은 View를 만드는 것이다. 이를 기반으로 한 ViewPager2 역시도 스와이프가 가능한 View를 만드는 데 사용되지만 Viewpager2는 FragmentStateAdapter를 통해 Fragment 간의 전환에도 주로 사용된다.
  • RecyclerView와의 차이점을 간단하게 알아보며 ViewPager2를 굳이 왜 사용하는지에 대해서 알아보자

 

RecyclerView vs ViewPager2

ViewPager2를 사용하는 이유

  • 먼저 ViewPager2는 Page단위로 데이터를 표시하고 관리한다 각 페이지는 Fragment나 View로 구성될 수 있으며, 사용자가 스와이프 하여 다음 페이지로 이동할 수 있는데,  배너와 같이 한번 스크롤이 되면 다음 페이지로만 이루어져 있는 View를 만들 때 RecyclerView는 별도로 다음 View의 중간에 오도록 snapping을 설정해줘야하는 반면 ViewPager2는 자동으로 부드러운 화면전환 효과와 스냅핑을 제공한다.
    • RecyclerView와는 다르게 각 스크롤 View 사이에서 멈추거나 하는 NoSnapping Scroll이 불가능하다.어떻게 보면 자동으로 페이지의 중앙을 잡아주는 기능이 단점이 될 수 있다. 필요 요건에 맞게 사용하도록 하자
  • 또한 ViewPager2는 State 저장 및 복원 기능을 제공한다. 이를 통해 앱의 화면 전환과 같은 구성변경 상황에서도 마지막으로 보던 페이지로 돌아갈 수 있다.
  • 이처럼 RecyclerView에서 별도로 코드를 지정해주어서 해야하는 작업들을 기본적으로 제공하고, FragmentPagerAdapter와 FragmentStatePagerAdapter 등을 이용하며 데이터 세트를 페이지 단위로 표시하고 관리하기 때문에 간결한 코드 작성이 가능하다.
  • Fragment 전환 시 TabLayout를 통해 효율적이고 직관적인 Fragment 이동이 가능하며, ViewPager2와 Indicator를 통해 사용자 컨트롤러를 만드는 것이 가능하다.
  • PageTransformer를 통해 페이지 전환 Animation에 다양한 커스텀을 적용할 수 있어 원하는 시각적 효과를 얻을 수 있다.

 

 

[ 2 ] ViewPager2 사용하기 - Fragment 전환

  • Fragment 전환의 간단한 예제를 만들어 보자

 

ViewPagerAdapter.kt // Adapter 생성하기

class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
    FragmentStateAdapter(fragmentManager, lifecycle) {

    private val fragmentList = mutableListOf<Fragment>()

    fun addFragment(fragment: Fragment){
        fragmentList.add(fragment)
    }

    override fun getItemCount(): Int {
        return fragmentList.size
    }

    override fun createFragment(position: Int): Fragment {
        return fragmentList[position]
    }
}
  • Adapter 생성
    • 먼저 FragmentStateAdapter를 만들어 해당하는 Item Count 및 각 Fragment의 위치를 정해주고, Activity에서 해당 ViewPagerAdapter에 들어갈 Fragment를 정의할 수 있도록 addFragment 메서를 정의해둔다.

 

 

 

activity_main_xml // ViewPager2 Layout위치 지정 및 TabLayout 생성

<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=".MainActivity">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager2"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="56dp"
        android:layout_marginBottom="56dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/main_tab_layout"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/viewPager2"
        android:background="@color/white"
        app:tabTextColor="@color/light_grey"
        app:tabSelectedTextColor="@color/black"
        app:tabGravity="fill"
        app:tabIconTint="@color/tablayout_color"
        app:tabIndicatorColor="@color/black"
        app:tabRippleColor="@color/apricot"
        app:tabIndicatorFullWidth="true"
        />

</androidx.constraintlayout.widget.ConstraintLayout>
  • TabLayout UI 속성
    • tabMode
      • Default : fixed
      • TabLayout의 모드를 지정해주는것으로 모든탭의 간격이 동일하게 고정된 fixed가 디폴트 값이며, TabLayout내부에 TabMenu가 많을 경우 Scrollable을 지정함으로 TabMenu가 스크롤이 가능하도록 할 수 있다.
    • tabGravity
      • Default : fill
      • TabMenu의 위치 지정 값으로 수평으로 고르게 분포되는 fill과 center, start 옵션값이 존재한다.
    • tabIndicatorColor
      • Default : theme에 따라 상이하다.
      • 현재 선택된 탭을 강조하는 표시선 ( Indicator )의 색상을 설정한다.
    • tabIndicatorFullWidth
      • Default : false
      • 선택된 탭을 강조하는 Indicator 의 길이를 탭 길이에 맞춘다. true / false
    • tabIndicatorGravity
      • Default : Bottom
      • Indicator의 위치를 지정할 수 있다. 
    • tabTextAppearance
      • Default : theme에 따라 상이하다
      • 탭 텍스트의 스타일을 지정한다. styles.xml 파일에서 사용자 정의 스타일을 정의하거나 사전 정의된 스타일을 사용할 수 있다.
    • tabBackground
      • Default : null
      • 탭의 배경으로 사용할 Drawable Source를 지정한다.
    • tabRippleColor
      • Default : theme에 따라 상이하다
      • 탭 클릭 시 시각적 클릭 이벤트 효과 색상을 지정한다.
    • tabMinWidth
      • Default : 0dp
      • 각 탭의 최소 너비를 지정한다. tabMode = "scrollable" 을 사용할 때 유용하다.

 

 

 

res/color/tablayout_color.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/light_red" android:state_selected="true"/>
    <item android:color="@color/sky"/>
</selector>
  • res/color에 selector를 정의함으로써 현재 해당하는 탭의 Icon 색상을 바꿀 수 있는데 이때 주의해야할 점은 state_selected = true 가 있는 코드줄이 위에 있어야 한다. 아래와 같이 state_selected = true가 아래에 있으면 선택된 탭의 색상이 변경되지 않고 먼저 작성한 Item의 색상으로 Icon이 적용된다.
// 잘못된 코드
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/sky"/>
    <item android:color="@color/light_red" android:state_selected="true"/>
</selector>

 

MainActivity.kt

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val tabTextList = listOf("First", "Second")
    private val tabIconList =
        listOf(R.drawable.ic_android_black_24dp, R.drawable.baseline_back_hand_24)

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

        val viewPager = binding.viewPager2
        val adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
        adapter.apply {
            addFragment(FirstFragment())
            addFragment(SecondFragment())
        }
        viewPager.adapter = adapter

        val tabLayout = binding.mainTabLayout
        TabLayoutMediator(tabLayout, viewPager) { tab, position ->
            tab.text = tabTextList[position]
            tab.setIcon(tabIconList[position])
        }.attach()
    }
}
  • TabLayoutMediator
    • TabLayoutMediator는 ViewPager2와 TabLayout을 연결하여 탭 클릭 시 해당하는 페이지로 이동하는 상호작용을 관리한다. TabLayoutMediator에서 해당하는 페이지의 탭 이름과 아이콘, 포지션을 정의하는 객체를 생성해주고 attach() 메서드로 탭에 대한 설정을 마무리 해준다.
    • 추가적인 탭 선택 이벤트를 구현하려면 TabLayout.OnTabSelectedListener 인터페이스를 구현하여 원하는 이벤트 동작을 정의할 수 있다.
  • ViewPager2 Adapter 연결
    • 생성해둔 ViewPagerAdapter를 XML에서 정의한 ViewPager2에 연결해준다. ViewPager2 객체를 생성할 때 SupportFragmentManager와 Activity의 LifeCycle을 전달하여 어댑터가 Fragment를 관리할 수 있도록 한다.
    • Adpater에 Fragment를 추가하여 ViewPager가 각각의 Fragment를 표시할 수 있도록 해주고 viewPager객체에 adapter를 적용시켜준다.

 

 


 

[오늘 복습한 내용]

1. RecyclerView


[오류,에러 등등]

1. 탭 아이콘 색상이 변경되지 않는 오류

res/color/tablayout_color.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/light_red" android:state_selected="true"/>
    <item android:color="@color/sky"/>
</selector>
  • res/color에 selector를 정의함으로써 현재 해당하는 탭의 Icon 색상을 바꿀 수 있는데 이때 주의해야할 점은 state_selected = true 가 있는 코드줄이 위에 있어야 한다. 아래와 같이 state_selected = true가 아래에 있으면 선택된 탭의 색상이 변경되지 않고 먼저 작성한 Item의 색상으로 Icon이 적용된다.
// 잘못된 코드
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/sky"/>
    <item android:color="@color/light_red" android:state_selected="true"/>
</selector>

state_selected = true 의 아이템이 아래에 있던 코드는 정상적으로 작동이 되질 않는다

 


[느낀 점]

1. RecyclerView 다양한 기능이 많아서 추가로 더 정리해봐야겠다.

 

2. 뭐든 하나 배우면 꼬리를 물어서 자세하게 파헤쳐봐야 겠다

 

3. 선택, 집중

 

 


[Reference]

 

// ViewPager2 Fragment 전환

https://notepad96.tistory.com/182

https://developer.android.com/training/animation/screen-slide-2?hl=ko