Androidの基本的なViewのひとつとしてListViewがある。
データを一覧表示するオブジェクトだが、私がAndroid開発を始めたころに最初に躓いたところでもある。というのは、データとリストの紐付けにAdapterというオブジェクトを介するところが今まで使ってきたプログラミング言語にはない要素で、ListViewに直接データソースを放り込んではいけないの?と思ってしまった。
でも、わかってしまえば納得の仕組みだ。
世に出回っているAndroidアプリには、単純にテキストを並べただけのリストもあれば、二段表示になってたり、アイコンが表示されてたり、チェックボックスが付いてたり、様々なレイアウトを見ることができる。
ListViewはあくまでも描画とユーザーインターフェイスを担当するオブジェクトで、レイアウトの生成とデータの配置はAdapterの役割なのだ。この仕組みのおかげで複雑なリストを作ることもできるわけだ。
ArrayAdapterを使った簡単なリスト
まずはアクティビティのレイアウトのXML。
ListViewを一個配置しただけの単純なもの
レイアウトファイル【activity_main.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">
<ListView
android:id="@+id/sample_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
続いてMainActivityのコード。
JAVAファイル【MainActivity.java】
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView listView = (ListView) findViewById(R.id.sample_list);
String[] arrayCarMaker = {
"TOYOTA",
"MAZDA",
"HONDA"
};
ArrayAdapter adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, arrayCarMaker);
listView.setAdapter(adapter);
}
}
これを実行すると次のようになる。
テキスト一行だけのリストはこんな感じでできてしまう。
ArrayAdapterのコンストラクタ引数は次の通り
- 第一引数:Context
- 第二引数:レイアウトのリソースID
- 第三引数:データ配列
レイアウトのリソースID[android.R.layout.simple_list_item_1]はAndroidにあらかじめ用意されているもので、中身はTextView一個だけの単純なものだ。ArrayAdapterがString配列のデータをTextViewに受け渡して紐付けているのである。
SimpleAdapterを使う方法
SimpleAdapterというクラスも用意されていて、自分で作成したレイアウトファイルを適用することもできる。個人的には名前ほどシンプルではないように思う。カスタムレイアウトを使うときはこの後に紹介するArrayAdapterを継承するやり方のほうがよい。
MainActivityのコード
【MainActivity.java】
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView listView = (ListView) findViewById(R.id.sample_list);
//フィールド名の配列(Mapのキーに使用)
String[] from = {
"code",
"name"
};
//fromのフィールド値を格納するViewのリソースIDの配列
int[] to = {
android.R.id.text1,
android.R.id.text2
};
//Mapリストの作成
List<Map<String,String>> maplist = new ArrayList<Map<String, String>>() ;
Map<String, String> listDataSource = new HashMap<>();
listDataSource.put("code", "1");
listDataSource.put("name", "TOYOTA");
maplist.add(listDataSource);
listDataSource = new HashMap<>();
listDataSource.put("code", "2");
listDataSource.put("name", "MAZDA");
maplist.add(listDataSource);
listDataSource = new HashMap<>();
listDataSource.put("code", "3");
listDataSource.put("name", "HONDA");
maplist.add(listDataSource);
//アダプターのインスタンスを作成
SimpleAdapter adapter = new SimpleAdapter(this, maplist, android.R.layout.simple_list_item_2, from, to);
//リストビューに適用
listView.setAdapter(adapter);
}
}
実行結果は上のようになる。
リソースの[android.R.layout.simple_list_item_2]はあらかじめ用意されているTextViewが二段になっているレイアウトだ。text1、text2という名前のTextViewがあるので、fromとtoの二つの配列でデータとViewを紐付けている。
SimpleAdapterが面倒くさいのはMapオブジェクトのリストを作って、どの項目をどのViewに当てはめるのか配列で指定しなければならないところ。コードを見ただけでは直感的に何しているのか見えにくい。
Mapリストの作り方はもっと効率の良いやり方があるがとりあえずここでは紹介しない。
ArrayAdapterを継承したカスタムアダプタを使用する方法
個人的にはこの方法が一番使いやすい。自由度が高いし、コードの可読性がよいからだ。
このセクションでは独自のレイアウトファイルを用意してリストを作ってみる
カスタムレイアウトのXML(TextViewを二個横に並べたレイアウト)
【lititem_carmaker.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="horizontal">
<TextView
android:id="@+id/samplelist_text1"
android:textSize="20sp"
android:layout_width="100dp"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/samplelist_text2"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
MainActivity.javaのコード。
【MainActivity.java】
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView listView = (ListView) findViewById(R.id.sample_list);
CarMaker[] arrayCarMaker = {
new CarMaker(1, "TOYOTA"),
new CarMaker(2, "MAZDA"),
new CarMaker(3, "HONDA")
};
CarMakerAdapter adapter = new CarMakerAdapter(this, R.layout.listitem_carmaker, arrayCarMaker);
listView.setAdapter(adapter);
}
}
//データを格納するクラス
class CarMaker {
private int id;
private String name;
public CarMaker (int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public void setId(int id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
}
//ArrayAdapterを継承したクラス
class CarMakerAdapter extends ArrayAdapter<CarMaker> {
Context context;
public CarMakerAdapter(Context context, int resource, CarMaker[] objects) {
super(context, resource, objects);
this.context = context;
}
//リストの行が生成されるたびにListViewから呼ばれる
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.listitem_carmaker, parent, false);
}
CarMaker carMaker = (CarMaker) getItem(position);
TextView tvId = (TextView) convertView.findViewById(R.id.samplelist_text1);
TextView tvName = (TextView) convertView.findViewById(R.id.samplelist_text2);
tvId.setText(String.valueOf(carMaker.getId()));
tvName.setText(String.valueOf(carMaker.getName()));
return convertView;
}
}
実行結果はこんな感じ。
データを格納するクラスとしてCarMakerクラスをを作成し、その配列をCarMakerAdapterに渡している。
CarMakerAdapterはArrayAdapterを継承したクラス。
ListViewはリストの各行を描画する際に、AdapterのgetView()メソッドを呼び出すため、これをオーバーライドし、LayoutInflaterを用いてカスタムレイアウトを生成する。そしてレイアウト上にあるTextViewにCarMakerクラスの各要素を文字列にしてセットしている。
getView()メソッドの引数convertViewは使いまわされるのでnullの時のみカスタムレイアウトをinflateしている。getView()が呼び出されるたびに新たなレイアウトを作っていると、どんどんメモリが消費されて、リストの行数が多いと最悪アプリが落ちてしまう可能性がある。各行に同じレイアウトを適用するなら、convertViewを再利用したほうが良い。
Adapterではデータをレイアウトに配置するだけでなく、ある条件の行だけ色を変える、といった処理も行うことができる
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.listitem_carmaker, parent, false);
}
CarMaker carMaker = (CarMaker) getItem(position);
TextView tvId = (TextView) convertView.findViewById(R.id.samplelist_text1);
TextView tvName = (TextView) convertView.findViewById(R.id.samplelist_text2);
if (carMaker.getId() == 2) { //・・・・・・(1)
tvId.setTextColor(Color.RED);
tvName.setTextColor(Color.RED);
} else {
tvId.setTextColor(Color.GRAY);
tvName.setTextColor(Color.GRAY);
}
tvId.setText(String.valueOf(carMaker.getId()));
tvName.setText(String.valueOf(carMaker.getName()));
return convertView;
}
上の例では(1)の部分でID=2のときTextViewの文字色を赤に変えている。
実行結果はこんな感じ
やったことはないが、行ごとにレイアウトを変えるということもできるかもしれない。
機会があったら試してみようと思う。