ANDROID 프로그래밍/VIEWPAGER

안드로이드 뷰페이저 기본 사용법. (Android ViewPager)

뽀따 2019. 1. 23. 19:47


1. 화면에 표시될 컨텐츠를 전환하는 방법.

안드로이드를 탑재한 스마트폰이 처음 만들어지던 시기에는, 앱 화면을 구성할 때 UI(User Interface)의 "직관성"이 가장 중요한 이슈 중 하나였습니다. 사용자가 특정 기능을 사용하기 위해 화면의 어떤 요소를 터치해야 하는지, 앱에 표시되는 컨텐츠를 전환하기 위해 어떤 버튼을 선택해야 하는지 등을 쉽게 알 수 있도록 만드는 것이 주 관심 대상이었죠.


그런데 하드웨어 기술 발전과 더불어 스마트폰 사용이 보편화되고 관련 시장이 확장, 성숙됨에 따라, 앱 사용성의 중요 포인트가 "직관성"에서 "편의성"으로 바뀌게 됩니다. 오직 탐색 기능을 위해 화면을 차지하는 여러 종류의 버튼들 또는 점점 커져버린 디스플레이 크기로 인해 한손만으로는 터치가 힘들어진 공간들처럼, 단순히 직관적인 UI에 대한 고민만으로는 사용자의 사용 편의성을 만족시킬 수 없는 상황이 만들어진 것이죠.


이에 스마트폰 제조사들은 여러 가지 새로운 기술들을 앱 개별이 아닌 시스템 차원에서 도입하게 됩니다. 이를테면 스와이프(swipe) 또는 멀티 터치(multi-touch)와 같은 제스쳐(gesture)를 조금 더 적극적으로 활용하게 만든 것이죠.


제스쳐(gesture)는 특정 영역 또는 요소에 한정된 단순 터치의 불편함을 개선할 수 있게 만들어주는 사용자 입력 방법입니다. 특히 스와이프(swipe)의 경우, 특정 공간에 구애받지 않고 화면을 쓸어 넘기는 액션만으로 다양한 기능을 동작할 수 있게 만들 수 있는데요, 스와이프(swipe)가 가장 잘 활용되는 방법 중 하나가, 화면을 좌/우 가로 방향으로 쓸어 넘겨 화면에 표시될 컨텐츠를 전환하는 것입니다. 마치, 책을 볼 때 손가락으로 현재 페이지를 쓸어 넘겨 다음 페이지로 넘어가듯이, 화면에 표시되는 뷰를 전환하기 위해 화면을 쓸어 넘기는 것입니다.


그리고 이러한 변화에 맞게, 앱 개발에 사용되는 UI 요소들도 새롭게 만들어지게 되었는데요. 그 중, 앞에서 설명한, 스와이프를 통해 컨텐츠 전환을 할 수 있도록 만들어주는 요소가 추가되었는데, 뷰페이저(ViewPager)가 바로 그것입니다.

2. 뷰페이저(ViewPager)

뷰페이저(ViewPager)는 데이터를 페이지 단위로 표시하고, 좌/우 뒤집기(flip)을 통해 페이지를 전환할 수 있도록 만들어주는 컨테이너입니다. 자체적으로 화면을 그리는 기능을 가지지는 않고, 여러 종류의 뷰(View) 위젯을 사용하여 각 뷰페이저의 페이지를 구성합니다. 뷰페이저가 뷰그룹(ViewGroup)으로부터 상속된 것을 보면, 컨테이너(Container) 역할을 수행한다는 것을 쉽게 유추할 수 있죠.


뷰페이저는 데이터를 "페이지 단위"로 화면에 표시합니다. 여기서 "페이지"라는 말이, 뷰페이저의 구현 방식을 요약하는 단어인데요. 알다시피, 안드로이드에서는 데이터 리스트를 아이템 단위의 뷰 또는 뷰 집합으로 표시할 때, 어댑터(Adapter)를 사용합니다. (어댑터를 사용하여 데이터를 아이템 뷰로 표시하는 대표적인 컴포넌트는 리스트뷰입니다. 관련 예제는 [안드로이드 리스트뷰 기본 사용법]에서 확인할 수 있습니다.)

2.1 안드로이드 어댑터(Android Adapter)

안드로이드 어댑터(Adapter)의 역할을 한 문장으로 표현하자면, "사용자가 정의한 데이터 리스트를 입력으로 받아들여 화면에 표시할 뷰(View)들을 생성"하는 것입니다. 일반적으로 데이터 리스트에 포함된 데이터 한개 당 하나의 아이템 뷰로 매핑되며, 아이템 뷰는 표시할 데이터에 따라 여러 개의 뷰가 조합된 형태로 구성될 수 있습니다.


안드로이드 SDK에서 제공하는 어댑터들이 데이터로부터 뷰를 만들어낸다는 공통된 역할을 수행한다고 해도, 모든 어댑터 클래스를 위해 동일한 메서드를 작성해야 하거나, 동일한 구현 과정을 거쳐야 하는 것은 아닙니다. 각 어댑터는 자신만의 메서드 실행 흐름을 가지며, 이에 따라 각각의 메서드가 오버라이드(override)되어야 합니다.


뷰페이저의 경우, 페이저어댑터(PagerAdapter)를 사용하여 각 페이지를 위한 뷰를 생성합니다.

2.2 페이저어댑터(PagerAdapter)

페이저어댑터(PagerAdapter)는 뷰페이저(ViewPager)의 페이지뷰를 생성하는데 사용되는 어댑터 클래스입니다.


PagerAdapter는 abstract 키워드로 정의된 추상 클래스입니다. 즉, PagerAdapter 객체를 바로 만들어서 사용할 수는 없고, PagerAdapter로부터 상속받은 자식 어댑터 클래스 객체를 구현해야 한다는 것을 의미합니다.


PagerAdapter 클래스를 상속한 자식 어댑터 클래스를 구현할 때, 오버라이드 해야 할 메서드는 아래와 같습니다.


메서드 설명
instantiateItem(ViewGroup container, int position) position에 해당하는 페이지 생성.
destroyItem(ViewGroup container, int position, Object object) position 위치의 페이지 제거.
getCount() 사용 가능한 뷰의 갯수를 리턴.
isViewFromObject(View view, Object object) 페이지뷰가 특정 키 객체(key object)와 연관되는지 여부.

표에 나열된 설명만으로는 각 메서드가 언제, 어떻게 동작하는지, 각 메서드의 코드를 어떻게 작성해야 하는지 파악하기가 쉽지 않죠? 좀 더 깊은 이해를 위해서 뷰페이저의 페이지 관리와 동작 방식을 설명할 필요가 있겠네요.

3. 뷰페이저(ViewPager) 동작 방식 이해하기.

뷰페이저는 페이지 단위로 뷰를 만들어 화면에 표시한다고 설명했습니다. 하나의 페이지가 뷰페이저 전체 화면에 표시되는데요, 그렇다면 뷰페이저에 포함된 페이지가 몇 개 있는지는 어떻게 알려줄 수 있을까요?

3.1 getCount() 메서드 : 뷰페이저의 전체 페이지 수 결정.

뷰페이저에 포함된 전체 페이지 수는 getCount() 메서드의 리턴 값으로 결정됩니다. 앞에서 언급했듯이, getCount()는 개발자가 직접 오버라이드하여 작성하는 메서드이므로 전체 페이지 수는 개발자가 결정합니다. 페이지를 표시할 데이터를 어떻게 관리하는지에 따라, 상수를 바로 리턴할 수도 있고, 배열이나 리스트 객체의 길이를 리턴할 수도 있습니다.


당연하게도, 스와이프 제스쳐 또는 ViewPager.setCurrentItem()을 통해 페이지를 전환하더라도 getCount() 메서드로 리턴되는 값 이상의 페이지로는 페이지 전환이 이루어지지 않습니다.


자, 전체 페이지 수가 어떻게 결정되는지 확인했으니, 이제 각 페이지가 언제 어떻게 만들어지는지 확인해볼까요?

3.2 뷰페이저(ViewPager)의 페이지 생성과 관리

뷰페이저에 페이지가 열 개 있다고 가정해 봅시다. 뷰페이저가 처음 화면에 표시될 때 첫 번째 페이지가 표시될텐데요, 그 첫 번째 페이지는 언제 만들어지는 걸까요? 그리고 스와이프에 의해 두 번째 페이지로 전환이 된다면, 이 두 번째 페이지는 또 언제 생성이 되는 걸까요? 각 페이지가 화면에 표시되는 매 순간 생성될까요? 아니면 뷰페이저가 화면에 표시되기 전에 모든 페이지를 미리 생성해놓고, 현재 페이지만 전환하는 걸까요?


정답은, 뷰페이저는 항상 현재 페이지를 기준으로 좌/우 하나 씩, 즉, 현재 페이지를 포함하여 "최대 세 개의 페이지를 생성 및 관리"한다 입니다. 이는 "페이지가 표시되는 매 순간 생성"하는 방법과 "미리 모든 페이지를 생성"하는 방법을 절충한 방법이라고 볼 수 있죠. 만약 페이지를 표시하는 순간에 뷰를 생성하게 되면, 생성 과정에 소요되는 지연으로 인해 페이지가 늦게 표시되는 문제가 생길 것입니다. 그리고 만약 앱의 시작과 동시에 모든 페이지를 생성하게 되면, 페이지 수가 많거나 뷰가 복잡한 경우 많은 메모리가 필요하게 되죠. 그래서 그 중간 정도의 해결책인, 현재 페이지 기준으로 세 개의 페이지를 생성하는 방법을 택한 것입니다. 글로만 설명하니 이해가 쉽지 않죠? 아래 그림을 통해 뷰페이저가 페이지를 생성하고 유지하는 방법을 살펴보시길 바랍니다.


그리고 뷰페이저는 내부적으로 각 페이지를 뷰(View)로써 직접 관리하지 않고, 각 페이지와 연관되는 키 객체(key object)로 관리합니다. 여기서 키 객체(key object)는 특별한 타입의 클래스 객체를 말하는 것은 아니고, 페이지 참조 및 식별을 위해 사용되는 Object 타입 객체를 말합니다. 키(key)는 ArrayList로 관리되는데, 이는 페이지의 위치와 관계없이 지정된 페이지를 추적하고 고유하게 식별하게 해줍니다.


그런데 사실, 일반적으로 뷰페이저가 내부적으로 페이지를 관리하는 방법에 대해 앱 개발자가 자세하게 알 필요는 없습니다. 개발자의 주 관심은 "페이지뷰를 어디서, 어떻게 만들면 되는가?"이죠. 음, 페이지뷰는 어디서 만들면 될까요?

3.3 instantiateItem() 메서드 : 화면에 표시할 페이지뷰 생성.

화면에 표시할 페이지뷰를 만드는 작업은 페이저어댑터의 instantiateItem() 메서드에서 수행합니다. 파라미터로 전달된 position에 해당하는 페이지를 생성한 다음, 또 다른 파라미터로 전달된 컨테이너(=뷰페이저 객체)에 생성된 페이지뷰를 추가하면 됩니다.

그런 다음, 페이지 식별을 위한 Object 객체를 리턴합니다. 물론, 생성된 페이지뷰의 참조를 리턴하는 게 일반적이지만, 반드시 페이지뷰의 참조를 직접 리턴해야 하는 것은 아닙니다. 페이지를 식별할 수 있는 키 객체(key object)를 리턴하면 됩니다.

설명에 대한 이해가 쉽지 않을 수 있겠는데요, 아래 예제 코드를 보면 어떤 내용인지 이해가 되실거라 생각합니다.

3.4 isViewFromObject() 메서드 : 페이지뷰가 키 객체와 연관되는지 확인.

isViewFromObject() 메서드는 뷰페이저 내부적으로 관리되는 키 객체(key object)와 페이지뷰가 연관되는지 여부를 확인하는 역할을 수행합니다. 어떤 정보를 키 객체(key object)로 사용할 것인가, 즉, instantiateItem()에서 리턴하는 객체의 종류에 따라 이 메서드의 구현 코드가 결정될텐데요, 일반적으로 instantiateItem()에서 페이지뷰의 View 객체를 리턴하고, isViewFromObject()에서는 해당 View와 Object가 동일한지 여부를 검사하여 그 결과를 리턴하도록 구현합니다.

    @Override
    public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
        return (view == (View)object) ;
    }

4. 뷰페이저(ViewPager) 사용하기.

앞서 설명한 내용만으로 뷰페이저(PagerView)를 어떻게 사용해야 하는지 쉽게 이해가 되시나요? 좀 더 쉬운 이해를 돕기위해, 예제를 통해 뷰페이저(PagerView)를 사용하는 방법에 대해 알아보겠습니다.


예제는 아래 그림과 같이, 전체 화면을 차지하는 뷰페이저에 열 개의 페이지를 만듭니다. 최대한 간단한 설명을 위해, 각 페이지에는 하나의 텍스트뷰만을 표시하도록 만들겠습니다.


4.1 워크 플로우

뷰페이저 사용하기 예제 구현은 아래와 같은 순서로 수행됩니다.


(참고로 아래의 예제는, 안드로이드 스튜디오 프로젝트 생성 단계에서 "Basic Activity"를 선택하여 생성된 코드를 기반으로 작성되었습니다.)

4.2 메인액티비티 레이아웃에 뷰페이저 추가.

예제에서 뷰페이저는 메인액티비티에 표시합니다. 이를 위해 메인액티비티 레이아웃 XML 파일에 PagerView를 추가합니다. 예제에서는, 메인액티비티의 루트 레이아웃이 ConstraintLayout인 것에 주의하세요.

[STEP-1] "content_main.xml" - 메인액티비티 레이아웃 리소스 XML 파일
    <android.support.v4.view.ViewPager
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:id="@+id/viewPager"/>

4.3 뷰페이저 페이지 레이아웃 리소스 XML 작성.

다음 단계로, 뷰페이저의 페이지로 표시될 페이지뷰 레이아웃을 작성합니다. 여기서 작성한 레이아웃 리소스는, 이후 단계에서 페이저어댑터를 구현할 때 instantiateItem() 메서드에서 LayoutInflater를 통해 뷰로 변환합니다.


그런데 한 가지 알아둘 것은, 뷰페이저의 페이지뷰 화면을 만들 때 반드시 본문의 예제처럼 LayoutInflater를 사용해야 하는 것은 아니라는 것입니다. 예제와는 다르게 new 키워드를 사용하여 뷰 위젯 등을 직접 생성하거나, 프래그먼트를 사용하여 페이지 화면을 만드는 등 다양한 방법으로 페이지뷰를 구성할 수 있습니다. 그러므로 예제에서의 페이지뷰 작성 방법을 무조건 따라야 한다는 오해는 하지 마시길 바랍니다.


레이아웃 리소스("/res/layout/")에 "page.xml"을 추가한 다음, 텍스트뷰를 하나 추가합니다.

[STEP-2] "/res/layout/page.xml" - 페이지 레이아웃 리소스 작성.
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:textSize="48sp"
        android:id="@+id/title"/>

</android.support.constraint.ConstraintLayout>

4.4 페이저어댑터(PagerAdapter) 상속 및 구현

앞서 뷰페이저를 추가하고 페이지에 대한 레이아웃도 만들었으므로, 이제 페이저어댑터를 구현하여 데이터 리스트로부터 페이지뷰를 생성하는 코드를 작성해야 합니다. 이를 위해 PagerAdapter 클래스를 상속한 클래스를 만들고, [2.2 페이저어댑터(PagerAdapter)]에서 소개한 몇 가지 메서드를 오버라이드합니다.

[STEP-3] "TextViewPagerAdapter.java" - PagerAdapter 상속 및 구현.
public class TextViewPagerAdapter extends PagerAdapter {

    // LayoutInflater 서비스 사용을 위한 Context 참조 저장.
    private Context mContext = null ;

    public TextViewPagerAdapter() {

    }

    // Context를 전달받아 mContext에 저장하는 생성자 추가.
    public TextViewPagerAdapter(Context context) {
        mContext = context ;
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        View view = null ;

        if (mContext != null) {
            // LayoutInflater를 통해 "/res/layout/page.xml"을 뷰로 생성.
            LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            view = inflater.inflate(R.layout.page, container, false);

            TextView textView = (TextView) view.findViewById(R.id.title) ;
            textView.setText("TEXT " + position) ;
        }

        // 뷰페이저에 추가.
        container.addView(view) ;

        return view ;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        // 뷰페이저에서 삭제.
        container.removeView((View) object);
    }

    @Override
    public int getCount() {
        // 전체 페이지 수는 10개로 고정.
        return 10;
    }

    @Override
    public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
        return (view == (View)object);
    }
}

4.5 뷰페이저에 페이저어댑터 지정하기.

마지막으로, 메인액티비티에서 뷰페이저의 참조를 가져온 다음, 앞서 구현한 TextViewPagerAdapter 인스턴스를 뷰페이저에 지정합니다.

[STEP-4] "MainActivity.java" - 뷰페이저에 페이저어댑터 지정.
public class MainActivity extends AppCompatActivity {

    private ViewPager viewPager ;
    private TextViewPagerAdapter pagerAdapter ;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ,,, 코드 계속.

        viewPager = (ViewPager) findViewById(R.id.viewPager) ;
        pagerAdapter = new TextViewPagerAdapter(this) ;
        viewPager.setAdapter(pagerAdapter) ;
    }
}

5. 실행 화면.

예제를 실행하면, 아래와 같은 화면이 표시됩니다. 첫 번째 페이지뷰(TEXT 0)가 표시되는 것을 확인할 수 있습니다.


그리고 오른쪽 엣지 스와이프를 통해 다음 페이지로 전환, 왼쪽 엣지 스와이프를 통해 이전 페이지로 전환할 수 있습니다.


6. 참고

.END.