Androidのお勉強 第二回 ListViewと独自Adapterについて

  • 104
    いいね
  • 0
    コメント

第二回 ListViewと独自Adapterについて

今回は情報をリストで表示するView「ListView」についての使い方と作法を学びます。

勉強会全体の目次は以下

では今回の第二回をやっていきましょう。

作ってみよう

前回の練習で空で作成したListSampleActivityがありますので、
そいつを使います。

まずはres/layout下に「activity_sample_list.xml」を作成します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <TextView
        android:id="@+id/title_sample_list_text"
        android:text="@string/app_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <ListView
        android:id="@+id/sample_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

次にAvtivityです。レイアウトでListViewを指定しているので、Activityと紐づけます。
紐づけたら、表示するデータを指定します。

とりあえず書いてみましょう。

package jp.co.kenshu;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;

public class ListSampleActivity extends Activity {

    protected ListView listView;
    protected TextView titleText;
    private String[] names = {"taro", "jiro", "saburo"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sample_list);
        if (savedInstanceState == null) {
            listView = (ListView) findViewById(R.id.sample_list);
            titleText = (TextView) findViewById(R.id.title_sample_list_text);
        }
        listView.setAdapter(new ArrayAdapter<String>(getApplicationContext(), android.R.layout.simple_list_item_1, names));
        listView.setOnItemClickListener(new SampleListItemClickListener(titleText));
    }

        static class SampleListItemClickListener implements ListView.OnItemClickListener {

            private final TextView textView;

            SampleListItemClickListener(TextView titleView) {
                this.textView = titleView;
            }

            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                ListView listView = (ListView) parent;
                String item = (String) listView.getItemAtPosition(position);
                textView.setText(item);
            }
        }
}

動かしてみよう

では実際に動かしてみてください。
こんな感じでListViewで選択したItemの文字がTitleに動的に設定されることがわかります。

list.png

解説

ListViewは複数のViewを表示するのにすぐれています。
縦に長いViewをスクロースさせて表示させるのに、「ScrollView」(ViewGroup)を使うことがありますが、実はListViewはそのScrollViewの子クラスとなります。

そのため、ListViewに表示されたViewは縦幅があるとスクロールバーが自動で表示されます。

そして、ちょっと余談ではありますが、ScrolllBarを継承したViewであるListViewや、GridViewは、それら自身をScrollViewで囲うようなことは推奨してません。
こんな感じ

<ScrollView
  …
  <ListView…

これがDeveloperでも進めてません。というより、スクロール系ViewGrouopの中にスクロール系ViewGrouopを埋め込むようには設計されていませんので、ご注意ください。

Adapterのセット

listView.setAdapter(new ArrayAdapter<String>(getApplicationContext(), android.R.layout.simple_list_item_1, names));

ListViewに表示する項目は「アダプター(Adapter)」と呼ばれるものをセットすることで表示されます。
ある程度単純なテキストをリスト表示したいような場合は、今回のような

new ArrayAdapter<String>(getApplicationContext(), android.R.layout.simple_list_item_1, names)

ArrayAdapterのように、SDKが標準で用意しているAdapterもありますので、こちらを利用すれば手軽にListViewにAdapterをセットできます。

ArrayAdapterのコンストラクタに以下をそれぞれ設定しています。

  • getApplicationContext()
  • android.R.layout.simple_list_item_1
  • names

第一引数は単純にContext情報を渡しています。
 ※Contextとは…
 Contextとは、簡単に言うと、アプリケーションの親情報です。ActivityはContextの子クラスとなります。
 APIの中にはちょいちょいこのContextを必要とするものが存在します。
 ActivityもContextなんだったら、thisで良いじゃん!!とか思うかもしれませんが、
 Activityはメモリの逼迫等に引きずられて、簡単に死にます(インスタンスがnull)
 そうなると、参照がなくなったActivityを持ち続けることになり、メモリリークの原因となります。
 その点、getApplicationContext()で取得できるContext情報はActivityのライフサイクルに依存せず、
 アプリケーションとしての純粋な情報となるため、メモリリークの危険性を回避できます。
 Contextを必要とするAPIには、可能な限りgetApplicationContext()を渡すようにしましょう。

第二引数が重要です。これがandroidSDKが標準で用意している、一行テキスト表示のAdapterサンプルになります。
第三引数は第二引数で指定したテキストリストに何を表示させるかのデータを指定しています。

ListViewのクリックリスナー

listView.setOnItemClickListener(new SampleListItemClickListener(titleText));

static class SampleListItemClickListener implements ListView.OnItemClickListener {

    private final TextView textView;

    SampleListItemClickListener(TextView titleView) {
        this.textView = titleView;
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        ListView listView = (ListView) parent;
        String item = (String) listView.getItemAtPosition(position);
        textView.setText(item);
    }
}

今回は「リスト表示されえちるアイテムを一つクリックした際のイベント」を定義してます。

ListView.OnItemClickListener

それを実現するInterfaceがこれです。
そして、このInterfaceが提供する

onItemClick()

このコールバックメソッドが実際にアイテムをクリックした際の挙動の実装を求める仕様となってます。
Interfaceなので、匿名インナークラスなり、外だしクラスなりで実装してやる必要があります。
今回は以下のようなstaticなインナークラスで実装していますので、

static class SampleListItemClickListener implements ListView.OnItemClickListener

このクラスをlistView.setOnItemClickListenerでnewして指定しています。

listView.setOnItemClickListener(new SampleListItemClickListener(titleText));

今回はTextViewのテキストを変更したかったので、コンストラクタに渡してます。

リスナーの具体的な実装を見てみましょう。

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    ListView listView = (ListView) parent;
    String item = (String) listView.getItemAtPosition(position);
    textView.setText(item);
}

onItemClick(AdapterView<?> parent, View view, int position, long id)

・parent… クリックした項目のAdapterViewです。この場合はListViewを使用しているため、ListViewになります。※ListViewはAdapterViewの子クラス。
・view… クリックしたアイテムのViewです。
・position… クリックした項目のListView内でのpositionをさします。indexです。0から始まります。
・id… クリックした項目にセットされているIDです。

なので、

ListView listView = (ListView) parent;

とListViewにキャストすることができます。

String item = (String) listView.getItemAtPosition(position);

ここは、Adapterにセットしたのが、String[]のnamesであるため、ListViewに表示されているViewからクリックしたポジションのアイテムが取得できます。
あとは取得したアイテム(namesの一部(String))をTextViewにセットしているという訳です。

Adapterを自作する

先ほどはとても簡単なリストを作成しました。
しかし、ListViewにはもっと凝ったUIを表示させたい時が多いと思います。
そのような場合は、自分で独自のUIを定義できるAdapterを作成します。

先ほどはTextViewが一つ表示されるだけの簡単なUIだったため、今度は以下のようなUIのListView(Adapter)を作成してみましょう。

cardlist.png

端末にインストールされているアプリの一覧情報を取得し、ListViewに表示するサンプルを作成します。
UIも綺麗ですね。ちょっと前に話題になったカードUIになります。

大工事しますよ!!以下の順で作成していきます。

  • activity_sample_list.xmlの修正
  • list_header_footer.xmlの作成
  • ListSampleActivityの修正
  • adapter_list_item_card.xmlの作成
  • CardListAdapetr.javaの作成

activity_sample_list.xmlの修正

こいつはちょい修正になります。
以下のように書き換えてください。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <ListView
        android:id="@+id/card_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

TextViewがなくなって、ListViewのid名を変えました。
こんだけです。

list_header_footer.xmlの作成

ListViewにヘッダーとフッターを付けます。
そのためのUIです。

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

ListSampleActivityの修正

ここが大工事です。
とりあえず以下の通りに修正してください。

package jp.co.kenshu;

import android.app.Activity;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ListView;

import java.util.List;

import jp.co.kenshu.adapter.CardListAdapter;

public class ListSampleActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sample_list);
        PackageManager packageManager = getPackageManager();
        List<PackageInfo> packageInfoList = packageManager.getInstalledPackages(PackageManager.GET_ACTIVITIES);

        CardListAdapter adapter = new CardListAdapter(getApplicationContext());

        if (packageInfoList != null) {
            for (PackageInfo info : packageInfoList) {
                adapter.add(info);
            }
        }

        int padding = (int) (getResources().getDisplayMetrics().density * 8);
        ListView listView = (ListView) findViewById(R.id.card_list);
        listView.setPadding(padding, 0, padding, 0);
        listView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
        listView.setDivider(null);

        LayoutInflater inflater = LayoutInflater.from(getApplicationContext());
        View header = inflater.inflate(R.layout.list_header_footer, listView, false);
        View footer = inflater.inflate(R.layout.list_header_footer, listView, false);
        listView.addHeaderView(header, null, false);
        listView.addFooterView(footer, null, false);
        listView.setAdapter(adapter);
    }
}

adapter_list_item_card.xmlの作成

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical"
              android:padding="8dip">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#ffffff"
        android:padding="8dip">
        <TextView
            android:id="@+id/title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_toLeftOf="@+id/icon"/>
        <TextView
            android:id="@+id/sub"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@+id/title"
            android:layout_marginTop="4dip"
            android:layout_toLeftOf="@+id/icon"
            android:textColor="#999999"
            android:textSize="12sp"/>
        <ImageView
            android:id="@+id/icon"
            android:layout_width="48dip"
            android:layout_height="48dip"
            android:layout_alignParentRight="true"/>
    </RelativeLayout>
    <View
        android:layout_width="match_parent"
        android:layout_height="1dip"
        android:background="#cccccc"/>
</LinearLayout>

CardListAdapetr.javaの作成

package jp.co.kenshu.adapter;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import jp.co.kenshu.R;

public class CardListAdapter extends ArrayAdapter<PackageInfo> {

    LayoutInflater mInflater;
    PackageManager packageManager;

    public CardListAdapter(Context context) {
        super(context, 0);
        mInflater = LayoutInflater.from(context);
        packageManager = context.getPackageManager();
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.adapter_list_item_card, parent, false);
        }

        PackageInfo info = getItem(position);

        TextView tv = (TextView) convertView.findViewById(R.id.title);
        tv.setText(info.applicationInfo.loadLabel(packageManager));

        tv = (TextView) convertView.findViewById(R.id.sub);
        tv.setText(info.packageName + "\n" + "versionName : " + info.versionName + "\nversionCode : " + info.versionCode);

        ImageView iv = (ImageView) convertView.findViewById(R.id.icon);
        iv.setImageDrawable(info.applicationInfo.loadIcon(packageManager));


        return convertView;
    }
}

解説

実行できましたか??

それでは、今回は何をやったかを、要点のみ解説します。

インストール済みのアプリを取得する

端末にインストール済みのアプリを取得するには、PackageManagerを使用します。
これは別に覚えなくていいです。色々なAPIを使ってみるという名目で触ってみただけです。
Androidでインストール済みのアプリを取得するには、以下のように

PackageManager packageManager = getPackageManager();
List<PackageInfo> packageInfoList = packageManager.getInstalledPackages(PackageManager.GET_ACTIVITIES);

PackageManagerのインスタンスを取得し、このオブジェクトが持つインストール済みのアプリ情報を

packageManager.getInstalledPackages

で取得してます。
こんなことできるんだなー程度で構いません。
あとはこの情報をforeachで回して、Adapterにaddしてます。

ListViewの境界線をなくす

ListViewはデフォルトで項目と項目の間に境界線が表示されます。
今回は境界線が邪魔だったので

listView.setDivider(null);

として、境界線をなくしました。こんなこともAPIで用意されているんですね。

ヘッダーフッターの追加

LayoutInflater inflater = LayoutInflater.from(getApplicationContext());
View header = inflater.inflate(R.layout.list_header_footer, listView, false);
View footer = inflater.inflate(R.layout.list_header_footer, listView, false);
listView.addHeaderView(header, null, false);
listView.addFooterView(footer, null, false);

ListViewにはヘッダーとフッターを別々のViewとして付与することもできます。
今回はカードUIの始点と終点の余白を均等にするためだけに、高さをもったViewを配置しています。
LayoutInflater(レイアウトインフレータ)を使うことで、setContentView()したレイアウトファイル以外のLayoutファイルへのアクセスが可能となります。
この手法はよくAdapterの中でも使用します。

inflaterには以下、三つの取得方法があります。

// コンテキストから取得
LayoutInflater inflater = LayoutInflater.from(getApplicationContext());
// アクティビティから取得
LayoutInflater inflater = getLayoutInflater();
// システムサービスから取得
LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);

実際にlayoutファイルからレイアウトを取得しているのが、

inflater.inflate(R.layout.list_header_footer, listView, false);

この記述になります。

第一引数がレイアウトファイル。第二引数が追加対象のViewGroup、第三引数が第二引数で指定したViewをroot要素とするかどうか。falseなら、単純に第一引数で指定したレイアウトのRootViewがそのままRootになる。
参考

独自Adapterの生成

ここからが本番です。ActivityではAdapterにInstall済みアプリケーションの一覧情報を渡していました。
あとはそれをどう表示させるか??がAdapterの役割です。

public class CardListAdapter extends ArrayAdapter<PackageInfo> {

先ほどそのまま使用したArrayAdapterを継承して、独自のAdapterを生成します。

<PackageInfo>

これは、このAdapterで取り扱う型を宣言しています。
ここで指定した型が、OnItemClickの際のコールバック型になります。

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

Adapterで表示させる画面は一行一行表示させるUIを指定します。
その際に、一行一行読み込まれる際のコールバックが

getView()

となります。独自のAdapterを作成するうには、必ずこのメソッドをOverrideします。
getViewは画面をスクロールし、新しい一行が表示されるたびに呼ばれます。
そして、一行のUIを生成するわけです。

しかし、この「一行のUIを生成する」という作業がなかなか処理が重いんですね。
スクロールする度にUIを生成していてはかつかつになるわけです。

そこでどうするかというのが、

convertView

こいつの出番です。
Android側で既にその対策は考えられていまして、実はViewを再利用する仕組みがあります。

文字で書くと伝えるのが難しいのですが、画面をスクロールして表示領域から消えたView(一行)は、破棄されず、スクロールして表示されようとする次の新しい行に対してインスタンスを再利用しようとします。
それがconvertViewになります。

再利用できるViewが渡った場合は、convertViewにnullでないオブジェクトが渡ります。
そのため、

if (convertView == null) {
    convertView = mInflater.inflate(R.layout.adapter_list_item_card, parent, false);
}

こんな記述で、「再利用可能なViewがあったらそれを使う。なかったら新しく作成するよ」って動きが可能になります。
もしこの記述がなく、

convertView = mInflater.inflate(R.layout.adapter_list_item_card, parent, false);

って毎回Viewを作成していたらカクカクでかつかつなアプリになってしまいます。

参考になるリンクをいくつか張っておきます。

http://hyoromo.hatenablog.com/entry/20090912/1252777077
http://lucasr.org/2012/04/05/performance-tips-for-androids-listview/

一個目のリンクが視覚的でわかりやすい。
二個目は「ViewHolder」というさらに効率的な使い方を紹介してます。
※今回はViewHolderについてはとりあげません。

そして、Adapterの中で一行一行のレイアウトを定義している訳ですが、
今回もinflteを使用して一行分のレイアウト情報を読み込んでいます。

mInflater.inflate(R.layout.adapter_list_item_card, parent, false);

adapter_list_item_cardというxmlレイアウトをListViewの一行一行にセットしているんですね。

getItem

PackageInfo info = getItem(position);

getItemもAdapterが持っているメソッドです。
これはgetViewが呼ばれた一行が持つアイテムを取得します。
今回はPackageInfoを複数もつListViewを形成しているため、これから生成されるpositionのPackageInfo一個分のデータがここで取得されるというわけです。

あとは、取得したPackageInfoデータを

TextView tv = (TextView) convertView.findViewById(R.id.title);
tv.setText(info.applicationInfo.loadLabel(packageManager));

inflateしたレイアウトに指定されているTextViewにセットしています。

info.applicationInfo.loadLabel(packageManager)

この記述でインストール済みアプリのアプリラベル名を取得しています。

tv.setText(info.packageName + "\n" + "versionName : " + info.versionName + "\nversionCode : " + info.versionCode);

ここで、アプリのパッケージ名とversion名、version番号をTextViewにセットしています。

練習問題

①. ListViewにイベントを付与する

現状ListViewにイベントがないため、クリックしたらそのアプリのアプリ名をアラートで表示させてみよう。

②. 47都道府県を表示するListViewを作ってみよう

新規画面として上記画面を作成してみよう。
MainActivityにボタンを一つ追加し、クリックしたら47都道府県一覧画面に遷移するようにしよう。
ListViewの項目をクリックしたら、クリックした都道府県名を赤字に変える処理を入れよう。
項目を長押ししたら、長押しした都道府県View一行をアニメーションでくるくる回してみよう。
※ListViewのAdapterは独自Adapterで作成すること。UIは任せる。