Androidアプリ開発 ViewPager レイアウトをxmlで生成する

2017年07月02日(編集2017年07月02日)
このエントリーをはてなブックマークに追加

Androidアプリ開発でViewPagerを使うと、スワイプで画面を切り替えるページを作成することができます。
この記事は、AndroidアプリでViewPagerの実装方法を記載した記事です。

環境はAndroid 8.0 (API level 26) です。

環境

  • OS X El Capitan
  • android sdk 26
  • Oracle jdk version 1.8.0_72
  • Android Studio 2.3.3
  • kotlin 1.1.1

難易度

初心者向け

サンプルコード

Android-UI-Demo(Java)
Android-UI-Demo(Kotlin)

動画

記事の内容を実装したアプリケーションの動画です。

ViewPager

ViewPagerを使うと、スワイプで画面を切り替えるページを作成することができます。
Material Design以降のUI設計ではあまり利用されなくなりましたが、チュートリアル等の作成でまだ利用されます。

FragmentPagerAdapterとの違い

FragmentPagerAdapterとViewPagerの違いは、Viewを使うかFragmentを使うかです。FragmentPagerAdapterのソースコードを読むとわかりますが、FragmentPagerAdapterはViewPagerのサブクラスです。
Fragmentを使う必要がなければ、ViewPagerを使うほうが良いでしょう。ただし、ViewPagerはwrap_contentが使えないので注意してください。

アプリ設計

この記事のサンプルでは、ViewPagerのviewをxmlで生成し、indicatorライブラリを使ってページ数を表示する画面を作成します。

fig1. ViewPager

kotlin

この記事のサンプルは、Javaとkotlinの両方で実装します。

kotlinをAndroid Studio3未満で利用するには、{project_folder}/build.gradleに次のように記述します。

{project_folder}/build.gradle
buildscript {
    ext.kotlin_version = '1.1.1'
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 
    }
}
          

kotlinのversionを最新でない1.1.1に指定しているのは、jackOptions(Java8)に対応するためです。

続いてapp/build.gradleにも追記します。

{project_folder}/app/build.gradle
apply plugin: 'kotlin-android'

dependencies {
    // kotlin
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
}
repositories {
    mavenCentral()
}
          

以上で準備は完了です。

実装

ViewPagerFragmentを実装します。

{project_folder}/app/src/main/java/ViewPagerFragment.java
public class ViewPagerFragment extends Fragment {

    public static final String TAG = "ViewPagerFragment";

    /**
     * Use this factory method to create a new instance of
     * this fragment using the provided parameters.
     *
     * @return A new instance of fragment ViewPagerFragment.
     */
    public static ViewPagerFragment newInstance() {
        return new ViewPagerFragment();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_view_pager, container, false);
        ViewPager viewPager = view.findViewById(R.id.viewPager);
        CustomPagerAdapter pagerAdapter = new CustomPagerAdapter(getResources().getConfiguration().orientation);
        viewPager.setAdapter(pagerAdapter);
        PageIndicatorView pageIndicatorView = view.findViewById(R.id.pageIndicatorView);
        pageIndicatorView.setViewPager(viewPager);
        return view;
    }

    private static class CustomPagerAdapter extends PagerAdapter {
        private int orientation;

        private CustomPagerAdapter(int orientation) {
            this.orientation = orientation;
        }

        public Object instantiateItem(ViewGroup container, int position) {
            final View view = LayoutInflater.from(container.getContext()).inflate(R.layout.view_page, container, false);

            TextView title = view.findViewById(R.id.title);
            title.setText(" page title : " + (position + 1));

            TextView summary = view.findViewById(R.id.summary);
            summary.setText(" page summary : This is page  " + (position + 1));

            TextView orientation = view.findViewById(R.id.orientation);
            orientation.setText(Configuration.ORIENTATION_PORTRAIT == this.orientation ? "PORTRAIT" : "LANDSCAPE");

            container.addView(view);

            return view;
        }

        @Override
        public int getCount() {
            int pageCount = 0;
            if (this.orientation == Configuration.ORIENTATION_PORTRAIT) {
                pageCount = 3;
            } else if (this.orientation == Configuration.ORIENTATION_LANDSCAPE) {
                pageCount = 5;
            }
            return pageCount;
        }

        // ページを構成するViewの判定
         @Override
        public boolean isViewFromObject(View view, Object object) {
            return view.equals(object);
        }

        // ページの破棄を行う
        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
        }

        @Override
        public int getItemPosition(Object object) {
            return POSITION_UNCHANGED;
        }
    }
}
          

kotlinの場合は以下のように実装します。

{project_folder}/app/src/main/kotlin/KtViewPagerFragment.java
class KtViewPagerFragment : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        // Inflate the layout for this fragment
        val view = inflater!!.inflate(R.layout.fragment_kt_view_pager, container, false)
        val viewPager = view.findViewById<ViewPager>(R.id.viewPager)
        val pagerAdapter = CustomPagerAdapter(resources.configuration.orientation)
        viewPager.adapter = pagerAdapter
        val pageIndicatorView = view.findViewById<PageIndicatorView>(R.id.pageIndicatorView)
        pageIndicatorView.setViewPager(viewPager)
        return view
    }

    private class CustomPagerAdapter(private val orientation: Int) : PagerAdapter() {

        override fun instantiateItem(container: ViewGroup, position: Int): Any {
            val view = LayoutInflater.from(container.context).inflate(R.layout.view_page, container, false)

            val title = view.findViewById<TextView>(R.id.title)
            title.text = " page title : $position"

            val summary = view.findViewById<TextView>(R.id.summary)
            summary.text = " page summary : This is page $position"

            val orientation = view.findViewById<TextView>(R.id.orientation)
            orientation.text = if (Configuration.ORIENTATION_PORTRAIT == this.orientation) "PORTRAIT" else "LANDSCAPE"

            container.addView(view)

            return view
        }

        override fun getCount(): Int {
            return when {
                this.orientation == Configuration.ORIENTATION_PORTRAIT -> 3
                this.orientation == Configuration.ORIENTATION_LANDSCAPE -> 5
                else -> 0
            }
        }

        // ページを構成するViewの判定
        override fun isViewFromObject(view: View, `object`: Any): Boolean {
            // Object 内に View が存在するか判定する
            return view == `object`
        }

        // ページの破棄を行う。
        override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
            container.removeView(`object` as View)
        }

        override fun getItemPosition(`object`: Any?): Int {
            return PagerAdapter.POSITION_UNCHANGED
        }
    }

    companion object {
        fun newInstance(): KtViewPagerFragment {
            return KtViewPagerFragment()
        }
    }
}
          

kotlinだと20行ほどコードが減ります。

■ 実装の解説

1. レイアウト生成

レイアウトの生成は、xmlで行います。PagerAdapterを継承したCustomPagerAdapterクラスのinstantiateItemメソッドでxmlをinflateします。

final View view = LayoutInflater.from(container.getContext()).inflate(R.layout.view_page, container, false);
        

LayoutInflater.fromメソッドでLayoutInflaterクラスを取得します。fromメソッドは以下のように実装されています。

public static LayoutInflater from(Context context) {
    LayoutInflater LayoutInflater =
            (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    if (LayoutInflater == null) {
        throw new AssertionError("LayoutInflater not found.");
    }
    return LayoutInflater;
}
        

LayoutInflaterクラスを取得したら、inflateメソッドでviewを取得します。第3引数のattachToRootをfalseにして、xmlで取得するviewをrootにします。
attachToRootをtrueにするとrootにaddViewされるので注意してください。

// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
    root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
    result = temp;
}
        

layoutのviewの生成ができたら、findViewByIdメソッドで必要なオブジェクトを取り出しデータを設定します。

2. view追加

viewの生成と設定を終えたら、生成したviewをViewGroupに追加して、returnでviewを返します。

container.addView(view);
return view;
        

3. destroyItem

viewを削除するとき、destroyItemメソッドが呼び出されます。destroyItemメソッドは、画面とその両端以外のページを削除します。例えば3ページ目を表示した場合、1ページ目がdestroyItemメソッドで削除されます。

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    container.removeView((View) object);
}
        

4. isViewFromObject

isViewFromObjectメソッドは、フォーカスや表示の調整のために頻繁に呼び出されます。viewの比較メソッドを記述します。

@Override
public boolean isViewFromObject(View view, Object object) {
    return view.equals(object);
}
        

5. indicator

ページindicatorを表示したい場合は、ライブラリを使います。Google純正では用意されていません。このサンプルでは、PageIndicatorViewを使います。
build.gradleに以下の追記をします。

ext {
    pageindicatorviewVersion = "0.1.2"
}
          

app/build.gradleに以下の追記をします。

dependencies {
    compile 'com.romandanylyk:pageindicatorview:' + rootProject.pageindicatorviewVersion
    compile 'com.romandanylyk:pageindicatorview:' + rootProject.pageindicatorviewVersion + '@aar'
}
        

インストールしたら、レイアウトxmlとコードを実装します。

xmlを実装します。

    <com.rd.PageIndicatorView
        android:id="@+id/pageIndicatorView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        attrs:layout_constraintBottom_toBottomOf="parent"
        attrs:layout_constraintLeft_toLeftOf="parent"
        attrs:layout_constraintRight_toRightOf="parent"
        attrs:piv_padding="12dp"
        attrs:piv_radius="8dp"
        attrs:piv_selectedColor="@color/colorAccent"
        attrs:piv_unselectedColor="@color/colorGray"
        attrs:piv_viewPager="@+id/viewPager" />
          

Javaを実装します。

PageIndicatorView pageIndicatorView = view.findViewById(R.id.pageIndicatorView);
pageIndicatorView.setViewPager(viewPager);
        

kotlinで実装します。

val pageIndicatorView = view.findViewById<PageIndicatorView>(R.id.pageIndicatorView)
pageIndicatorView.setViewPager(viewPager)
        

以上で実装は完了です。

ビルド

上記のコードの動作を確認してみましょう。

fig2. ViewPager(kotlin)

ページビューが表示されました。

結論

ViewPagerは枯れた技術ですが、今後も利用するケースは出てくると思います。
とはいえ実装する前に、UIとしてViewPagerを使う必要が本当にあるか考えるようにしましょう。

関連記事

タグ検索で調べてみよう

UI Android8.0 kotlin1.1.1