モケラ

Tech Sheets

mokelab

ListViewで、独自のレイアウトを使用する

最終更新日:2015-08-16

ArrayAdapterをそのまま使うと、簡単にリスト表示が実装できますが、その反面レイアウトの自由度は減ります。ここでは、次の図のように1行のレイアウトを独自のものにする方法を紹介します。

1行分のレイアウトXMLを作成する

res/layoutに、リストの1行分のレイアウトを作成します。ここではitem_week.xmlとします。

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    >
    <ImageView
        android:id="@+id/icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@android:drawable/star_on"
        android:layout_centerVertical="true"
        />
    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@+id/icon"
        android:layout_centerVertical="true"
        android:layout_marginLeft="8dp"
        />
</RelativeLayout>

ArrayAdapterを継承したクラスを作成する

次に、ArrayAdapterを継承したクラスを作成します。説明のために簡略化した実装例を示しますが、パフォーマンスの問題があるので、このままコピペして使わないでください。

public class WeekAdapter extends ArrayAdapter<String> {

    private LayoutInflater mLayoutInflater;

    public WeekAdapter(Context context) {
        super(context, 0);
        mLayoutInflater = LayoutInflater.from(context);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View root = mLayoutInflater.inflate(R.layout.item_week, null);

        String item = getItem(position);
        TextView textView = (TextView) root.findViewById(R.id.text);
        textView.setText(item);

        return root;
    }
}

コンストラクタでLayoutInflaterオブジェクトを取得しておきます。super()の第2引数は今回は独自レイアウトを使用するため、0を渡しておきます。

次に、このクラスのポイントとなるgetView()をオーバーライドします。第1引数のpositionで 位置が渡されるので、getItem(position)でデータを取得し、Viewを生成してreturnします。

パフォーマンス改善

この実装で論理的には正しそうですが、パフォーマンスの面で次の問題を抱えています。パフォーマンスが悪いと、スクロール時にひっかかりが生じ、ユーザー体験の低下につながってしまいます。

  • 毎回ViewをXMLから生成している点
  • findViewById()でViewを探している点

mLayoutInflater.inflate(R.layout.item_week, null)はレイアウトXMLの複雑度にもよりますが、一般的にはコストの高い処理です。なので、できれば以前作った1行分のレイアウト(=View)は再利用したいものです。実はgetView()には、再利用のための仕組みがすでに備わっています。再利用可能なViewがある場合は、getView()の第2引数であるconvertViewに渡されます。なので、convertViewがnullのときだけViewを生成するよう、次のように書き換えます。

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        convertView = mLayoutInflater.inflate(R.layout.item_week, null);
    }
    String item = getItem(position);
    TextView textView = (TextView) convertView.findViewById(R.id.text);
    textView.setText(item);

    return convertView;
}

次に、getView()の中で毎回findViewById()でデータをセットするためのViewを取得しています。findViewById()は該当Viewが見つかるまで、順にViewを探していくので時間のかかる処理となります。なので、Viewを生成した直後にfindViewById()で該当Viewを探しておき、その結果を後から使い回すようにします。

Viewには、setTag() / getTag()という、任意のオブジェクトをセットし、取得するためのメソッドがあります。ここでは、findViewById()で取得したViewを保存するためのViewHolderというオブジェクトをセットすることにします。

以上を踏まえ、パフォーマンスも考慮したWeekAdapterの実装は次のようなものになります。

public class WeekAdapter extends ArrayAdapter<String> {

    private LayoutInflater mLayoutInflater;

    public WeekAdapter(Context context) {
        super(context, 0);
        mLayoutInflater = LayoutInflater.from(context);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            convertView = mLayoutInflater.inflate(R.layout.item_week, null);
            convertView.setTag(new ViewHolder(convertView));
        }
        ViewHolder holder = (ViewHolder) convertView.getTag();

        String item = getItem(position);
        holder.mTextView.setText(item);

        return convertView;
    }

    private static class ViewHolder {
        TextView mTextView;
        ViewHolder(View root) {
            mTextView = (TextView) root.findViewById(R.id.text);
        }
    }
}

一覧に戻る