안드로이드 탭 만들기. [TabLayout] (Android TabLayout)

2019. 1. 19. 14:18


1. 안드로이드 탭. TabHost, TabWidget, TabSpec

[안드로이드 탭 기본 사용법]에서, 탭호스트(TabHost)와 탭위젯(TabWidget), 그리고 프레임레이아웃(FrameLayout)을 사용하여 안드로이드 탭을 만드는 방법에 대해 살펴보았습니다.


탭 버튼을 표시하는 탭위젯(TabWidget)과 선택된 탭에 따른 페이지를 위한 프레임레이아웃(FrameLayout), 그리고 이 모든 요소들을 관리하며 이들의 컨테이너 역할을 수행하는 탭호스트(TabHost)에 대해 설명했는데요.

TabHost의 구조


그런데, 설명과 그림에서 볼 수 있는 구조의 단순함, 명료함과는 달리, 탭호스트(TabHost)를 사용하는 방법은 그리 간단하지 않습니다. 바로 탭호스트 사용 시, 미리 숙지해야 하는 몇 가지 제약사항들이 존재하기 때문이죠. [안드로이드 탭 기본 사용법 - 2.3 TabHost 사용 제약 사항]에서 설명했듯이, 탭호스트 내 요소들의 배치를 위해 리니어레이아웃(LinearLayout)같은 별도의 뷰그룹을 사용해야 한다던가, 탭위젯(TabWidget)의 id 속성 값이 반드시 "id/tabs" 여야 하는 점, 컨텐츠(Contents) 표시를 위한 프레임레이아웃(FrameLayout)의 id 속성 값 또한 "id/tabcontent"로 고정되어야 하는 등의 제약사항들이 바로 그런 것들입니다.


안드로이드 SDK에서 제공되는 위젯(Widget)들의 사용방법이 모두 간편할 수는 없겠지만, 이러한 제약사항들의 존재는 탭 사용법의 직관성을 해치고, 개발자로 하여금 그 구현 과정이 복잡하다고 느끼게 만드는 요인임에는 틀림없습니다.


그렇다면, 좀 더 간편하게 탭(Tab) 기능을 구현하는 방법은 없을까요? 좀 더 단순하고, 명확하고, 그리고 직관적인 방법.


있습니다. 바로, 탭레이아웃(TabLayout)을 사용하는 것입니다.

2. 탭레이아웃(TabLayout)

탭레이아웃(TabLayout)은 안드로이드에서 탭호스트(TabHost)외에 탭(Tabs) 관련 기능을 구현할 때 사용할 수 있는 또 하나의 방법입니다. 아니, 조금 더 정확하게 표현하자면, 앞에서 소개한 탭호스트(TabHost) 구현의 복잡성을 보완하여, 보다 단순하고 직관적인 방법으로 탭(Tabs) 기능을 구현할 수 있도록, 안드로이드 5.1(Lollipop, API Level 22.2.0) 부터 Design Support Library를 통해 새롭게 추가된 위젯(Widget)입니다.

TabLayout 클래스


2.1 레이아웃 리소스 XML에서 탭레이아웃(TabLayout)에 탭 추가하기.

"단순"과 "직관"이라는 단어로 강조했듯이, 탭레이아웃(TabLayout)에 하나의 탭을 추가하고 화면에 표시하는 방법은 매우 간단합니다. 레이아웃 리소스 XML 작성 시, TabLayoutTabItem을 추가하기만 하면 됩니다. (TabLayout이 Design Support Library를 통해 제공되는 위젯(android.support.design.widget)이라는 사실을 상기하세요.)

<android.support.design.widget.TabLayout
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:id="@+id/tab_layout">

    <android.support.design.widget.TabItem
        android:text="TAB-1"/>
    <android.support.design.widget.TabItem
        android:text="TAB-2"/>
    ...

</android.support.design.widget.TabLayout>

2.2 자바 코드에서 탭레이아웃(TabLayout)에 탭 추가하기.

그리고 자바 코드에서는, TabLayout 클래스의 addTab() 메서드를 통해 새로운 탭을 추가할 수 있습니다.

    TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout) ;
    tabLayout.addTab(tabLayout.newTab().setText("TAB-3")) ;
    tabLayout.addTab(tabLayout.newTab().setText("TAB-4")) ;

그런데 위의 코드에서, addTab() 메서드에 전달된 파라미터가 TabLayout 인스턴스의 newTab() 메서드 호출을 통해 생성된 TabLayout.Tab 인스턴스라는 것을 알아둘 필요가 있습니다. 무슨 말인지 선뜻 이해가 잘 안되시나요? 그럼 위의 코드를 조금 풀어서 작성해보겠습니다.

    TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout) ;

    TabLayout.Tab tab = tabLayout.newTab() ;
    tab.setText("TAB-3") ;
    tabLayout.addTab(tab) ;

레이아웃 리소스 XML에서는 탭 아이템을 추가할 때 TabItem을 사용한다고 설명했습니다. 하지만 자바 코드에서는 TabItem 대신 TabLayout.Tab 클래스를 사용합니다.


TabItem은 조금 특별한 뷰 위젯인데요. 보통의 뷰 위젯들이 리소스 XML과 자바 코드에서 동등한 의미와 방법으로 사용되는데 반해, TabItem은 리소스 XML내에서 탭 아이템을 선언할 때만 사용되는 요소입니다. 즉, XML 내에서 탭 아이템의 텍스트, 아이콘 또는 커스텀 요소 등을 설정할 때만 사용되고, TabLayout 내에 실질적인 탭 아이템을 관리하는 용도로 사용되지는 않는 것이죠.


대신, 앞서 자바 코드에서 확인했듯이, TabLayout의 실질적인 탭 아이템 정보는 TabLayout.Tab 클래스를 통해 추가, 관리됩니다.

2.3 탭 선택 이벤트 처리하기

사용자가, 다른 항목을 보기 위해 탭 버튼을 클릭하여 탭 선택 상태에 변화가 생기면, 리스너를 통해 어떤 탭의 선택 상태가 바뀌었는지 확인할 수 있습니다. 이를 위해 사용하는 리스너는 OnTabSelectedListener이며, TabLayout.addOnTabSelectedListener() 메서드를 사용해 리스너를 설정할 수 있습니다.

    tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
            // TODO : tab의 상태가 선택 상태로 변경.
        }

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {
            // TODO : tab의 상태가 선택되지 않음으로 변경.
        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {
            // TODO : 이미 선택된 tab이 다시 
        }
    })

TabLayout.addOnTabSelectedListener()에는 세 개의 메서드가 선언되어 있는데요. 각 메서드의 역할은 그 이름을 통해 쉽게 유추할 수 있습니다.


메서드 설명
onTabSelected() tab의 상태가 선택되지 않음에서 선택 상태로 변경됨. (주로 활용)
onTabUnselected() tab의 상태가 선택 상태에서 선택되지 않음으로 변경됨.
onTabReselected() 이미 선택된 상태의 tab이 사용자에 의해 다시 선택됨.

2.4 이벤트 핸들러에서 선택 상태가 변경된 탭 식별하기.

OnTabSelectedListener의 각 이벤트 핸들러를 보면, 모든 메서드에 TabLayout.Tab 객체가 전달되는 것을 확인할 수 있습니다. "2.2 자바 코드에서 탭레이아웃(TabLayout)에 탭 추가하기."에서, TabLayout내에서 실질적인 탭 아이템 정보를 TabLayout.Tab을 통해 관리한다고 설명했으니, TabLayout.Tab 객체에 선택된(또는 선택되지 않음으로 변경된) 탭의 정보가 담겨있다는 것을 쉽게 유추할 수 있을 것입니다.


어떤 탭이 선택되었는지 확인하는 방법은, 탭을 사용하는 방식에 따라 개발자가 결정하면 됩니다. 예를 들어 탭의 갯수가 고정되고 그 순서가 변하지 않는다면, 탭의 순서 번호가 중요한 식별자가 될 것입니다. 반면 탭의 갯수 또는 순서가 가변적이라면, 탭에 표시된 텍스트 또는 태그(tag) 값 등이 식별자가 될 수 있겠죠.


다행히, 여러 상황에서도 유연하게 대응할 수 있도록, TabLayout.Tab에는 탭 식별을 위한 몇 가지 메서드가 마련되어 있습니다.


메서드 설명
int getPosition() Tab의 순서를 리턴. (TabLayout에 추가된 순서)
Object getTag() Tab에 지정된 태그(tag) 객체를 리턴. (setTag()로 전달한 객체.)
CharSequence getText() Tab에 표시된 텍스트 문자열 리턴. (setText()로 설정한 텍스트.)
boolean isSelected() Tab 선택 여부 리턴.

아래 코드는 순서에 따라 선택 탭을 식별하는 코드입니다.

    public void onTabSelected(TabLayout.Tab tab) {
        int pos = tab.getPosition() ;
        if (pos == 0) { // 첫 번째 탭 선택.

        }
    }

2.5 탭 선택에 따른 컨텐츠 표시하기.

기본적으로, 탭위젯(TabWidget)과 컨텐츠를 모두 포함하는 탭호스트(TabHost)와 다르게, 탭레이아웃은 탭 선택에 따라 표시될 컨텐츠를 가지지 않습니다. 그래서 탭 선택 이벤트를 탭레이아웃 외부에서 처리할 수 있게 OnTabSelectedListener를 제공하고, 개발자가 이벤트 핸들러를 직접 구현하도록 만들어둔 것이죠.


물론, 컨텐츠 표시를 위한 구현 방법을 오로지 앱 개발자의 몫으로 남긴 것은 아닙니다. [Creating swipe views with tabs]에서와 같이, 뷰페이저(ViewPager)와의 연동을 통해 탭호스트보다 더욱 유연하고 확장성있게 컨텐츠를 표시할 수 있는 방법을 가이드하고 있습니다.


하지만 일단, 뷰페이저와의 통합에 관한 내용은 여기서 당장 다루지 않고, 탭레이아웃의 동작에 대한 설명에 집중하도록 하죠. 뷰페이저와의 연동은 다른 글을 통해 설명하겠습니다.

3. 탭레이아웃(TabLayout)으로 탭 기능 구현하기.

그럼 이제 예제를 통해, 탭레이아웃을 사용하여 탭 기능을 구현하는 방법에 대해 알아보겠습니다.


예제는 아래 그림과 같은 화면으로 구성됩니다. (본문의 예제 화면 구성은 [안드로이드 탭 기본 사용법]에서 작성한 예제와 거의 동일합니다. 하지만 탭호스트 대신 탭레이아웃을 사용한 점이 다릅니다.)

예제 화면 구성


탭 선택에 따른 컨텐츠는 탭레이아웃 외부에 배치한 프레임레이아웃을 통해 표시하도록 만들겠습니다.


참고로, 본문의 예제는 탭레이아웃의 사용법과 동작에 중점을 두었기 때문에, 컨텐츠 표시는 최대한 간단한 방법으로 구현하였습니다. 그러므로 본문의 예제는 탭 동작 이해를 위한 참고 정도로만 활용하시고, 대부분의 경우, 뷰페이저(ViewPager)와 결합하여 유연하고 확장성있는 컨텐츠 표시 방법을 선택하시길 추천합니다.

3.1 메인액티비티 레이아웃 구성.

가장 먼저, 메인액티비티의 레이아웃을 작성합니다. 예제 화면대로 레이아웃 리소스 XML을 작성합니다.

[STEP-1] "content_main.xml" - 메인액티비티의 레이아웃 리소스 XML.
    <android.support.design.widget.TabLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/tabs">

        <android.support.design.widget.TabItem
            android:id="@+id/tabItem1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="TAB-1" />

        <android.support.design.widget.TabItem
            android:id="@+id/tabItem2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="TAB-2" />

        <android.support.design.widget.TabItem
            android:id="@+id/tabItem3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="TAB-3" />

    </android.support.design.widget.TabLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/tabs"
        android:id="@+id/contents">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#4CAF50"
            android:gravity="center"
            android:visibility="visible"
            android:id="@+id/text1"
            android:text="TEXT 1" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#FF9800"
            android:gravity="center"
            android:visibility="invisible"
            android:id="@+id/text2"
            android:text="TEXT 2" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#009688"
            android:gravity="center"
            android:visibility="invisible"
            android:id="@+id/text3"
            android:text="TEXT 3" />

    </FrameLayout>

3.2 탭레이아웃 선택 이벤트 처리.

각 탭이 선택되면, 외부 프레임레이아웃에 선택된 탭에 해당하는 뷰를 표시하도록 만들건데요. 이를 위해, 자바 코드에서 탭레이아웃의 참조를 가져온 다음, addOnTabSelectedListener() 메서드를 사용하여 OnTabSelectedListener 객체를 전달합니다.

[STEP-2] "MainActivity.java" - 탭레이아웃 선택 이벤트 리스너 등록.
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        /// ... 코드 계속

        TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs) ;
        tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                // TODO : process tab selection event.
            }

            @Override
            public void onTabUnselected(TabLayout.Tab tab) {
                // do nothing
            }

            @Override
            public void onTabReselected(TabLayout.Tab tab) {
                // do nothing
            }
        }) ;
    }
}

3.3 선택된 탭의 컨텐츠 표시.

탭이 선택되면, 선택된 탭 순서에 해당하는 컨텐츠를 표시하는 코드를 작성합니다. 예제에서는 텝레이아웃 외부에 프레임레이아웃을 배치한 다음, 프레임레이아웃 내 자식(Children) 뷰를 변경하는 방법을 사용하였습니다.


선택된 탭의 위치는 TabLayout.TabgetPosition() 메서드를 사용하여 얻어올 수 있습니다.


[STEP-3] "MainActivity.java" - 선택된 탭의 컨텐츠 표시.
public class MainActivity extends AppCompatActivity {

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

        TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs) ;
        tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                int pos = tab.getPosition() ;
                changeView(pos) ;
            }

            /// ... 코드 생략
        }) ;
    }

    private void changeView(int index) {
        TextView textView1 = (TextView) findViewById(R.id.text1) ;
        TextView textView2 = (TextView) findViewById(R.id.text2) ;
        TextView textView3 = (TextView) findViewById(R.id.text3) ;

        switch (index) {
            case 0 :
                textView1.setVisibility(View.VISIBLE) ;
                textView2.setVisibility(View.INVISIBLE) ;
                textView3.setVisibility(View.INVISIBLE) ;
                break ;
            case 1 :
                textView1.setVisibility(View.INVISIBLE) ;
                textView2.setVisibility(View.VISIBLE) ;
                textView3.setVisibility(View.INVISIBLE) ;
                break ;
            case 2 :
                textView1.setVisibility(View.INVISIBLE) ;
                textView2.setVisibility(View.INVISIBLE) ;
                textView3.setVisibility(View.VISIBLE) ;
                break ;

        }
    }
}

4. 예제 실행 화면

예제를 빌드하고 실행하면, 아래와 같은 화면이 표시됩니다.

예제 실행 화면1


그리고 화면의 탭을 선택하면, 선택된 탭의 순서에 따라 프레임레이아웃에 표시되는 컨텐츠가 변경되는 것을 확인할 수 있습니다.

예제 실행 화면2


5. 참고

.END.


ANDROID 프로그래밍/TAB