본문 바로가기

책 요약하기/이것이 안드로이드다

#5.5 탭 메뉴로 화면 구성 ViewPager와 TabLayout 2021-03-14

5.1 ViewPager에서 프래그먼트 사용하기

ViewPager와 Adapter

뷰 페이저는 리사이클러뷰와 구현 방식이 비슷한데 한 화면에 하나의 아이템만 보여지는 리사이클러뷰라고 생각하면 됩니다. 페이저어댑터를 통해서 뷰페이저에서 보여질 화면들을 연결하는 구조도 리사이클러뷰와 동일합니다.

1. 뷰페이저 어댑터 생성 합니다.

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

 

2. 뷰페이저어댑터를 만듭니다.(뷰페이저에 프래그먼트를 보여주기 위해서) FragmentAdapter 클래스를 생성한 후에 FragmentPagerAdapter를 상속받습니다.

class FragmentAdapter : FragmentPagerAdapter {
}

 

3. FragmentPagerAdapter 뒤에서 [Alt+Enter] 클릭 한 후에 파라미터가 2개인 생성자로 선택하고 클래스 내부에 [ctrl+i]로 2개의 메소드를 구현합니다.

class FragmentAdapter(fm: FragmentManager, behavior: Int) : FragmentPagerAdapter(fm, behavior) {
    override fun getItem(position: Int): Fragment {
        TODO("Not yet implemented")
    }

    override fun getCount(): Int {
        TODO("Not yet implemented")
    }
}
FragmentPagerAdapter의 필수 메서드
getItem(): 현재 페이지의 position이 파라미터로 넘어옵니다. position에 해당하는 위치의 프래그먼트를 반환해야 합니다.
getCount(): 어댑터가 화면에 보여줄 전체 프래그먼트의 개수를 반환해야 합니다.

 

4. fragmentList 변수를 생성하고, 2개의 메서드를 구현합니다.

class FragmentAdapter(fm: FragmentManager, behavior: Int) : FragmentPagerAdapter(fm, behavior) {
    var fragmentList = listOf<Fragment>()
    override fun getItem(position: Int): Fragment {
        return fragmentList.get(position)
    }

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

 

메인 액티비티에서 연결하기

1. MainActivity.kt에서 프래그먼트 목록을 생성하는 코드를 추가합니다.

2. adapter를 생성하고 viewPager에 adapter를 적용합니다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 프래그먼트 목록 리스트 변수 생성
        val fragmentList = listOf(FragmentA(), FragmentB(), FragmentC(), FragmentD())

        // 어댑터 생성 및 프래그먼트 목록 변수를 적용
        val adapter = FragmentAdapter(supportFragmentManager, 1)
        adapter.fragmentList = fragmentList


        // 뷰페이저 어댑터에 프래그먼트 어댑터 적용
        var viewPager : ViewPager = findViewById<ViewPager>(R.id.viewPager)
        viewPager.adapter = adapter

    }
}

 

TabLayout 적용하기

1. activity_main.xml의 디자인 편집창을 열고 tabLayout을 추가합니다.

 

2. FragmentAdapter.kt를 열고 getPageTitle()메서드를 오버라이드 하여 코드를 작성합니다.

class FragmentAdapter(fm: FragmentManager, behavior: Int) : FragmentPagerAdapter(fm, behavior) {
    var fragmentList = listOf<Fragment>()
    override fun getItem(position: Int): Fragment {
        return fragmentList.get(position)
    }

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

    override fun getPageTitle(position: Int): CharSequence? {
        return when (position) {
            0 -> "A"
            1 -> "B"
            2 -> "C"
            else -> "D"
        }
    }
}

 

5.2 View를 사용하는 뷰페이저 만들기

앞에서 프래그먼트를 사용해 뷰페이저를 구현했는데 이 방식은 각 프래그먼트의 생명주기를 따로 관리해야해서 번거롭습니다. 단순하게 화면에 텍스트나 이미지를 표시하는 용도를 넘어 실시간으로 데이터를 주고받고 갱신하거나 메뉴 간 이동이 잦으면 생명 주기 관리 문제로 앱이 종료됩니다.
이런 문제로 인해 프래그먼트 대신에 레이아웃을 인플레이트해서 사용합니다. 동일한 조건이라면 프래그먼트를 사용할 때보다 뷰가 시스템 자원(메모리, cpu, 등)을 덜 사용하기 때문에 더 효율적일 수 있습니다.

 

1. 4개의 레이아웃을 생성합니다.

 

2. 4개의 레이아웃 파일을 인프레이트해서 사용할 커스텀뷰 클래스를 생성합니다. 레이아웃 파일을 뷰로 만들어서 사용할 계획이므로 뷰를 삽입할 수 있는 레이아웃 클래스 중에 하나를 상속받아서 만듭니다. 그리고 layout_a.xml파일을 인플레이트해서 view변수에 담은 후 자기 자신인 CustomA에 addView 해줍니다. customA 클래스는 레이아웃을 상속받았으므로 정렬, 패딩과 같은 옵션을 추가할 수 있습니다. 다음과 동일하기 customB, customC, customD 클래스 파일도 생성합니다.

class customA(context: Context?) : LinearLayout(context) {
    var view = LayoutInflater.from(context).inflate(R.layout.layout_a, 
        this, false)
}

 

[Ctrl + i], [Ctrl + o]키의 차이
[Ctrl + i] (implement) : 메서드명만 있는 인터페이스가 설계되어 있습니다. 메서드 내부에 코드를 작성해두면 부모 클래스에 작성되어 있는 코드에서 우리가 작성한 인터페이스 메서드를 호출해서 사용합니다. 인터페이스는 구현하지 않으면 컴파일되지 않습니다.

[Ctrl + o] (Override) : 부모 클래스에 이미 만들어져 있는 메서드를 내 코드에 맞게 재정의하는 것입니다. 구현하지 않아도 컴파일되며, 부모 클래스에 있는 메서드가 호출되고 실행됩니다.

CustomPagerAdapter 만들기

1. 생성한 뷰를 사용하는 커스텀 어댑터를 생성합니다. 프래그먼트를 사용할 때와는 다른 PagerAdapter를 상속받아서 사용합니다. 해당 CustomPagerAdapter 클래스 파일을 생성하고 instantiateItem(), destroyItem() 메서드를 추가합니다.

class CustomPagerAdapter : PagerAdapter() {
    override fun isViewFromObject(view: View, `object`: Any): Boolean {

    }

    override fun getCount(): Int {

    }

    override fun instantiateItem(container: ViewGroup, position: Int): Any {
        return super.instantiateItem(container, position)
    }

    override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
        super.destroyItem(container, position, `object`)
    }
}

 

2.  getCount() 메서드(뷰 리스트의 개수 return), instantiateItem(container, position): Any, destroyItem(container, position, object) 메서드를 작성합니다.

    override fun getCount(): Int {
        return views.size
    }

    override fun instantiateItem(container: ViewGroup, position: Int): Any {
        // 현재 페이지에 맞는 view를 꺼내서 view 변수에 저장
        var view = views.get(position)

        // 뷰 페이저(container)에 해당 view를 추가
        container.addView(view)

        // 사용 view를 return합니다. 이유는 어댑터가 생성된 view를 가지고 있다가
        // 필요 없을경우 삭제 등의 추가적인 관리를 위해 사용하므로 해당 view를 리턴합니다.
        return view

    }

    override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
        // 마지막 파라미터(object)로 우리가 instantiateItem() 메서드에서 return 하는 뷰가 전달됩니다.
        container.removeView(`object` as View?)
    }
destroyItem 메서드의 역할
뷰페이저는 기본적으로 한 번에 3개의 페이지를 생성합니다. 예를 들어 A, B, C 페이지가 있는데 B 페이지를 호출하면 빠른 화면처리를 위해서 앞뒤의 A와 C 페이지도 미리 생성해둡니다. 그리고 3개의 페이지에 속하지 않는 페이지는 삭제를 해서 메모리 효율을 높이는데 그 역할을 destroyItem() 메서드가 합니다.

 

3. isViewFromObject() 메서드는 instantiateItem() 메서드에서 생성된 오브젝트를 사용할지 여부를 판단합니다. 

    override fun isViewFromObject(view: View, `object`: Any): Boolean {
        // instantiateItem() 메서드에서 생성된 object의 view 타입 체크
        return view == `object` as View?
    }

 

4. getPageTitle() 메서드를 오버라이드하고, 메서드 안에 메뉴명을 반환하는 코드를 작성합니다.

    override fun getPageTitle(position: Int): CharSequence? {
        return when(position) {
            0-> "A"
            1-> "B"
            2-> "C"
            else-> "D"
        }
    }

 

5. 전체코드 모습

class CustomPagerAdapter : PagerAdapter() {
    // 뷰 목록 변수 생성
    var views = listOf<View>()



    override fun isViewFromObject(view: View, `object`: Any): Boolean {
        // instantiateItem() 메서드에서 생성된 object의 view 타입 체크
        return view == `object` as View?
    }

    override fun getCount(): Int {
        return views.size
    }

    override fun instantiateItem(container: ViewGroup, position: Int): Any {
        // 현재 페이지에 맞는 view를 꺼내서 view 변수에 저장
        var view = views.get(position)

        // 뷰 페이저(container)에 해당 view를 추가
        container.addView(view)

        // 사용 view를 return합니다. 이유는 어댑터가 생성된 view를 가지고 있다가
        // 필요 없을경우 삭제 등의 추가적인 관리를 위해 사용하므로 해당 view를 리턴합니다.
        return view

    }

    override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
        // 마지막 파라미터(object)로 우리가 instantiateItem() 메서드에서 return 하는 뷰가 전달됩니다.
        container.removeView(`object` as View?)
    }

    override fun getPageTitle(position: Int): CharSequence? {
        return when(position) {
            0-> "A"
            1-> "B"
            2-> "C"
            else-> "D"
        }
    }
}

 

레이아웃 파일에 viewPager와 TabLayout 추가하고 소스코드 연결하기

1. viewPager, tabLayout 레이아웃을 activity_main.xml파일에 추가합니다.

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent">


        <com.google.android.material.tabs.TabLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <com.google.android.material.tabs.TabItem
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Monday" />

            <com.google.android.material.tabs.TabItem
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Tuesday" />

            <com.google.android.material.tabs.TabItem
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Wednesday" />
        </com.google.android.material.tabs.TabLayout>
    </androidx.viewpager.widget.ViewPager>

 

2. MainActivity.kt에 뷰페이저에서 사용할 뷰 클래스를 모두 생성해서 views 변수에 저장합니다.

val views : List<View> = listOf(customA(this), customB(this),
        customC(this), customD(this))

 

3. 커스텀 어댑터를 생성하고 뷰 클래스 목록을 어댑터에 저장합니다. 그리고 viewPager에 adapter를 설정합니다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 커스텀뷰 목록 변수 생성
        val views : List<View> = listOf(customA(this), customB(this),
        customC(this), customD(this))

        // 커스텀 어댑터 생성 및 adapter의 뷰 클래스 목록에 커스텀뷰 목록 변수를 저장
        val adapter: CustomPagerAdapter = CustomPagerAdapter()
        adapter.views = views

        // viewPager에 adapter를 연결
        var viewPager : ViewPager = findViewById<ViewPager>(R.id.viewPager)
        viewPager.adapter = adapter
        
    }
}

 

- 미니 퀴즈 5-5 -

1. 뷰페이저와 탭 레이아웃으로 화면을 구성할 때 프래그먼트를 화면 아이템으로 사용한다면 어떤 어댑터를 사용할 수 있나요?

답 : FragmentPagerAdapter(FragmentManager) 해당 메서드 또는 이것을 상속받는 커스텀 어댑터

 

2. 뷰페이저와 탭 레이아웃을 연결할 때 메뉴명을 생성해주는 메서드의 이름은 무엇인가요?

답 : getPageTitle(position: Int): CharSequence?

 

3. 페이저어댑터에서 뷰를 생성하는 메서드는 무엇인가요?

답 : instantialItem(container: ViewGroup, position: Int): Any