Android Tips #30 ViewPager を使ってスワイプで View を切り替える

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

今回はスワイプで View を切り替えることができる ViewPager を使ってみたいと思います。 SupportPackage に含まれているので Android 1.6 から使うことができます! もう当たり前のように知っていて、よく使われているかたはかなり多いと思いますが、、知らないかたは、かなり便利なのでこの機会にぜひ覚えましょう!

view_pager03

まずは ViewPager を使ってみる

1. PageAdapter のカスタムクラスをつくる

まずは PageAdapter を実装したカスタムクラスをつくります。以下のメソッドが最低限必要です。

isViewFromObject() Object に View が含まれているか判定する。
getCount() ViewPager に登録する全アイテム数を返す。
instantiateItem() アイテムを追加するときに呼ばれる。このメソッド内で View をコンテナに追加する。
destroyItem() アイテムを削除するときに呼ばれる。このメソッド内で View の削除をおこなう。

PageAdapter では基本的にアイテムは Object として取り扱います。instantiateItem() で返すオブジェクトは Object ですし、 destroyItem() で渡される値も Object です。実際は ViewGroup か View が渡される感じになると思います。
isViewFromObject() は頻繁に呼ばれるメソッドで、 Object の中に View が含まれているか判定するためのメソッドです。Object が ViewGroup の場合は 対象の View まで掘り下げて判定してあげる必要があります。Object が 単純な View であれば Object#equals() の判定で良いと思います。
getCount() は ViewPager で表示したいアイテム数を返します。アイテムの情報を Object として持ちたい場合は ArrayList をつくって size() を渡すようにします。
以下の例では色のリストをセットでき、アイテムの色がテキスト色になる TextView を表示する PagerAdapter をつくってみました。 add() メソッドをつくり、外からアイテムを設定できるようにしました。

package jp.classmethod.android.sample.viewpager;

import java.util.ArrayList;

import android.content.Context;
import android.support.v4.view.PagerAdapter;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

/**
 * カスタム PagerAdapter クラス.
 */
public class CustomPagerAdapter extends PagerAdapter {

    /** コンテキスト. */
    private Context mContext;
    
    /** リスト. */
    private ArrayList<Integer> mList;

    /**
     * コンストラクタ.
     */
    public CustomPagerAdapter(Context context) {
        mContext = context;
        mList = new ArrayList<Integer>();
    }

    /**
     * リストにアイテムを追加する.
     * @param item アイテム
     */
    public void add(Integer item) {
        mList.add(item);
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {

        // リストから取得
        Integer item = mList.get(position);

        // View を生成
        TextView textView = new TextView(mContext);
        textView.setText("Page:" + position);
        textView.setTextSize(30);
        textView.setTextColor(item);
        textView.setGravity(Gravity.CENTER);

        // コンテナに追加
        container.addView(textView);

        return textView;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        // コンテナから View を削除
        container.removeView((View) object);
    }

    @Override
    public int getCount() {
        // リストのアイテム数を返す
        return mList.size();
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        // Object 内に View が存在するか判定する
        return view == (TextView) object;
    }

}

2. ViewPager に Adapter をセットする

あとは ViewPager をレイアウトに追加し setAdapter() で先ほどつくったカスタム PagerAdapter をインスタンス化しセットするだけです。先ほどのクラスでは add() メソッドを用意しておいたので、それを使ってアイテムを追加します。

package jp.classmethod.android.sample.viewpager;

import android.app.Activity;
import android.graphics.Color;
import android.os.Bundle;
import android.support.v4.view.ViewPager;

/**
 * ViewPager を表示するサンプル.
 */
public class PagerActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // カスタム PagerAdapter を生成
        CustomPagerAdapter adapter = new CustomPagerAdapter(this);
        adapter.add(Color.BLACK);
        adapter.add(Color.RED);
        adapter.add(Color.GREEN);
        adapter.add(Color.BLUE);
        adapter.add(Color.CYAN);
        adapter.add(Color.MAGENTA);
        adapter.add(Color.YELLOW);

        // ViewPager を生成
        ViewPager viewPager = new ViewPager(this);
        viewPager.setAdapter(adapter);
        
        // レイアウトにセット
        setContentView(viewPager);
    }
    
}

これで完成です!スワイプするとページが切り替わります!

view_pager01

応用編1 : ボタンでページを切り替える・ページを動的に追加する

応用編ということで、戻る/進むボタンを置いてページを切り替えるようにして、追加ボタンでページを追加できるようにしてみました!
ページの切り替えは ViewPager#setCurrentItem() でおこないます。ViewPager#getCurrentItem() で現在表示されているアイテムの Index を取得し、それを足したり引いたりしています。リストの範囲外になっても Exception などは起きないので、よくある Index の調整は不要です。
ページを動的に追加するのは先ほどのカスタム PagerAdapter で実装した add() メソッドを使うと簡単におこなえます。

PagerControlActivity.java

package jp.classmethod.android.sample.viewpager;

import android.app.Activity;
import android.graphics.Color;
import android.os.Bundle;
import android.support.v4.view.ViewPager;
import android.view.View;
import android.view.View.OnClickListener;

/**
 * ViewPager をコントロールする機能を追加するサンプル.
 */
public class PagerControlActivity extends Activity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        setContentView(R.layout.activity_pager_control);

        // カスタム PagerAdapter を生成
        CustomPagerAdapter adapter = new CustomPagerAdapter(this);
        adapter.add(Color.BLACK);
        adapter.add(Color.RED);
        adapter.add(Color.GREEN);
        adapter.add(Color.BLUE);
        adapter.add(Color.CYAN);
        adapter.add(Color.MAGENTA);
        adapter.add(Color.YELLOW);

        // ViewPager を生成
        ViewPager viewPager = (ViewPager) findViewById(R.id.view_pager);
        viewPager.setAdapter(adapter);
        
        // 戻るボタン
        findViewById(R.id.preview_button).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                ViewPager viewPager = (ViewPager) findViewById(R.id.view_pager);
                viewPager.setCurrentItem(viewPager.getCurrentItem() - 1);
            }
        });
        // 進むボタン
        findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                ViewPager viewPager = (ViewPager) findViewById(R.id.view_pager);
                viewPager.setCurrentItem(viewPager.getCurrentItem() + 1);
            }
        });
        // 追加ボタン
        findViewById(R.id.add_button).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                ViewPager viewPager = (ViewPager) findViewById(R.id.view_pager);
                CustomPagerAdapter adapter = (CustomPagerAdapter) viewPager.getAdapter();
                // ランダムに色を追加する
                int R = (int)( Math.random() * 256);
                int G = (int)( Math.random() * 256);
                int B = (int)( Math.random() * 256);
                adapter.add(Color.rgb(R, G, B));
                adapter.notifyDataSetChanged();
            }
        });
        
    }

}

activity_pager_control.xml

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

    <android.support.v4.view.ViewPager
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        >
        
        <Button 
            android:id="@+id/preview_button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="PREVIEW"
            />
        
        <Button 
            android:id="@+id/add_button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="ADD"
            />
        
        <Button 
            android:id="@+id/next_button"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="NEXT"
            />
        
    </LinearLayout>

</RelativeLayout>

view_pager02

応用編2 : 画像ギャラリーをつくってみる

最後に、さらなる応用編として端末内の画像を読み込んで表示するギャラリーをつくってみました。基本は上記までの方法と一緒です。 Cursor の読み込みはこれまた SupportPackage にある CursorLoader を使って非同期で読み込んでみました。AsyncTaskLoader の具体的な解説はこちらに載っているので参考にしてください。

ImagePagerAdapter.java

package jp.classmethod.android.sample.viewpager;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;

import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.v4.view.PagerAdapter;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;

/**
 * 画像を表示する PagerAdapter.
 */
public class ImagePagerAdapter extends PagerAdapter {

    /** コンテキスト. */
    private Context mContext;

    /** ContentResolver. */
    private ContentResolver mResolver;

    /** ID のリスト. */
    private ArrayList<Long> mList;

    /**
     * コンストラクタ.
     * @param context {@link Context}
     */
    public ImagePagerAdapter(Context context) {
        mContext = context;
        mResolver = mContext.getContentResolver();
        mList = new ArrayList<Long>();
    }

    /**
     * アイテムを追加する.
     * @param id ID
     */
    public void add(Long id) {
        mList.add(id);
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {

        // リストから取得
        Long id = mList.get(position);
        Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id.toString());
        Bitmap bitmap = null;
        try {
            bitmap = getBitmap(uri);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // View を生成
        ImageView imageView = new ImageView(mContext);
        imageView.setImageBitmap(bitmap);

        // コンテナに追加
        container.addView(imageView);

        return imageView;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        // コンテナから View を削除
        container.removeView((View) object);
    }

    @Override
    public int getCount() {
        return mList.size();
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view.equals(object);
    }
    
    /**
     * Bitmap を取得する.
     * @param imageUri 画像の URI
     * @return Bitmap
     * @throws IOException
     */
    public Bitmap getBitmap(Uri imageUri) throws IOException {  
        BitmapFactory.Options mOptions = new BitmapFactory.Options();  
        mOptions.inSampleSize = 10;  
        Bitmap resizeBitmap = null;  
      
        InputStream is = mResolver.openInputStream(imageUri);  
        resizeBitmap = BitmapFactory.decodeStream(is, null, mOptions);  
        is.close();
      
        return resizeBitmap;  
    }  

}

ImagePagerActivity.java

package jp.classmethod.android.sample.viewpager;

import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.view.ViewPager;

/**
 * 画像を使った ViewPager のサンプル.
 */
public class ImagePagerActivity extends FragmentActivity {
    
    /** ViewPager. */
    private ViewPager mPager;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // ViewPager をレイアウトにセット
        mPager = new ViewPager(this);
        setContentView(mPager);
        // CursorLoader を呼び出す
        getSupportLoaderManager().initLoader(0, null, callbacks);
    }
    
    /** CursorLoader のコールバック. */
    private LoaderCallbacks<Cursor> callbacks = new LoaderCallbacks<Cursor>() {
        
        @Override
        public Loader<Cursor> onCreateLoader(int id, Bundle bundle) {
            Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
            return new CursorLoader(getApplicationContext(), uri, null, null, null, null);
        }
        
        @Override
        public void onLoaderReset(Loader<Cursor> loader) {
        }
        
        @Override
        public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
            // Cursor から id を取得して PagerAdapter に入れる
            ImagePagerAdapter adapter = new ImagePagerAdapter(ImagePagerActivity.this);
            c.moveToFirst();
            do {
                long id = c.getLong(c.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID));
                adapter.add(id);
            } while (c.moveToNext());
            // ViewPager にセット
            mPager.setAdapter(adapter);
        }
        
    };

}

view_pager03

ソースコード

ソースコードを github に公開しました!ぜひ実装時の参考にしてください!

suwa-yuki/ViewPagerSample

まとめ

今回は ViewPager の基本と、少しだけですが応用してみました。画像ギャラリーは本来であればもう少し調整すべき点は(いくつも)ありますが、基本的な実装はこれだけで済むので非常に楽ですね!用途はさまざまありますし Android 1.6 から使えるのでかなり有用です。まずは使ってみてください!

参考

  • Hideai Minami

    素晴らしいサンプルありがとうございます!

    いつも感心しております。

    ちょっと作ってみたところ、どうも追加ボタンの処理時にエラー落ちしてしまいます。
    ランダムカラーセット後にadapterのチェンジを実施したところ、うまくいきました。
    adapter.add(Color.rgb(R, G, B));
    adapter.notifyDataSetChanged();

    私だけかな?

    • suwa.yuki

      いつもコメントいただきありがとうございます!

      少し調べましたが、どうやら ADT 22 以降の PagerAdapter では、アイテムの追加や削除を行った場合、 getCount() が呼び出される前に notifyDataSetChanged() を呼ばないと例外が発生するようになったみたいです。

      http://stackoverflow.com/questions/16756131/fragmentstatepageradapter-stopped-working-after-updating-to-adt-22

      notifyDataSetChanged()を実行するよう、記事内のコードとGitHubのサンプルを更新しました。

      こういったご報告は非常に助かります。ありがとうございました!