Android

TabLayoutとViewPagerを利用したタブの実装

初めてQiitaに投稿させてもらいます! 今までは多くの方々の記事を見て、たくさん勉強をさせてもらっていましたが、自分も多くの方々の参考になるような記事を書いていけるよう努めますので、よろしくお願いします。

本題ですが、TabLayoutを使用しているときに、ActivityのToolbarのメニューをクリックしてFragmentのメソットを呼びたかったのですが、分かりやすく書いているページがすぐには見つからなかったのでまとめます。
まずは、TabLayoutとViewPagerを使ったシンプルなタブの実装を紹介します。タブの実装を紹介している記事は多くありますが、この記事ではMaterialDesignを考慮したりして作っていこうと思います。次の記事でActivityからFragmentを操作する方法とFragmentからActivityを操作する方法をまとめようと思います。

タブの実装(表示まで)

  • TabLayout ... ActivityのLayout.xmlに記載するユーザーが操作する「タブ」のバーに値するもの。TabLayoutに個々のタブが動的に入ります。
  • ViewPager ... Fragmentがセットされます。左右にスワイプることで、ViewPagerにセットされたFragmentの切り替えが行われます。
  • Fragment ... Activity上で動く、ライフサイクルを持ったViewのようなもの。Activityと同じような使い方ができます。
  • Adapter ... ViewPagerにFragmentをセットします。AdapterをセットしたViewPagerをTabLayoutにセットすることで、タブを選択した時に、それに対応したFragmentに切り替わります。

TabLayout

今回はToolbarとタブが一体化したようなものを作ります。TabLayoutを使うことで細かい設定をすることなく、MaterialDesignに準拠したタブが設定されます。style.xmlでActionbarを非表示にしてToolbarをLayoutで表記し、TabLayoutに影をつけます。ToolbarとTabLayoutの背景色を一致させることが必要です。

MaterialDesignでのToolbarの影(elevation)の高さは4dpです。@dimen/abc_action_bar_elevation_materialも使用できます。
Elevation & shadows - Material Design

values/styles.xml
<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
</resources>
layout/activity_main.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="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="...MainActivity">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?attr/colorPrimary"
        android:minHeight="?attr/actionBarSize"
        android:theme="?attr/actionBarTheme"
        app:title="Tab Sample"
        app:titleTextColor="@color/white"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <android.support.design.widget.TabLayout
            android:id="@+id/tabLayout"
            style="@style/MyCustomTabLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="?attr/colorPrimary"
            android:elevation="4dp" />

        <android.support.v4.view.ViewPager
            android:id="@+id/viewPager"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </LinearLayout>
</LinearLayout>

@style/MyCustomTabLayoutは、タブが選択されている時と選択されていないときでタブのテキストカラーを変えるオリジナルのスタイルです。MaterialDesignでは選択中のタブのテキストカラーが#fff、選択中でないタブのテキストカラーが#fff/不透明度70%となっています。
Tabs - Components - Material Design
ARGBのカラーコード透明度まとめ - Qiita

values/styles.xml
<resources>
    ...

    <style name="MyCustomTabLayout" parent="Widget.Design.TabLayout">
        <item name="tabTextAppearance">@style/MyCustomTabText</item>
        <item name="tabSelectedTextColor">@color/white</item>
    </style>

    <style name="MyCustomTabText" parent="TextAppearance.AppCompat.Button">
        <item name="android:textColor">@color/white_70percent</item>
    </style>
</resources>

Fragment

次にFragmentを作成します。タブを実装する際のFragmentの役割は、ViewPagerにAdapterを介して導入することでユーザーの操作を受け付けることができます。そしてタブの選択が変わるごとに、ViewPagerが表示するFragmentが切り替わるようにするので、タブの数だけFragmentを作る必要があります。(複数のタブが同じような動作をユーザーに要求する場合は1つのFragmentを複数のタブに対応させることでタブの数よりも少ないFragmentで実装することもできます。)FragmentはActivityと同様に、class/javaファイルとlayout/xmlファイルから成ります。そして、Fragmentはライフサイクルを持つので、Activityと同じように使うことができます。ただし、FragmentのライフサイクルはActivityのライフサイクルと異なるので、注意が必要です。

Main1Fragment.java
package ...;

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class Main1Fragment extends Fragment {

    public Main1Fragment() {
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_main1, container, false);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
    }
}
Main2Fragment.java
package ...;

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class Main2Fragment extends Fragment {

    public Main2Fragment() {
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_main2, container, false);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
    }
}

これがFragmentの基本形です。OnCreatedView()メソッドでFragmentのLayoutをインフレートしたViewを返します。また、onViewCreated()メソッドを、ActivityでいうonCreate()メソッドと同じような使い方ができます。super.onViewCreated(view, savedInstanceState);でOverride元の動作をおこなっているので、これ以下に任意のコードを記載します。

ここで、2つのLayoutが区別のつくようにFragmentのLayoutを変更しておきます。今はTextViewのtextのみ変更します。

layout/fragment_main1.xml
<LinearLayout 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"
        android:orientation="vertical"
        android:padding="16dp"
    tools:context="...Main1Fragment">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="fragment1" />

</LinearLayout>
layout/fragment_main2.xml
<LinearLayout 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"
        android:orientation="vertical"
        android:padding="16dp"
    tools:context="...Main2Fragment">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="fragment2" />

</LinearLayout>

Adapter

次にAdapterを作成します。ViewPagerにFragmentを導入するための仕組みがAdapterです。ViewPagerにセットできるAdapterはFragmentPagerAdapterを継承したアダプターです。

OriginalFragmentPagerAdapter.java
package ...;

import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.view.ViewGroup;

public class OriginalFragmentPagerAdapter extends FragmentPagerAdapter {

    private CharSequence[] tabTitles = {"タブ1", "タブ2"};

    public OriginalFragmentPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return tabTitles[position];
    }

    @Override
    public Fragment getItem(int position) {
        switch (position) {
            case 0:
                return new Main1Fragment();
            case 1:
                return new Main2Fragment();
            default:
                return null;
        }
    }

    @Override
    public int getCount() {
        return tabTitles.length;
    }
}

CharSequence型の配列でタブのタイトルを宣言します。
getCount()メソッド:タブの総数を返しています。
getPageTitle()メソッド:タブの総数分のタブのタイトルをタブに反映しています。
getItem()メソッド:positionに対応したFragmentを反映しています。タブを選択するたびに呼ばれます。その時に引数としてposition受け取るので、そのpositionに対応したFragmentを返します。

Activity

最後にActivityの設定です。

MainActivity.java
package ...;

import android.support.design.widget.TabLayout;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

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

        OriginalFragmentPagerAdapter adapter = new OriginalFragmentPagerAdapter(getSupportFragmentManager());
        ViewPager viewPager = findViewById(R.id.viewPager);
        viewPager.setOffscreenPageLimit(2);
        viewPager.setAdapter(adapter);

        TabLayout tabLayout = findViewById(R.id.tabLayout);
        tabLayout.setupWithViewPager(viewPager);
    }
}

OriginalFragmentPagerAdapterを宣言します。FragmentManagerを引数とし、宣言するときはgetSupportFragmentManager()を渡すことでインスタンスを生成できます。ViewPagerをlayoutのidと結びつけ、setAdapter()メソッドでアダプターをセットします。
setOffscreenPageLimit()メソッドはViewPagerのページをユーザーが変更した際に、何ページ前までのFragmentを保持するかを引数として渡します。例えば3つのFragmentを導入したViewPagerでこの値を2に設定すると、1ページから3ページにいき、また1ページに戻った時に1ページ目は再び生成されてしまいます。よって、値などの保持し、再読み込みされないようにする場合には、setOffscreenPageLimit(3)とする必要があります。
そして、TabLayoutはsetupWithViewPager()メソッドで、AdapterがセットされたViewPagerをセットすることで、タブをクリックするとそのタブに対応したFragmentがViewPagerで表示されるようになります。

Fragmentの操作

以上で、TextViewを配置した静的なレイアウトをもったFragmentの表示が行え、ViewPagerやタブを操作することでViewPagerのFragmentが切り替わることが確認できたと思います。
次はFragment内でユーザーの操作を受け取ります。FragmentのLayoutにEditTextとButtonを1つずつ作り、Buttonをクリックした時にEditTextから文字列を受け取り、Toastで表示するプログラムを書いてみましょう。

FragmentのLayoutを変更します。

layout/fragment_main1.xml
<LinearLayout ...>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="fragment1" />

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:inputType="text" />

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button_Fragment1" />
</LinearLayout>

Fragmentのjavaファイルを変更します。

Main1Fragment.java
...

public class Main1Fragment extends Fragment {
    ...

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        final EditText editText = view.findViewById(R.id.editText);
        Button button = view.findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getContext(), editText.getText().toString(), Toast.LENGTH_LONG).show();
            }
        });
    }
}

FragmentのLayoutのViewのidを結びつけるにはonViewCreated()メソッド内でview.findViewById(R.id...)のように表記します。引数のviewはFragmentを作成した際にOnCreatedView()メソッドで返したViewなので、そのLayoutからidを結びつけることができます。
Toastを表示しているところについてですが、makeTextの1つ目の引数ContextをgetContext()としています。これはActivityでMainActivity.thisやthisやgetApplicationContext()などと書いていたところで、FragmentではgetContext()と書くことでそのFragmentの動作している親のActivityを受け取ることができます。

最後に

これで基本的なタブの実装ができます。
次の記事ActivityからFragmentの操作 - Qiitaで、ActivityからFragmentのメソッドを呼ぶ方法などを紹介したいと思います。