[Android] ViewPager2 알아보기

임재영·2021년 10월 9일
0

Android

목록 보기
10/10
post-thumbnail

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

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

이 때 횡으로 플립핑 하는 형태의 ViewHolder를 구현하려면 ViewPager를 사용해야 합니다.

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

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

이러한 문제로 인해 ViewPager를 사용하지 않고 수직 RecyclerView에 수평 RecyclerView를 넣고 Pager의 느낌을 내기 위하여 PagerSnapHelper를 커스텀해서 사용 하는 경우가 빈번했습니다.

PagerSnapHelper를 커스텀해서 사용하는 것은 비용이 큰 작업입니다.

그러나 우리는 새로운 ViewPager2 덕분에 더 이상 이런 작업들을 직접 할 필요가 없게 되었습니다! RecyclerView를 기반으로 만들어진 컴포넌트이며, 내부적으로 PageSnapHelper를 이미 구현되어 있습니다.


ViewPager2 뭐가 달라졌을까?

다음 그림을 통해 ViewPager2ViewPager와 다르게 RecyclerView를 기반으로 만들어진 컴포넌트라는것을 확인할 수 있습니다.

ViewPager2ViewGroup을 상속받았고 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);
        . . .
	}
}

그리고 LinearLayoutManagerPageSnapHelper를 설정하는것을 확인 할 수 있습니다.

때문에 ViewPager2에서는 RecylerView.Adapter의 기능들을 이용 할 수 있습니다.

한 가지 아쉬운 점은 ViewPager2final class로 선언되어있기 때문에 Custom ViewPager2를 만들 수 없습니다.

이어서 ViewPager2에 새롭게 추가된 기능은 다음과 같습니다.

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

이제 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를 적용 할 때 몇가지 주의 해야 할 사항이 있습니다.

Pages must fill the whole ViewPager2 (use match_parent)

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)

이런 에러가 발생하는 이유는 ViewPager2initialize 할 때 addOnChildAttachStateChangeListener를 설정 하는데 이 과정에서 enforceChildFillListener를 등록하게 됩니다.

해당 메서드를 살펴보면, layoutParamwidth 또는 heightmatch_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로 설정 되어야 합니다.

PageTransformer 설정 뒤 notifyDataSetChanged() 호출 시 View가 깨지는 문제

ViewPager2PageTransformer을 설정 한 뒤, 데이터를 추가하고나서 notifyDataSetChanged() 메서드를 호출 하면 뷰가 깨지는 현상이 발생합니다.

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

LayoutManager는 강제로 현재 보이는 모든 View들에 대해서 rebind relayout을 수행하게 되는데, 이 과정에서 PageTransformer의 설정이 다시 적용되지 않기 때문입니다.

전체를 갱신 하는 notifyDataSetChanged() 메서드 대신에 다음과 같이 부분 갱신을 수행하는 메서드를 호출하게 되면 정상적으로 작동하는것을 확인할 수 있습니다.

  • notifyItemChanged(int)
  • notifyItemRangeChanged(int, int)

결론

  • RecyclerView 안에서 ViewPager를 써야한다면 대신 ViewPager2를 사용해보자
  • 경우에 따라 ViewPager2RecyclerView의 전환이 용아하기 때문에 변화에 대응하기 유리하다

참고 자료

profile
어제의 나보다 더 나은 사람이 되자

0개의 댓글