안녕하세요. 강남언니에서 Android 개발을 맡고 있는 David 입니다.
이번 글에서는 ViewPager2 의 새로운 기능, 사용방법 그리고 적용할 때 주의해야할 점에 대하여 다뤄보도록 하겠습니다.

대부분 앱의 메인페이지는 Feed 형식으로 이뤄져 있습니다.

사용자들한테 정보를 많이 보여주기 위하여 RecyclerView안에 다양한 ViewHolder가 존재하는 포맷으로 이뤄져 있습니다.

ViewHolder 중 Pager 형식의 ViewHolder를 구현하려면 ViewPager를 사용해야 합니다.

하지만 RecyclerView에 ViewPager를 집어넣었을 경우 문제점이 발생하게 됩니다.

ViewPagerPagerAdapter 기반으로 구성되어있는데 스크롤을 진행할 때 마다 instantiateItem()destroyItem() 메서드가 호출되기 때문에 스크롤 할 때 버벅거리는 현상이 나타납니다.

이러한 문제점을 해결하기 위하여 ViewPager를 사용하지 않고 RecyclerView에 RecyclerView를 집어넣고 Pager의 느낌을 내기 위하여 PagerSnapHelper를 이용하여 해결했습니다.

하지만 PagerSnapHelper를 커스텀해서 사용하기에는 너무 많은 cost가 드는 문제점이 있었습니다.

이번에 나온 ViewPager2RecyclerView기반으로 만들어진 컴포넌트이기 때문에 PagerSnapHelper를 별도로 커스텀하여 개발할 필요가 없습니다.


What is new in ViewPager2

위에서 말했다 싶이 ViewPager2는 ViewPager와 다르게 RecyclerView를 기반으로 만들어진 컴포넌트라는것을 확인할 수 있습니다.

ViewPager2는 ViewGroup을 상속받았고

initialize 메서드에서 RecyclerView를 생성하는것을 확인할 수 있습니다.

public final class ViewPager2 extends ViewGroup {
		private void initialize(Context context, AttributeSet attrs) {
        	mRecyclerView = new RecyclerViewImpl(context);
        	mLayoutManager = new LinearLayoutManagerImpl(context);
        	mRecyclerView.setLayoutManager(mLayoutManager);
					mPagerSnapHelper = new PagerSnapHelperImpl();
          mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
        	. . .
    	}
}

그리고 LinearLayoutManager를 세팅한 다음 PageSnapHelper를 설정하는것을 확인할 수 있습니다.

그래서 ViewPager2에서는 RecylerView.Adapter의 API들을 이용할 수 있습니다.

그리고 ViewPager2는 final class로 선언되어있기 때문에 Custom ViewPager를 만들 수 없습니다.

부가적으로 ViewPager2에 새롭게 추가된 기능은 다음과 같습니다.

  • RTL (right to left) layout support
  • Vertical orientation support
  • Reliable Fragment support
  • Dataset change animations

그러면 이제 ViewPager2 적용하는 방법에 대하여 알아보도록 하겠습니다.


How to use ViewPager2

ViewPager2를 적용하기 위하여 build.gradle에 다음과 같이 추가합니다.

dependencies {
    implementation "androidx.viewpager2:viewpager2:1.0.0"
}

그리고 layout file에 ViewPager2를 설정합니다

<?xml version="1.0" encoding="utf-8"?>
<androidx.viewpager2.widget.ViewPager2 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/viewPager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

RecyclerView.Adapter를 설정하듯이 Adapter를 다음과 같이 설정 합니다.

class RecyclerViewAdapter(var items: ArrayList<String> = arrayListOf()) : RecyclerView.Adapter<ViewHolder>() {
  
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(LayoutInflater.from(parent.context)
                          .inflate(R.layout.list_item, parent, false))
    }
  
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.setData(items[position])
    }
  
override fun getItemCount(): Int = items.size
  
  ...
}

ViewPager2의 Adapter 설정은 RecylerView.Adapter 또는 FragmentStateAdapter로 설정 할 수 있습니다.

viewPager.adapter = RecylerViewAdapter() 
viewPager.adapter = FragmentStateAdapter()

기존에 사용하던 FragmentStatePagerAdapterFragmentStateAdapter로 변경되었습니다.

그러면 다음과 같은 결과가 나옵니다

Vertical Orientation Support

Vertical Scroll를 사용하고 싶으면

layout file에서 다음과 같이 설정할 수 있고

android:orientation="vertical"

programmtically도 설정할 수 있습니다.

viewPager.orientation = ViewPager2.ORIENTATION_VERTICAL

아주 쉽게 Vertical Scroll 기능을 적용할 수 있는 것을 볼 수 있습니다.


How to create a two-page preview

ViewPager에서 양쪽 페이지를 미리보는 기능을 만들려면

setPageMargin() 메서드를 호출하여 설정했습니다.

하지만 ViewPager2에서는 setPageMargin() 메서드가 존재하지 않습니다.

ViewPager2에서 양쪽 페이지를 미리보는 기능을 만들려면

clipToPadding, clipChilderen 값을 false로 지정해주고

android:clipToPadding="false"
android:clipChildren="false"

setOffscreenPageLimit() 값을 설정해줍니다.

그리고 setPageTransformer() 메서드 에서 값을 설정해줘야 합니다.

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

viewPager.setPageTransformer { page, position ->
    page.translationX = position * -offsetPx
}

page의 offset을 설정해야하는데

현재 보이는 screen width에서 page간의 margin값을 빼주고

pager width 값을 빼면 offsetPx를 구할 수 있습니다.

해당 값을 설정하게 되면 다음과 같이 적용된 모습을 볼 수 있습니다.

offsetPx 값을 고정으로 설정해도 양쪽 페이지를 미리 볼 수 있지만

기기마다 screen width가 다르기 때문에 일정하게 보이지 않은 경우가 발생합니다.


Catuious about when applying ViewPager2

ViewPager2를 적용할때 몇가지 주의해야할 사항이 있습니다.

1. Pages must fill the whole ViewPager2 (use match_parent) Error

ViewPager2의 ChildView를 inflate 할 경우에

 LayoutInflater.from(context).inflate(resource, this, attachToRoot)

attachToRoot값은 false여야 하고 width, height 값은 match_parent 여야 합니다.

위의 방식대로 진행하지 않을 경우

java.lang.IllegalStateException: Pages must fill the whole ViewPager2 (use match_parent)
    at androidx.viewpager2.widget.ViewPager2$2.onChildViewAttachedToWindow(ViewPager2.java:170)

다음과 같은 에러메시지가 뜹니다.

그 이유는 ViewPager2가 initialize 할 때 addOnChildAttachStateChangeListener를 설정하는데

그때 enforceChildFillListener 를 등록하게 됩니다. 해당 메서드를 살펴보면

private RecyclerView.OnChildAttachStateChangeListener enforceChildFillListener() {
    return new RecyclerView.OnChildAttachStateChangeListener() {
        @Override
        public void onChildViewAttachedToWindow(@NonNull View view) {
            RecyclerView.LayoutParams layoutParams =
                    (RecyclerView.LayoutParams) view.getLayoutParams();
            if (layoutParams.width != LayoutParams.MATCH_PARENT
                    || layoutParams.height != LayoutParams.MATCH_PARENT) {
                throw new IllegalStateException(
                        "Pages must fill the whole ViewPager2 (use match_parent)");
            }
        }
    };
}

layoutParamwidth 또는 height 둘 중 하나가 match_parent가 아닐 경우 익셉션이 일어나도록 설정되어 있습니다.

그래서 ChildView의 width와 height는 match_parent로 설정을 해줘야 합니다.

2. PageTransformer 설정 한 후 notifyDataSetChanged() 호출 시 View가 깨지는 Error

ViewPager2에 PageTransformer을 설정 한 후

데이터를 추가하고나서 notifyDataSetChanged() 메서드를 호출하면 다음과 같은 현상이 나타납니다.

그 이유는 notifyDataSetChanged() 메서드를 호출하게 되면

LayoutManager는 강제로 현재 보이는 모든 View들을 다시 rebind하고 relayout 하게되는데

이 과정에서 PageTransformer를 설정했던게 정상적으로 보이지 않게 됩니다.

notifyDataSetChanged() 메서드 대신에 아래와 같은 메서드를 호출하게 되면 정상적으로 작동하는것을 확인할 수 있습니다.

  • notifyItemChanged(int)
  • notifyItemInserted(int)
  • notifyItemRemoved(int)
  • notifyItemRangeChanged(int, int)
  • notifyItemRangeInserted(int, int)
  • notifyItemRangeRemoved(int, int)

Conclusion

ViewPager에서 ViewPager2로 migration 작업을 진행할 때 많은 cost가 들지 않았습니다.

무엇보다 RecyclerView.Adapter를 사용할 수 있다 라는 점이 편했습니다.

하지만 위에서 말했다 싶이 몇가지 주의해야할점이 있었는데

Google Guide에도 명시되어있지 않아서 ViewPager2를 적용하는데 꽤 애를 먹었습니다.

지금은 1.0.0 버전이여서 ViewPager2로 migration 작업을 진행하는 게 이를 수도 있겠지만

언제든지 ViewPager2에서 RecyclerView로 전환할 수 있는 장점과

그 외에 RecyclerView안에 ViewPager2를 집어넣어도 리소스를 많이 사용하지 않는다라는 점 등등

이점이 많기 때문에 ViewPager2로 migration 작업하는 것을 추천합니다.

[참조 블로그] : https://medium.com/google-developer-experts/exploring-the-view-pager-2-86dbce06ff71

David
Android Developer
강남언니에서 안드로이드 개발을 맡고 있습니다. 새로운 기술을 꾸준히 연구하고 있고 코드 품질을 높이기 위하여 노력하고 있습니다.