대부분 앱의 메인페이지는 Feed 형식으로 이뤄져 있습니다.
사용자들한테 정보를 많이 보여주기 위하여 RecyclerView안에 다양한 ViewHolder가 존재하는 형태로 이뤄져 있습니다.
이 때 횡으로 플립핑 하는 형태의 ViewHolder를 구현하려면 ViewPager를 사용해야 합니다.
하지만 RecyclerView에 ViewPager를 집어넣었을 경우 문제점이 발생하게 됩니다.
ViewPager는 PagerAdapter 기반으로 구성되어있는데 스크롤을 진행할 때 마다 instantiateItem() 와 destroyItem() 메서드가 호출 되기 때문에 스크롤 할 때 버벅거리는 현상이 나타납니다.
이러한 문제로 인해 ViewPager를 사용하지 않고 수직 RecyclerView에 수평 RecyclerView를 넣고 Pager의 느낌을 내기 위하여 PagerSnapHelper를 커스텀해서 사용 하는 경우가 빈번했습니다.
PagerSnapHelper를 커스텀해서 사용하는 것은 비용이 큰 작업입니다.
그러나 우리는 새로운 ViewPager2 덕분에 더 이상 이런 작업들을 직접 할 필요가 없게 되었습니다! RecyclerView를 기반으로 만들어진 컴포넌트이며, 내부적으로 PageSnapHelper를 이미 구현되어 있습니다.
다음 그림을 통해 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의 기능들을 이용 할 수 있습니다.
한 가지 아쉬운 점은 ViewPager2는 final class로 선언되어있기 때문에 Custom ViewPager2를 만들 수 없습니다.
이어서 ViewPager2에 새롭게 추가된 기능은 다음과 같습니다.
이제 ViewPager2를 적용하는 방법에 대하여 알아보도록 하겠습니다.
ViewPager2를 적용하기 위하여 앱모듈의 build.gradle에 다음 의존성을 추가합니다.
dependencies {
implementation "androidx.viewpager2:viewpager2:1.0.0"
}
그리고 layout.xml 파일에 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 MyAdapyer(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
...
}
위와 같이 만들어진 어댑터는 다음과 같이 적용 할 수 있습니다.
viewPager.adapter = MyAdapter()
이렇게 ViewPager2를 프로젝트에 적용하기 위한 간단한 방법들을 소개 해드렸고, 이어서 ViewPager2에서 변경 된 부가 기능에 대해서 설명 드리겠습니다.
layout 파일에서 android:orientation="vertical"과 같이 적용 할 수 있습니다.Programmtically 한 설정은 viewPager.orientation = ViewPager2.ORIENTATION_VERTICAL과 같습니다.ViewPager에서 양쪽 페이지를 미리보는 기능을 만들려면, setPageMargin() 메서드를 호출하여 설정했습니다.
하지만 ViewPager2에서는 setPageMargin() 메서드가 존재 하지 않습니다.
ViewPager2에서 양쪽 페이지를 미리보는 기능을 만들기 위해선 다음과 같이 작업해야 합니다.
android:clipToPadding="false"
android:clipChildren="false"
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과 width 값을 빼서 offset을 구했습니다.
그냥 상수를 적용해도 되긴 하지만, 그렇게 할 경우 여러 기기에서 보여지는 UI가 제각각이 될 수 있기 때문에 위와 같이 적용했습니다.
ViewPager2를 적용 할 때 몇가지 주의 해야 할 사항이 있습니다.
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를 등록하게 됩니다.
해당 메서드를 살펴보면, layoutParam의 width 또는 height가 match_parent가 아닐 경우 예외를 발생시키도록 설정 되어 있습니다.
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)");
}
}
};
}
때문에 ChildView의 width, height는 match_parent로 설정 되어야 합니다.
ViewPager2에 PageTransformer을 설정 한 뒤, 데이터를 추가하고나서 notifyDataSetChanged() 메서드를 호출 하면 뷰가 깨지는 현상이 발생합니다.
그 이유는 notifyDataSetChanged() 메서드를 호출 하게 되면
LayoutManager는 강제로 현재 보이는 모든 View들에 대해서 rebind relayout을 수행하게 되는데, 이 과정에서 PageTransformer의 설정이 다시 적용되지 않기 때문입니다.
전체를 갱신 하는 notifyDataSetChanged() 메서드 대신에 다음과 같이 부분 갱신을 수행하는 메서드를 호출하게 되면 정상적으로 작동하는것을 확인할 수 있습니다.
notifyItemChanged(int)notifyItemRangeChanged(int, int)RecyclerView 안에서 ViewPager를 써야한다면 대신 ViewPager2를 사용해보자ViewPager2와 RecyclerView의 전환이 용아하기 때문에 변화에 대응하기 유리하다