안드로이드

[Kotiln/Android] TabLayout과 ViewPager를 이용한 화면 이동

mingmaeng 2019. 12. 18. 22:05

안드로이드 개발함에 있어서 하단 탭은 자주 등장한다. 보통 하단 탭을 이용할 때 BottomNavigation을 이용하였는데, 

좀 더 유연하게 TabLayout과 ViewPager를 통해 여러 Fragment를 이동하는 실습을 해보겠다.

 

TabLayout을 이용할 때 우리는 아이콘뿐만 아니라 밑에 글씨까지 같이 추가하고 싶을 때가 있다.

단순하게 TabLayout에 글자와 아이콘을 추가하게 되면 서로 겹쳐 보이게 되는 불상사가 일어나기 때문에 위에 실습처럼

아이콘과 글씨가 같이 보이게 하고 싶으면 CustomView를 통해 내가 원하는 아이콘을 만들면 된다.

 

주의할 점은 TabLayout을 이용해 위와 같은 화면을 만들고 싶을 때 최소 3개 이상의 화면이 필요하다.

(안드로이드 자체에서 권장하는 최소 개수다.)

 

현재 실습에는 마지노선인 3개의 화면만 있지만 더 많이 만들고 싶으면 그저 화면 개수만 늘리면 되니 그리 어렵지 않다.

천천히 하나하나 따라 하면 굉장히 쉽다는 걸 알 수 있다.


 

TabLayout과 ViewPager 사용하기

우리는 TabLayout을 사용할 것이기 때문에 res/layout에 있는 xml파일로 들어가 TabLayout을 검색해준다.

그러면 사진처럼 TabLayout 옆에 다운로드 마크가 보이게 되는데 이걸 클릭하고 그 뒤에 나오는 확인 창을 눌러주면

자동으로 프로젝트에 TabLayout을 적용시킬 수 있다.

빨간 동그라미를 클릭

다운이 끝나면 TabLayout을 화면에 끌어 놓고 안에 들어 있는 item 태그를 전부 지워주면 다음과 같이 된다.

activity_main.xml
  <com.google.android.material.tabs.TabLayout
        android:id="@+id/main_tablayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:tabGravity="fill"
        app:tabIndicatorColor="@color/colorPrimary"
        app:tabMode="fixed"
        app:tabIconTint="@color/colorPrimary"
        app:tabSelectedTextColor="@color/colorPrimary">

    </com.google.android.material.tabs.TabLayout>

여기서 주요 속성만 몇 개 찾아보자

 

  • app:tabGravitiy : 탭의 정렬 방식에 대한 옵션이다.

    - fill : 탭의 너비를 동일한 간격으로 한다.

    - center : 탭을 가운데로 정렬한다.
  • app:tabMode : 탭의 표시 방식을 결정한다.

    - fixed : 모든 탭이 나오도록 설정한다.

    - scrollable : 탭이 화면 밖을 나갈 경우 스크롤이 되도록 설정한다.
  • app:tabIconTint : 아이콘 색깔을 결정한다. (우리는 customView를 사용하기 때문에 쓰지 않는다.)
  • app:tabSelectedTextColor : 탭이 선택됐을 때 글자 색을 바꿔준다. (이것도 쓰지 않는다.)
  • app:tabIndicatorColor : 탭이 선택됐을 때 인디케이터의 색을 바꿔준다.
  • app:tabIndicatorHeight : 탭 하단의 인디케이터 높이를 지정해준다.

우리는 ViewPager도 쓸 것이기 때문에 ViewPager까지 xml에 추가하고 하단 탭처럼 쓰고 싶기 때문에 TabLayout을

하단에 배치해준다. activity_main.xml의 코드는 다음과 같이 된다.

 

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/main_viewPager"
        android:layout_width="match_parent"
        android:layout_height="622dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/main_tablayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:tabGravity="fill"
        app:tabIndicatorColor="@color/colorPrimary"
        app:tabMode="fixed">

    </com.google.android.material.tabs.TabLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

CustomView 만들기

우리는 글씨와 아이콘을 동시에 쓰기 위해 이제 TabLayout에 들어갈 아이콘을 만들어야 한다.

먼저 아이콘으로 쓸 xml을 하나 만들어 준다.

custom_tab_button.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/tab_logo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:src="@android:drawable/ic_menu_call"/>

    <TextView
        android:id="@+id/tab_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:textSize="10sp"
        android:text="TextView" />
</LinearLayout>

아이콘은 LinearLayout으로 만들어 주었으며, 간단하게 ImageView 하나, 그 밑에 TextView 하나가 있는 구성이다.

text 속성과 src 속성은 아이콘 배치를 위해 설정해 놓은 것이고, 탭 레이아웃에 장착할 때
이미지와 글씨를 바꿔 줄 예정이다.


Fragment 만들기

여러 화면을 보여줄 것이기 때문에 Fragment을 이용하며 화면에 텍스트만 바꿀 것이기 때문에 하나의 Fragment로

여러 개를 만들 것이다.

fragment_tab.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".FragmentTab">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView"
        android:textSize="40sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

FragmentTab.kt
package org.techtown.tabviewpager


import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.fragment_tab.view.*


class FragmentTab : Fragment() {
    var name = ""
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        val view =inflater.inflate(R.layout.fragment_tab, container, false)
        view.textView.text = name

        return view
    }


}

 코틀린은 익스텐션이 가능하기 때문에 id를 그대로 가져와서 사용한다. name에 내가 원하는 문장을 넣고, 해당 문장으로 텍스트 뷰의 문장을 바꿔준다.

 


ViewPager와 연결시킬 Adapter 만들기

 ViewPager를 이용하기 위해서는 관리해 줄 Adapter가 필요하다. FragmentStatePagerAdapter를 상속받은 클래스를 하나 만들어 준다.

PageAdapter.kt
package org.techtown.tabviewpager.Adapter

import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import org.techtown.tabviewpager.FragmentTab
import java.util.concurrent.CountDownLatch

class PageAdapter(fm : FragmentManager) : FragmentStatePagerAdapter(fm){
    private var fragments : ArrayList<FragmentTab> = ArrayList()
    
    override fun getItem(position: Int): Fragment = fragments[position]

    override fun getCount(): Int = fragments.size

    fun addItems(fragment : FragmentTab){
        fragments.add(fragment)
    }
}

 생각보다 코드가 매우 짧기 때문에 하나씩 살펴보자.

private var fragments : ArrayList<FragmentTab> = ArrayList()

뷰 페이저와 연동시킬 fragment들을 모아둔 곳이다. 우리가 이번 실습에서 쓸 fragment들은 똑같은 FragmentTab이기 때문에 파라미터는 FragmentTab이다.

override fun getItem(position: Int): Fragment = fragments[position]

getItem(position : Int) 함수는 position에 위치한 프래그먼트를 반환하는 함수다. 우리는 fragments라는 ArrayList에 담아뒀으므로 position을 인덱스 삼아 반환시켜주면 된다.

 

override fun getCount(): Int = fragments.size

getCount()는 page의 개수를 반환한다. fragments 배열의 크기가 곧 page의 개수가 될 것이다.

 

fun addItems(fragment : FragmentTab){
        fragments.add(fragment)
    }

addItems() 함수를 통해서 fragments 배열에 내가 원하는 fragment를 넣어줄 것이다.

 

Adapter에 대한 설정은 이걸로 끝이다.

모든 준비가 되었으니 이제 MainActivity에서 설정해주면 끝난다.

 


MainActivity 설정

 MainActivity 코드를 보기 전에 먼저 어떤 식으로 설정이 이뤄지는지 간단하게 살펴보자.

  • 먼저 원하는 page 개수만큼 Fragment를 만들어 준다.
  • 뷰 페이저를 관리하는 PageAdapter를 생성한다.
  • PageAdapter 내에 있는 fragments 배열에 우리가 만든 fragment들을 넣어준다.
  • View에 PageAdapter를 장착한다.
  • TabLayout에 ViewPager를 연동시킨다.
  • TabLayout 탭에 CustomView를 통해 원하는 모양으로 View를 넣어준다.

순서대로 보기 전에 먼저 우리가 위에서 만든 custom_tab_button.xml을 이용해 원하는 아이콘을 만드는

함수를 구현해 보자.

class MainActivity : AppCompatActivity() {
    private lateinit var mContext : Context

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

        mContext = applicationContext
        .
        .
        .
       
    }
    
    private fun createView(tabName: String): View {
        var tabView = LayoutInflater.from(mContext).inflate(R.layout.custom_tab_button, null)

        tabView.tab_text.text = tabName
        when (tabName) {
            "찾기" -> {
                tabView.tab_logo.setImageResource(android.R.drawable.ic_menu_search)
                return tabView
            }
            "사진" -> {
                tabView.tab_logo.setImageResource(android.R.drawable.ic_menu_camera)
                return tabView
            }
            "전화" -> {
                tabView.tab_logo.setImageResource(android.R.drawable.ic_menu_call)
                return tabView
            }
            else -> {
                return tabView
            }
        }
    }
    
 }

creatView(tabName : String) 함수의 파라미터로 각 탭의 이름을 받고(tabName) tabName이 무엇이냐에 따라서 그에 맞는 아이콘을 ImageView에 세팅한다. (아이콘은 기본으로 제공하는 아이콘들이다.)

만약 이미지 뷰에 기본 아이콘이 아니라 직접 만든 아이콘을 넣고 싶다면 drawable 폴더에 파일을 넣고, R.drawable.XXXX 식으로 호출하면 된다.

main_tablayout.getTabAt(0)?.setCustomView(createView("찾기"))
main_tablayout.getTabAt(1)?.setCustomView(createView("사진"))
main_tablayout.getTabAt(2)?.setCustomView(createView("전화"))

실제로 TabLayout에 우리가 직접 만든 View를 넣는 코드다. 코드 익스텐션을 통해 좀 더 간결하게 코드를 썼다.

main_tablayout은 activity_main.xml에 있는 TabLayout id값이다. 우리는 CustomView를 쓰기 때문에 getTabAt() 다음에 setCustomView를 쓰면 된다.

 

만약 "나는 그냥 CustomView 쓸 필요 없이 아이콘이나 Text만 있으면 돼!" 하는 사람들은

setIcon()이나 setText()를 사용하면 된다.

 

이제 CustomView 만드는 함수도 구현이 끝났고 본격적인 설정에 들어가 보자.

 

 val searchFragment = FragmentTab()
 searchFragment.name = "찾기 창"
 val cameraFragment = FragmentTab()
 cameraFragment.name = "사진 창"
 val callFragment = FragmentTab()
 callFragment.name = "전화 창"

FragmentTab 클래스의 프래그먼트를 여러 개 만들고 각각의 name에 원하는 text를 집어넣어준다.

 

val adapter = PageAdapter(supportFragmentManager) // PageAdapter 생성
adapter.addItems(searchFragment)
adapter.addItems(cameraFragment)
adapter.addItems(callFragment)

그다음 PageAdapter를 생성하고 우리가 만든 Fragment들을 addItems() 함수를 통해 추가해준다.

 

main_viewPager.adapter = adapter // 뷰페이저에 adapter 장착
main_tablayout.setupWithViewPager(main_viewPager) // 탭레이아웃과 뷰페이저를 연동

코드 익스텐션을 통해 ViewPager에 어댑터를 장착해 주고, TabLayout에 ViewPager를 연동하면 끝이다.

 

main_tablayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener{
override fun onTabReselected(p0: TabLayout.Tab?) {}
override fun onTabUnselected(p0: TabLayout.Tab?) {}
override fun onTabSelected(p0: TabLayout.Tab?) {}
})

이건 tab이 선택되었을 때 리스너를 설정해준 것인데, 해당 실습에서는 굳이 넣을 필요 없다.

간단하게 필요 메서드를 살펴보면 다음과 같다.

 

  • onTabSelected() : 탭이 선택되었을 때 호출
  • onTabUnselected() : 탭이 선택되지 않았을 때 호출
  • onTabReselected() : 탭이 다시 선택되었을 때 호출

마지막으로 메인 전체 코드를 보며 마무리하겠다.

MainActivity.kt
package org.techtown.tabviewpager

import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import com.google.android.material.tabs.TabLayout
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.custom_tab_button.view.*
import org.techtown.tabviewpager.Adapter.PageAdapter

class MainActivity : AppCompatActivity() {
    private lateinit var mContext : Context

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

        mContext = applicationContext
        initViewPager() // 뷰페이저와 어댑터 장착
    }

    private fun createView(tabName: String): View {
        var tabView = LayoutInflater.from(mContext).inflate(R.layout.custom_tab_button, null)

        tabView.tab_text.text = tabName
        when (tabName) {
            "찾기" -> {
                tabView.tab_logo.setImageResource(android.R.drawable.ic_menu_search)
                return tabView
            }
            "사진" -> {
                tabView.tab_logo.setImageResource(android.R.drawable.ic_menu_camera)
                return tabView
            }
            "전화" -> {
                tabView.tab_logo.setImageResource(android.R.drawable.ic_menu_call)
                return tabView
            }
            else -> {
                return tabView
            }
        }
    }

    private fun initViewPager(){
        val searchFragment = FragmentTab()
        searchFragment.name = "찾기 창"
        val cameraFragment = FragmentTab()
        cameraFragment.name = "사진 창"
        val callFragment = FragmentTab()
        callFragment.name = "전화 창"


        val adapter = PageAdapter(supportFragmentManager) // PageAdapter 생성
        adapter.addItems(searchFragment)
        adapter.addItems(cameraFragment)
        adapter.addItems(callFragment)


        main_viewPager.adapter = adapter // 뷰페이저에 adapter 장착
        main_tablayout.setupWithViewPager(main_viewPager) // 탭레이아웃과 뷰페이저를 연동


        main_tablayout.getTabAt(0)?.setCustomView(createView("찾기"))
        main_tablayout.getTabAt(1)?.setCustomView(createView("사진"))
        main_tablayout.getTabAt(2)?.setCustomView(createView("전화"))

//        main_tablayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener{
//            override fun onTabReselected(p0: TabLayout.Tab?) {}
//
//            override fun onTabUnselected(p0: TabLayout.Tab?) {}
//
//            override fun onTabSelected(p0: TabLayout.Tab?) {}
//        })

    }
}

 

글이 길어져서 읽는데 많이 힘들 수도 있지만 너무 급하게 읽지 말고 천천히 순서대로 진행하게 되면 생각보다

무척이나 쉽게 느껴질 것이다. 탭을 통해서 화면 전환은 앱에 있어서 거의 들어간다시피 하기 때문에 알아두면

굉장히 좋은 기능이다. TabLayout을 사용하면 화면의 위치 배치에 따라 상단 탭으로 쓸 수도 있고 하단 탭으로 쓸 수도 있다는 것도 큰 장점 중 하나다.

 

해당 실습 코드는 Git에 올려놓았으니 다운 받아서 참고해보길 바란다.

https://github.com/kangmin1012/AndroidExcercise/tree/TabViewPager