View on GitHub
mixi-inc/AndroidTraining
ListViewとViewPager
この章では、ListView と ViewPager の2つの特殊な View について解説します。

参考:Building Layouts with an Adapter | Android Developers
参考:Making ListView Scrolling Smooth | Android Developers
参考:PagerAdapter | Android Developers
参考:FragmentPagerAdapter | Android Developers

目次

ListView

ListView は、縦にスクロールする一覧表示のための View です。

ListView 自身には、一覧の中身を管理する機能はありません。
代わりに、Adapter という仕組みを用いて、データソースの管理と、データの View へのバインドをさせ、ListView は、スクロール位置に合わせて必要な View を Adapter から取り出すことをします。

リストデータは、List インタフェースを実装したデータソースや、或いは、データベースへ問い合わせた結果のデータソースである場合もあります。

ListAdapter

データをバインドして表示する際には、 ListAdapter インタフェースを実装した Adapter を使用します。
Adapter にはいくつかの種類が提供されています。以下はその一部となります。

名前 役割
BaseAdapter ListView と Spinner で使用可能な共通基底クラスです。 ListView では ListAdapter 用の interface を実装します
ArrayAdapter 配列やリストデータソースをバインドする際に使用します
CursorAdapter データベースへ問い合わせた結果をバインドする際に使用します
SimpleCursorAdapter データベースへ問い合わせた結果を View へ容易にバインドできるようにしたCursorAdapterです

ListView を表示する

ListView を表示するには以下の作業が必要となります。

  • ListView をレイアウトxmlに配置
  • ListView に Adapter をセットする

activity_main.xml

<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" >

    <!-- ListViewを配置 -->
    <ListView
        android:id="@+id/ListView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

MainActivity.java

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

        // ListViewに表示するデータを作成する
        ArrayList<String> list = new ArrayList<String>();
        for (int i = 0; i < 20; i++) {
            list.add("hoge" + i);
        }

        ListView listView = (ListView) findViewById(R.id.ListView);
        // android.R.layout.simple_list_item_1はAndroidで既に定義されているリストアイテムのレイアウトです
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(mActivity,
                android.R.layout.simple_list_item_1, list);

        listView.setAdapter(adapter);
        // タップした時の動作を定義する
        listView.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                // Adapterからタップした位置のデータを取得する
                String str = (String) parent.getItemAtPosition(position);
                Toast.makeText(mActivity, str, Toast.LENGTH_SHORT).show();
            }
        });
    }

まずは ArrayAdapter のインスタンスを作成します。
ここではコンストラクタの引数に Context とリストアイテムのレイアウトIDと表示するデータをリスト形式で渡しています。
ArrayAdapter には様々なコンストラクタが用意されています。適宜、適切なものを使用すると良いでしょう。

レイアウトIDにはandroid.R.layout.simple_list_item_1を設定しています。これは Android で用意されているリストアイテムのレイアウトです。中には TextView が1つあるだけです。
表現豊かなレイアウトを使用したい場合、自分で作成したレイアウトをリストアイテムに表示することもできます。
その方法は次項リストアイテムをカスタマイズするにて説明します。

次に、 setAdapter メソッドで ListView に Adapter をセットします。
最後に、 setOnItemClickListener メソッドでリストをタップした時の動作を設定しています。
実行すると以下の様なListViewが表示されます。

listview

カスタマイズしたリストアイテムを表示する

カスタマイズしたリストアイテムを表示するには以下の作業が必要となります。

  • リストアイテムのレイアウトxmlの作成
  • ListView をレイアウトxmlに配置
  • 独自 Adapter の作成
  • ListView に独自 Adapter をセットする

リストアイテムのレイアウトxmlを作成する

新規にレイアウトxmlを作成します。 リストアイテムのレイアウトは以下のように作成します。
custom_list_item_layout

custom_list_item.xml

<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="wrap_content"
    android:layout_margin="4dp" >

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:src="@drawable/ic_launcher" />

    <TextView
        android:id="@+id/TitleText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/imageView1"
        android:textSize="18sp" />

    <TextView
        android:id="@+id/SubTitleText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/TitleText"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/TitleText" />

</RelativeLayout>

独自 Adapter を作成します

Adapter 内ではリストアイテム View の生成と表示位置に対応するデータの取得、設定といった処理を行います。

CustomListItemAdapter.java

public class CustomListItemAdapter extends ArrayAdapter<String> {

    private LayoutInflater mLayoutInflater;

    public CustomListItemAdapter(Context context, List<String> objects) {
        // 第2引数はtextViewResourceIdとされていますが、カスタムリストアイテムを使用する場合は特に意識する必要のない引数です
        super(context, 0, objects);
        // レイアウト生成に使用するインフレーター
        mLayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        View view = null;

        // ListViewに表示する分のレイアウトが生成されていない場合レイアウトを作成する
        if (convertView == null) {
            // レイアウトファイルからViewを生成する
            view = mLayoutInflater.inflate(R.layout.custom_list_item, parent, false);
        } else {
            // レイアウトが存在する場合は再利用する
            view = convertView;
        }

        // リストアイテムに対応するデータを取得する
        String item = getItem(position);

        // 各Viewに表示する情報を設定
        TextView text1 = (TextView) view.findViewById(R.id.TitleText);
        text1.setText("Title:" + item);
        TextView text2 = (TextView) view.findViewById(R.id.SubTitleText);
        text2.setText("SubTitle:" + item);

        return view;
    }

}

ArrayAdapter を継承して独自の Adapter を作成しています。
getView メソッドをオーバーライドして、戻り値に表示するリストアイテムの View を返します。
表示している位置のデータは getItem メソッドで取得することができます。

custom_list_item

View の再利用について

Android ではリストアイテムを表示する際に、既に View が生成されていてる場合はそれを再利用します。
リストに表示するデータが50件あった場合、リストアイテムの View を50個を生成することはしないということです。
画面を構成するのに必要な分の View が生成され、あとは再利用するため無駄な View を生成するといったことはありません。
どのように View が再利用されているかを確認するため先ほどの CustomListItemAdapter クラスの getView メソッドを以下のように修正します。

CustomListItemAdapter.java

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        View view = null;

        // ListViewに表示する分のレイアウトが生成されていない場合レイアウトを作成する
        if (convertView == null) {
            // レイアウトファイルからViewを生成する
            view = mLayoutInflater.inflate(R.layout.custom_list_item, parent, false);
            
            // リストアイテムに対応するデータを取得する
            String item = getItem(position);

            // 各Viewに表示する情報を設定
            TextView text1 = (TextView) view.findViewById(R.id.TitleText);
            text1.setText("Title:" + item);
            TextView text2 = (TextView) view.findViewById(R.id.SubTitleText);
            text2.setText("SubTitle:" + item);

        } else {
            // レイアウトが存在する場合は再利用する
            view = convertView;
        }
        // 一度作成したレイアウトの表示データを変更しないことにより、再利用されたデータがどこに表示されるかを確認する
        return view;
    }

実行すると以下のように View が使いまわされていることがわかります。
recycle_list_item

パフォーマンスについて

ListView がスクロールされるたびに findViewById メソッドを呼び出すとパフォーマンスが低下します。 再利用した View を表示するときでも各要素を更新することがあります。
頻繁に findViewById メソッドを呼び出す場合には”view holder”デザインパターンを利用すると良いです。

詳しくは、 Hold View Objects in a View Holder | Android Developersを御覧ください。

ViewPager

ViewPager は、横にフリックして View を切り替えるための View です。
単純な View だけでなく、Fragment を持たせて、複数の Fragment を切り替える用途にも使用出来ます。

ViewPager も、ListView と同じく、Adapter に中身を管理させます。
また、ViewPager は Support Package で提供されている機能です。

PagerAdapter

データをバインドして表示する際には、ListAdapter インタフェースを実装した Adapter を使用します。
PagerAdapter は ViewPager を表示するための基本的な Adapter です。

PagerAdapterの使い方

ここでは TextView をもつページが5つある ViewPager を作成します。

ViewPager を表示するには以下の作業が必要となります。

  • ViewPager をレイアウトxmlに配置
  • 独自 PagerAdapter を実装
  • ViewPager に Adapter をセットする
ViewPagerをレイアウトxmlに配置

ViewPager は Support Package で提供されているため、android.support.v4.view.ViewPagerと記述します。

activity_main.xml

<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.v4.view.ViewPager
        android:id="@+id/Pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>
独自 PagerAdapter を実装

PagerAdapter を実装するには最低限以下のメソッドを実装する必要があります。

メソッド名 役割
Object instantiateItem(ViewGroup container, int position) ページを生成します
void destroyItem(ViewGroup container, int position, Object object) ページを削除します
int getCount() ページ数を返却します
boolean isViewFromObject(View view, Object object) instantiateItemで返却されたキーObjectとページが関連付いているかどうかを確認します。

SamplePagerAdapter.java

class SamplePagerAdapter extends PagerAdapter {

    private static final int PAGE_COUNT = 5;

    private Context mContext;

    public SamplePagerAdapter(Context context) {
        super();
        mContext = context;
    }

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

        // TextViewを生成
        TextView textView = new TextView(mContext);
        textView.setText("Position:" + position);
        //コンテナに追加
        container.addView(textView);

        // ここではTextView自体をキーとして返しています
        return textView;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        // viewの削除
        // objectはinstantiateItemで返却したオブジェクトです
        container.removeView((View) object);
    }

    @Override
    public int getCount() {
        // ページ数を返します。今回は固定値としています。
        return PAGE_COUNT;
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        // キーが正しいかを判定
        return view == (TextView) object;
    }

ViewPager はキーとなる object でそれぞれのページを関連付けています。このキーは instantiateItem メソッドで返却された object であり Adapter 内でのユニークな識別子として使用されます。
上記のコードでは TextView 自身をキーとして使用しています。

ViewPagerにAdapterをセットする

MainActivity.java

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

        ViewPager pager = (ViewPager) findViewById(R.id.Pager);
        pager.setAdapter(new SamplePagerAdapter(this));

    }

FragmentPagerAdapter

ViewPager は Fragment の切り替えに使われるケースが多いです。
Fragment は簡単に提供できますし、それぞれのページのライフサイクルの管理ができるためです。
ViewPager で Fragment を使う際のよくあるユースケースをカバーし実装されたものが、 FragmentPagerAdapter と FragmentStatePagerAdapter になります。

FragmentPagerAdapter は表示したページをメモリに保持します。 FragmentStatePagerAdapter は非表示になったページはステートの保持のみを行い Fragment を破棄します。ページ数が多い場合メモリ消費が大きくなるため使用すると良いです。

FragmentPagerAdapter の使い方

  • ViewPager をレイアウトxmlに配置
  • Fragment を作成
  • 独自 FragmentPagerAdapter を実装
  • ViewPager にAdapter をセットする
ViewPagerをレイアウトxmlに配置

PagerAdapter の使い方と同様のため割愛します。

Fragmentを作成

サンプルでは表示位置(position)を TextView 表示します。 そのため、newInstance メソッドを用意し position を引数として渡し Bundle を設定しています。
Bundle から position を取得し、TextView に設定しています。

fragment_main.xml

<FrameLayout 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:background="@android:color/black" >

    <TextView
        android:id="@+id/TextView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="4dp"
        android:background="@android:color/white"
        android:gravity="center"
        android:textSize="90sp"
        android:textStyle="bold" />

</FrameLayout>

SampleFragment.java

public class SampleFragment extends Fragment {

    public static SampleFragment newInstance(int position) {

        SampleFragment sampleFragment = new SampleFragment();
        Bundle bundle = new Bundle();
        bundle.putInt("position", position);
        sampleFragment.setArguments(bundle);

        return sampleFragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        Bundle bundle = getArguments();
        int position = 0;
        if (bundle != null) {
            position = bundle.getInt("position");
        }

        View view = inflater.inflate(R.layout.fragment_main, container, false);

        TextView text = (TextView) view.findViewById(R.id.TextView1);
        text.setText(String.valueOf(position));

        return view;
    }
}
独自 FragmentPagerAdapter を実装

FragmentPagerAdapter を実装するには最低限以下のメソッドを実装する必要があります。

メソッド名 役割
Fragment getItem (int position) Fragmentを生成します
int getCount() ページ数を返します

SampleFragmentPagerAdapter.java

public class SampleFragmentPagerAdapter extends FragmentPagerAdapter {

    private static final int PAGE_COUNT = 5;

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

    @Override
    public Fragment getItem(int position) {
        return SampleFragment.newInstance(position);
    }

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

}
ViewPager に Adapter をセットする

Fragment を管理するため FragmentManager をコンストラクタに渡します。 Support Package を使用しているため、 FragmentManager を生成するメソッド名は getSupportFragmentManager となります。
( Support Package でない場合は getFragmentManager となります)

MainActivity.java

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

        ViewPager pager = (ViewPager) findViewById(R.id.Pager);
        
        FragmentManager fm = getSupportFragmentManager();
        SampleFragmentPagerAdapter sampleFragmentPagerAdapter = new SampleFragmentPagerAdapter(fm);
        
        pager.setAdapter(sampleFragmentPagerAdapter);
    }

実習・課題

ListView

  1. (実習) カスタマイズしたリストアイテムを表示するで説明したプロジェクトを写経して実行してください。余裕がある場合はViewの再利用についてについても試してみて下さい。説明したプロジェクトはAndroidTraining/projects/fundamentals/6th/CustomListItemSampleにあります。
  2. (実習) 実習1の画面にボタンを配置し、ボタンをタップしたらListViewの先頭を表示するようにして下さい。
    ヒント:ListView には smoothScrollToPosition メソッドがあります。
  3. (課題) 以下のListViewを表示するように実装してください。
  4. (課題) 課題3で作成したListViewをタップしたら、次画面に遷移してタップしたアイテムの情報を表示するようにしてください。
  5. (課題) 課題3のContextMenuにListViewが設定されています。ContextMenuには追加と削除メニューが定義されています。追加をタップしたときは選択されているアイテムを新たなアイテムとしてListViewにデータを追加してください。削除をタップしたときは選択されているアイテムを削除するように実装してください。
    ヒント:データの追加、削除、表示の更新処理はAdapterで行います。

ViewPager

  1. (実習) FragmentPagerAdapter の使い方で説明したプロジェクトを写経して実行してください。説明したプロジェクトはAndroidTraining/projects/fundamentals/6th/FragmentViewPagerSampleにあります。
  2. (課題) 実習1のプロジェクトを引き続き使用してください。ActionBarのTab Navigationを追加し、Tabを選択したら該当のページが表示されるようにしてください。(3番目のTabを選択したら3ページ目が表示される)また、ページをスクロールさせたら該当のTabが選択されるようにしてください。 また、Tabとページの数は3つとしてください。