EDIT MODE

モバイルアプリ開発に関することを書いています

Design Support Library v22.2.0について Part 3

Part 1ではTextInputLayoutとFloatingActionButtonについて、Part 2ではSnackbarとCoordinatorLayoutについて調べたことを、それぞれ書きました。

今回は、TabLayoutについて書いていきます。

TabLayoutの概要

リファレンスを確認すると、TabLayoutクラスはHorizontalScrollViewクラスを継承していました。 API Level 20まで使われていたActionBar.Tabなどとは、関連がないようです。 Tabの生成にはTabLayout.TabクラスnewTab()メソッドを使います。

MaterialDesignにおけるTabの使い方は、デザインガイドラインで細かく指定されています。 このクラスの見た目や振る舞いは、ガイドラインに沿った形で実装されているようです。

基本的な使い方

とりあえずTabを画面に配置するために、各Tabのコンテンツは一旦無視して、以下のように実装してみました。

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

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

</RelativeLayout>
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
        tabLayout.addTab(tabLayout.newTab().setText("tab 1"));
        tabLayout.addTab(tabLayout.newTab().setText("tab 2"));
        tabLayout.addTab(tabLayout.newTab().setText("tab 3"));
    }
}

f:id:androhi:20150617014729p:plain

やっていることは、newTab()メソッドでタブを生成してsetText()メソッドでタブのタイトルを設定しているだけですが、デフォルトでも以下のような点が実現出来てました。

  • タブのタイトルが自動でAllCaps
  • タブのインジケーターがアニメーションする
  • カレントでないタブのタイトル色が薄くなる

またsetIcon()メソッドでアイコンをタブに表示することも出来ます。アイコンサイズは24dpです。

tabLayout.addTab(tabLayout.newTab().setIcon(R.drawable.ic_event_grey_600_24dp));
tabLayout.addTab(tabLayout.newTab().setIcon(R.drawable.ic_devices_grey_600_24dp));
tabLayout.addTab(tabLayout.newTab().setIcon(R.drawable.ic_directions_subway_grey_600_24dp));

f:id:androhi:20150617014754p:plain

ただし、setText()setIcon()を両方使うと横並びに表示されてしまい、ガイドラインTabs with icons and textとは異なってしまいました。この場合は、setCustomView()メソッドガイドラインに沿うよう、補うしかなさそうです。

f:id:androhi:20150617014814p:plain

ModeとGravity

ガイドラインを見ると、Tabには以下のような2つのModeと2種類の見た目について、記述されています。

  • Mode : Fixed / Scrollable
  • Gravity : Center / Fill

それぞれsetTabMode()メソッドsetTabGravity()メソッドで指定出来ました。

1. Fixed & Center

f:id:androhi:20150617014835p:plain

2. Fixed & Fill

f:id:androhi:20150617014858p:plain

3. Scrollable & Fill

f:id:androhi:20150617014921p:plain

4. Scrollable & Center

f:id:androhi:20150617014936p:plain

ViewPagerとの併用

リファレンスにも書かれているように、TabLayoutクラスはViewPagerと併用するためのインターフェースやメソッドが用意されています。 個人的には、これがTabLayoutクラスを使う醍醐味だと感じました。

セットする方法は2パターン用意されていて、簡単に言うとマニュアル方式とオートマチック方式です。 マニュアル方式だと、以下の3ステップで設定します。

  1. TabLayout#setTabsFromPagerAdapter(PagerAdapter)メソッドでPagerAdapterをTabLayoutにセットする
  2. TabLayout.TabLayoutOnPageChangeListenerインターフェースでTabの切り替えイベントをViewPagerにリンクする
  3. TabLayout.ViewPagerOnTabSelectedListenerインターフェースでViewPagerの切り替えイベントをTabLayoutにリンクする

オートマチック方式では、上記3ステップと同等の処理を1つのメソッド(TabLayout#setupWithViewPager(ViewPager))の内部で、全てやってくれるそうです。

ちなみにどちらの方式も、タブタイトルは内部でPagerAdapter#getPageTitle(int)メソッドから取ってきたものを、セットするとのことです。

これらを踏まえてコードで書くと、以下のようになりました。

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.design.widget.TabLayout
        android:id="@+id/tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        />
    
    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/tabs"
        />

</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/page_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:textSize="32sp"
        />

</RelativeLayout>
public class MainActivity extends AppCompatActivity implements ViewPager.OnPageChangeListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
        ViewPager viewPager = (ViewPager) findViewById(R.id.pager);

        FragmentPagerAdapter adapter = new FragmentPagerAdapter(getSupportFragmentManager()) {
            @Override
            public Fragment getItem(int position) {
                return TestFragment.newInstance(position + 1);
            }

            @Override
            public CharSequence getPageTitle(int position) {
                return "tab " + (position + 1);
            }

            @Override
            public int getCount() {
                return 3;
            }
        };

        viewPager.setAdapter(adapter);
        viewPager.addOnPageChangeListener(this);

        //オートマチック方式: これだけで両方syncする
        tabLayout.setupWithViewPager(viewPager);

        //マニュアル方式: これでViewPagerのPositionとTabのPositionをsyncさせるらしい
        //tabLayout.setTabsFromPagerAdapter(adapter);
        //viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
        //tabLayout.setOnTabSelectedListener(new TabLayout.ViewPagerOnTabSelectedListener(viewPager));

    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

    }

    @Override
    public void onPageSelected(int position) {
        Log.d("MainActivity", "onPageSelected() position="+position);
    }

    @Override
    public void onPageScrollStateChanged(int state) {

    }

    public static class TestFragment extends Fragment {

        public TestFragment() {
        }

        public static TestFragment newInstance(int page) {
            Bundle args = new Bundle();
            args.putInt("page", page);
            TestFragment fragment = new TestFragment();
            fragment.setArguments(args);
            return fragment;
        }

        @Nullable
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            int page = getArguments().getInt("page", 0);
            View view = inflater.inflate(R.layout.fragment_test, container, false);
            ((TextView) view.findViewById(R.id.page_text)).setText("Page " + page);
            return view;
        }
    }
}

f:id:androhi:20150617015000p:plain

もしタブタイトルにアイコンを設定したい場合は、getPageTitle()でnullなどを返してTabLayoutにViewPagerをセットした後で、setIcon()を使えば良さそうです。

先程のコードを、以下のように書き換えました。(変更がない箇所は省略しています)

...
@Override
public CharSequence getPageTitle(int position) {
    return null;
}

...
        
//オートマチック方式: これだけで両方syncする
tabLayout.setupWithViewPager(viewPager);

//アイコンセット
tabLayout.getTabAt(0).setIcon(R.drawable.ic_devices_grey_600_24dp);
tabLayout.getTabAt(1).setIcon(R.drawable.ic_directions_subway_grey_600_24dp);
tabLayout.getTabAt(2).setIcon(R.drawable.ic_event_grey_600_24dp);
...

f:id:androhi:20150617015019p:plain

ブランドカラーの設定

以下のようにブランドカラーを設定する場合は、ほぼToolbarと同じ設定の仕方でした。

DarkかLiteかは親のテーマを引き継ぎますし、背景色はandroid:background=?attr/colorPrimaryの記述が必要です。 インジケーターには特に何も指定しなくてもcolorAccentが適用されるようです。

もしタブタイトルのテキスト色を変えたい場合は、setTabTextColors(ColorStateList)メソッドsetTabTextColors(int, int)メソッドを使って変更出来ます。

<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
    <item name="colorPrimary">#3f51b5</item>
    <item name="colorPrimaryDark">#303f9f</item>
    <item name="colorAccent">#ff4081</item>
</style>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        />

    <android.support.design.widget.TabLayout
        android:id="@+id/tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/toolbar"
        android:background="?attr/colorPrimary"
        />

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/tabs"
        />

</RelativeLayout>

f:id:androhi:20150617015046p:plain

まとめ

TabLayoutいいクラスですね。タブの実装が、かなり楽になりそうだなと思いました。 特にViewPagerと使うことを十分に考慮してあるのが、とても助かります。

ただタブタイトルを自動でgetPageTitle()からセット出来たりするとこまでやってくれるのであれば、いっそTabPagerViewクラスとか作ってしまってsetTabEnable(boolean)メソッドとかでタブが生えたり消えたり出来たら良かったなーとも思いました。

とはいえ、ほとんど場合は今まで通りViewPagerを生成して、TabLayout#setupWithViewPager(PagerAdapter)を呼べば済みそうなので、本当に便利なクラスだと思います。