【Android】ExpandableListViewの各データにイベントを設定する(完成版)

今更ですがあけましておめでとうございます。昨年からのやり残しがあったので消化していきます。

ExpandableParent<EventArg>

実際に出来たコードはこんな感じです。まずはExpandableParent<EventArg>。これは親グループになります。

public class ExpandableParent<EventArg> implements Serializable {

    private static final long serialVersionUID = 2918564478660933690L;
    protected String displayName;
    protected List<ExpandableChild<EventArg>> children = new ArrayList<ExpandableChild<EventArg>>();
    transient protected V1<EventArg> event;

    public ExpandableParent(String displayName) {
        this.displayName = displayName;
    }

    public ExpandableParent(String displayName, List<ExpandableChild<EventArg>> children) {
        this.displayName = displayName;
        this.children = children;
    }

    public ExpandableParent(String displayName, V1<EventArg> event) {
        this.displayName = displayName;
        this.event = event;
    }

    public String getDisplayName() {
        return this.displayName;
    }

    public List<ExpandableChild<EventArg>> getChildren() {
        return this.children;
    }

    public void addChild(ExpandableChild<EventArg> child) {
        children.add(child);
    }

    public void addChild(String displayName, V1<EventArg> event) {
        addChild(new ExpandableChild<EventArg>(displayName, event));
    }

    public boolean hasChild() {
        return !children.isEmpty();
    }

    public void setEvent(V1<EventArg> event) {
        this.event = event;
    }

    public boolean hasEvent() {
        return event != null;
    }

    public V1<EventArg> getEvent() {
        return this.event;
    }

    public void callEvent(EventArg arg) {
        this.event.call(arg);
    }

    @SuppressWarnings("unchecked")
    @Override
    public boolean equals(Object o) {
        if(o == null) return false;

        try {
            if(o instanceof ExpandableParent) {
                return displayName.equals(((ExpandableParent<EventArg>)o).getDisplayName());
            } else if (o instanceof String) {
                return displayName.equals((String)o);
            } else {
                return false;
            }
        } catch(ClassCastException e) {
            return false;
        }
    }

    @Override
    public int hashCode() {
        return displayName.hashCode();
    }

    @Override
    public String toString() {
        return displayName.toString();
    }

}

ExpandableChild<EventArg>

次にExpandableChild<EventArg>です。これは子要素になります。

public class ExpandableChild<EventArg> implements Serializable {

    private static final long serialVersionUID = -713600160473673018L;
    protected String displayName;
    transient protected V1<EventArg> event;

    public ExpandableChild(String displayName, V1<EventArg> event) {
        this.displayName = displayName;
        this.event = event;
    }

    public String getDisplayName() {
        return this.displayName;
    }

    public V1<EventArg> getEvent() {
        return this.event;
    }

    public void callEvent(EventArg arg) {
        this.event.call(arg);
    }

    @SuppressWarnings("unchecked")
    @Override
    public boolean equals(Object o) {
        if(o == null) return false;

        try {
            if(o instanceof ExpandableChild) {
                return displayName.equals(((ExpandableChild<EventArg>)o).getDisplayName());
            } else if (o instanceof String) {
                return displayName.equals((String)o);
            } else {
                return false;
            }
        } catch(ClassCastException e) {
            return false;
        }
    }

    @Override
    public int hashCode() {
        return displayName.hashCode();
    }

    @Override
    public String toString() {
        return displayName.toString();
    }
}

Parent及びChildのeventにtransientがついてるんですが、こうしないとActivity#onSaveInstanceStateで落ちます。

恐らく匿名クラスを使用しているせいで呼び出し元のクラスも巻き込んでシリアライズしようとしてしまうんだと思います。(参考:1.10 Serializable インタフェース 注釈)本格的な調査はしていないんですが、transientを使用してもBundle#putSerialize、Activity#onSaveInstanceStateによってイベントそのものが消滅することはないですし、別に本当にシリアライズしてどこかに保存しておきたいものでもないので、妥協しています。

また、Adapterの方で実際にViewをinflateする時このままだと表示文字列しか設定できないので、継承前提の作りにしてあります。そう言う意味では別にabstractにしてもいいんですが、具象クラスとしても機能はするのでそのままです。

EventExpandableAdapater<EventArg>

では次にEventExpandableAdapater<EventArg>。

public class EventExpandableAdapater<EventArg> extends BaseExpandableListAdapter {

    private List<? extends ExpandableParent<EventArg>> _parents;
    private LayoutInflater _layoutInflater;

    public EventExpandableAdapater(List<? extends ExpandableParent<EventArg>> parents, Context cont) {
        _parents = parents;
        _layoutInflater = (LayoutInflater)cont.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    @Override
    public Object getChild(int groupPosition, int childPosition) {
        ExpandableParent<EventArg> p = _parents.get(groupPosition);
        if(p.hasChild()) {
            return p.getChildren().get(childPosition);
        } else {
            return null;
        }
    }

    @Override
    public long getChildId(int groupPosition, int childPosition) {
        return childPosition;
    }

    @Override
    public View getChildView(int groupPosition, int childPosition, boolean isLastChild
            , View convertView, ViewGroup parent) {

        ExpandableParent<EventArg> p = _parents.get(groupPosition);
        if(!p.hasChild()) return null;

        ExpandableChild<EventArg> child = p.getChildren().get(childPosition);

        ViewHolder vh = new ViewHolder();
        View view = convertView;

        if (view == null) {
            view = _layoutInflater.inflate(R.layout.adapter_clickable_item, null);
            vh.txvClickableItemName = (TextView)view.findViewById(R.id.txvClickableItemName);
            view.setTag(vh);
        }
        else {
            vh = (ViewHolder)view.getTag();
        }

        vh.txvClickableItemName.setText(child.getDisplayName());

        return view;
    }

    @Override
    public int getChildrenCount(int groupPosition) {
        ExpandableParent<EventArg> p = _parents.get(groupPosition);
        if(p.hasChild()) {
            return p.getChildren().size();
        } else {
            return 0;
        }
    }

    @Override
    public Object getGroup(int groupPosition) {
        return _parents.get(groupPosition);
    }

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

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

    @Override
    public View getGroupView(int groupPosition, boolean isLastChild, View convertView, ViewGroup parent) {
        ExpandableParent<EventArg> p = _parents.get(groupPosition);

        ViewHolder vh = new ViewHolder();
        View view = convertView;

        if (view == null) {
            view = _layoutInflater.inflate(R.layout.adapter_clickable_item, null);
            vh.txvClickableItemName = (TextView)view.findViewById(R.id.txvClickableItemName);
            view.setTag(vh);
        }
        else {
            vh = (ViewHolder)view.getTag();
        }

        vh.txvClickableItemName.setText(p.getDisplayName());

        return view;
    }

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

    @Override
    public boolean isChildSelectable(int groupPosition, int childPosition) {
        return true;
    }

    public boolean hasChild(int groupPosition) {
        return _parents.get(groupPosition).hasChild();
    }

}

LayoutInflaterで呼び出しているadapter_clickable_itemは単にTextViewが一個だけあるテスト用なので特に気にしなくてOKです。

ただし、子だからと言って自動でインデントしてはくれないので、xmlそのものをわけたり、マージンを設定する必要はあると思います。

getChildViewの方で子要素がない場合NULLを返していますが、これは特に問題ありませんでした。もしかしたらNULLチェックすら必要ないかもしれません。

selector.xml

次にselectorです。これはres/drawableにxmlとして配置します。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:state_empty="true" android:drawable="@android:color/transparent"/>
</selector>

android:drawable=”@android:color/transparent”を指定することでIndicatorを非表示にすることが出来ます。

state_emptyだけ設定すれば他はデフォルトになるかな?と思ったらそんなに上手くはいかず、どの状態でも非表示になってしまいました。必要であればこれを参考に何らかのdrawableを指定する必要がありそうです。

実際にExpandableListViewにセットする場合はxmlの方でandroid:groupIndicator=”@drawable/selector”とするか、ExpandableListView#setGroupIndicatorメソッドでセットしましょう。

実際の呼び出し

前回の例と同じです。

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    ExpandableListView exListView = (ExpandableListView)findViewById(R.id.ex_listview);
    
    List<ExpandableParent<String>> parents = new ArrayList<ExpandableParent<String>>();
    
    parents.add(new ExpandableParent<String>("親要素だけ", new V1<String>() {
        @Override
        public void call(String arg) {
            Toast.makeText(getApplicationContext(), arg, Toast.LENGTH_SHORT).show();
        }
    }));
    
    ExpandableParent<String> parent = new ExpandableParent<String>("子要素もあるやつ");
    parent.addChild("こども", new V1<String>() {
        @Override
        public void call(String arg) {
            Toast.makeText(getApplicationContext(), arg, Toast.LENGTH_SHORT).show();
        }
    });
    
    parents.add(parent);
    
    exListView.setAdapter(new EventExpandableAdapater<String>(parents, getApplicationContext());
    
    exListView.setOnGroupClickListener(new OnGroupClickListener() {
        @Override
        public boolean onGroupClick(ExpandableListView parentListView, View v, int groupPosition, long id) {
            EventExpandableAdapater<String> adapter = (EventExpandableAdapater<String>)parentListView.getAdapter();
            
            if(adapter.hasChild(groupPosition)) return false;
            
            ExpandableParent<EventArg> parent = adapter.getParent(groupPosition);
            parent.callEvent(parent.getDisplayName());
            
            return true;
        }
    });
    
    exListView.setOnChildClickListener(new OnChildClickListener() {
        @Override
        public boolean onChildClick(ExpandableListView parentListView, View v, int groupPosition, int childPosition, long id) {
            EventExpandableAdapater<String> adapter = (EventExpandableAdapater<String>)parentListView.getAdapter();
            ExpandableChild<String> child = (ExpandableChild<String>)adapter.getChild(groupPosition, childPosition);
            
            if(child != null) child.callEvent(child.getDisplayName());
            
            return false;
        }
    });
    
}

まとめ

実は年末には出来てたんですが、eventのシリアライズがらみでハマり、今日やっと解決しました。