ExpandableListをカスタマイズする

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

ExpandableListViewとは?

親子関係を持ったデータを保持し、展開できるListViewのことです。こんな感じ

色々情報を見てみましたが、子の情報がStringだけだったりして、少々物足りなかったので拡張してみました。イメージとしては子供にString以外の情報を与えて、文字列以外の表示をできるようカスタマイズしてみます

親のListを展開すると、その親に紐づく子供の名前写真が表示されるようなListを目標に作ってみます

ExpandableListViewをカスタマイズする準備

  • xmlのレイアウトに「ExpandableListView」を設定する
  • データを色々と制御したりするためのAdapterを作成する。「BaseExpandableListAdapter」を継承させる
  • データ一行のLayoutを定義したxmlを作成する

まずは子データの一つを示す、名前とアイコンのIDを持ったクラスを作成します

// 子アイテムの定義
public class ItemDto {
	private String name = "";							// 名前
	private int resourceId = R.drawable.ic_launcher;	// アイコンのResource ID. DefaultはLauncherアイコン
	
	public ItemDto(String name, int id) {
		this.name = name;
		this.resourceId = id;
	}
	
	public ItemDto(String name) {
		this.name = name;
	}
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getResourceId() {
		return resourceId;
	}
	public void setResourceId(int resourceId) {
		this.resourceId = resourceId;
	}
}

子アイテムを展開した時のデータ一行分のレイアウト定義は次のように設定します

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    <TextView 
        android:id="@+id/member_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentLeft="true"
        android:gravity="center_vertical|left"
        android:paddingLeft="36dip"
        android:layout_centerVertical="true"
        />
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignRight="@id/member_list"
        android:textSize="10sp"
        android:text="@string/message"
        />
</RelativeLayout>

親のグループアイテムにStringのList、子供のアイテムはItemDtoのListとします。親と子の関係は、親が「1」に対し子が「多」になるので、親アイテムはList、子アイテムはListのListとなります(分かりづらい!)

import java.util.List;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseExpandableListAdapter;
import android.widget.TextView;

public class SampleExpandableListAdapter extends BaseExpandableListAdapter {
	
	private List<String> groups;
	private List<List<ItemDto>> children;
	private Context context = null;
	private int[] rowId;
	
	/**
	 * Constructor
	 */
	public SampleExpandableListAdapter(Context ctx, int[] rowId, List<String> groups, List<List<ItemDto>> children) {
		this.context = ctx;
		this.groups = groups;
		this.children = children;
		this.rowId = rowId;
	}
	
	/**
	 * 
	 * @return
	 */
	public View getGenericView() {
		// xmlをinflateしてViewを作成する
		View view = LayoutInflater.from(context).inflate(R.layout.line_item, null);
		return view;
	}
	
	
	public TextView getGroupGenericView() {
		AbsListView.LayoutParams param = new AbsListView.LayoutParams(
				ViewGroup.LayoutParams.MATCH_PARENT, 64);
		
		TextView textView = new TextView(context);
		textView.setLayoutParams(param);
		
		textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT);
		textView.setPadding(64, 0, 0, 0);
		
		return textView;
	}
	
	public int getRowId(int groupPosition) {
		return rowId[groupPosition];
	}
	

	@Override
	public Object getChild(int arg0, int arg1) {
		return children.get(arg0).get(arg1);
	}

	@Override
	public long getChildId(int arg0, int arg1) {
		// TODO Auto-generated method stub
		return arg1;
	}

	@Override
	public View getChildView(int arg0, int arg1, boolean arg2, View arg3,
			ViewGroup arg4) {
		// 子供のViewオブジェクトを作成
		View childView = getGenericView();
		TextView textView = (TextView)childView.findViewById(R.id.member_list);
		ItemDto dto  = children.get(arg0).get(arg1);
		textView.setText(dto.getName());
		
		Drawable icon = context.getResources().getDrawable(dto.getResourceId());
		int width = (int)(48 * context.getResources().getDisplayMetrics().density + 0.5f);
		icon.setBounds(0, 0, width, width);
		textView.setCompoundDrawables(icon, null, null, null);
		
		return childView;
	}

	@Override
	public int getChildrenCount(int arg0) {
		return children.get(arg0).size();
	}

	@Override
	public Object getGroup(int arg0) {
		return groups.get(arg0);
	}

	@Override
	public int getGroupCount() {
		return children.size();
	}

	@Override
	public long getGroupId(int arg0) {
		return arg0;
	}

	@Override
	public View getGroupView(int arg0, boolean arg1, View arg2, ViewGroup arg3) {
		TextView textView = getGroupGenericView();
		textView.setText(getGroup(arg0).toString());
		return textView;
	}

	@Override
	public boolean hasStableIds() {
		return true;
	}

	@Override
	public boolean isChildSelectable(int arg0, int arg1) {
		return true;
	}

}

重要なのはgetChildViewgetGroupViewです。

getChildView

子要素のViewを返します。今回は、ここでカスタムレイアウトのXMLをインフレートして単なるTextViewではなく、複数の要素を表示できるViewを作成しました。

引数名 役割
int groupPosition 親要素の位置
int childPosition 子供の要素の位置
boolean isLastChild 子要素の一番最後であるかどうか
View convertView オブジェクトをnewする処理というのは結構コストがかかります
そのためリソースが潤沢でない(最近はびっくりするほど潤沢ですが)Androidでは、同じような入れ物を使うならばすでに作ってあるものを再利用しようというキャッシュを利用します
convertViewがnullの時はViewを新規作成し、nullでなければ既存のconvertViewを利用することで、パフォーマンスの低下とメモリ使用量の増加を抑えられます
この辺はListViewをカスタマイズする時にも出てくる、ArrayAdapter#getViewの中のものと同じようなものと思われます
ViewGroup parent Viewが紐づく親のViewGroup。今のところ利用したことありません・・・

getGroupView

親要素のViewを返します。今回は、単なるTextViewをxmlレイアウトを使わずにプログラム上で生成します
※paddingをうまく調整してやらないと、左端に表示される「▽」にかぶってしまい見えませんでしたので要調整です。本来はdimens.xml等でちゃんと定義してあげた方が良いかと思いますが、今回は決め打ちで指定してます

引数名 役割
int groupPosition 親要素の位置
boolean isExpanded 親のListが展開済みかどうか
View convertView 上記と同じ(はず)!
ViewGroup parent 上記と同じ(はず)!

ExpandableListViewを配置する画面の定義は、以下のようにします

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

Activityで、Adapterを生成してデータを作り、ExpandableListViewにセットします

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ExpandableListView;

public class SampleExpandableListActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        ExpandableListView listView = (ExpandableListView)findViewById(R.id.sample_list);
        int[] rowId = {0,1,2};
        listView.setAdapter(new SampleExpandableListAdapter(this, rowId, createGroupItemList(), createChildrenItemList()));
    }
    
    /**
     * 
     * @return
     */
    private List<List<ItemDto>> createChildrenItemList() {
    	List<ItemDto> child = new ArrayList<ItemDto>();
    	child.add(new ItemDto("ねこ1"));
    	child.add(new ItemDto("ねこ2"));
    	child.add(new ItemDto("コーギー1"));
    	child.add(new ItemDto("コーギー2"));
    	child.add(new ItemDto("しばいぬ1"));
    	
    	List<ItemDto> child2 = new ArrayList<ItemDto>();
    	child2.add(new ItemDto("猫1", R.drawable.cat1));
    	child2.add(new ItemDto("猫2", R.drawable.cat2));
    	child2.add(new ItemDto("コーギー1", R.drawable.corgi1));
    	child2.add(new ItemDto("コーギー2", R.drawable.corgi2));
    	child2.add(new ItemDto("柴犬", R.drawable.siba1));
    	
    	List<List<ItemDto>> result = new ArrayList<List<ItemDto>>();
    	result.add(child);
    	result.add(child2);
    	
    	return result;
    }
    
    /**
     * 
     * @return
     */
    private List<String> createGroupItemList() {
    	List<String> groups = new ArrayList<String>();
    	groups.add("いぬ・ねこ(ドロイド君)");
    	groups.add("いぬ・ねこ(本人出演)");
    	return groups;
    }
}

これで完成です!

下だけ展開してみる。
上も展開してみる。

参考にさせてもらった元ネタ>http://y-anz-m.blogspot.jp/2010/08/androidexpandable-list.html

【おまけ】
Eclipseの「Shift+Alt+s」のメニューに出てくる「Override/Implement Method」は便利なのですが、引数が「int arg0」などになってしまって分かりづらいですね。この設定も(できるの?)要調査ですね