【Android】ListViewのHeader/FooterのViewに直接イベントを設定する

ListViewのHeader/FooterはAdapterを操作するのではなくListView#addHeaderViewもしくはListView#addFooterViewを使用するのが定石となっています。

ListViewにAdapterをセットする前にこのメソッドを呼ばなきゃいけないとか、上記のメソッドを呼ぶとListView#getAdapterで返ってくるAdapterの型が強制的にWrapedListAdapterにされてしまうとか、地味かつ嫌らしいハマりポイントがあったりしますが、そんなに難しいものではありません。後はHeader/FooterのViewをinflateする適当なUtilityメソッドを作っておけばOKです。

が、onItemClickListnerなどでイベントを発生させようとすると若干面倒です。まぁpositionとListView#getCountでなんとかなるんですが、私はあのパターンが嫌いすぎてListViewのアイテムそのものにイベントを持たせる男ですので、当然今回もクロージャを使ってHeader/Footerにイベントを持たせます。

下準備

とりあえずはそれっぽいコードを用意します。

public class TestActivity extends Activity {
    
    @Override
    public onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        
        ListView listview = (ListView) findViewById(R.id.listview);
        listview.addFooterView(getFooterView());
        
        String[] datas = {"test1", "test2"};
        
        ArrayAdapter<String> adapter
            = new ArrayAdapter<String>(getApplicationContext(), R.layout.adapter_test, datas);
        
        listview.setAdapter(adapter);
        
        listview.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                // TODO:check footer view.
            }
        });
    }
    
    public View getFooterView() {
        LayoutInflater layoutInflater = getLayoutInflater();
        return layoutInflater.inflate(R.layout.util_footer);
    }
}

Header/Footerかどうかを判定する

さて、ポイントとなるのはonItemSelectedのシグネチャであるView viewです。これはクリックされたviewが渡されてきます。

なので、inflateしたViewをメンバ変数なりなんなりで保持しておけば普通にequalsで判定させることが出来ます。

public class TestActivity extends Activity {
    
    private View _footer;
    
    @Override
    public onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        
        ListView listview = (ListView) findViewById(R.id.listview);
        _footer = getFooterView();
        listview.addFooterView(_footer);
        
        String[] datas = {"test1", "test2"};
        
        ArrayAdapter<String> adapter
            = new ArrayAdapter<String>(getApplicationContext(), R.layout.adapter_test, datas);
        
        listview.setAdapter(adapter);
        
        listview.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                if(view.equals(_footer)) {
                    // TODO: fire event
                    
                    return;
                }
            }
        });
    }
    
    public View getFooterView() {
        LayoutInflater layoutInflater = getLayoutInflater();
        return layoutInflater.inflate(R.layout.util_footer);
    }
}

Header/Footerにイベントを設定する

まぁぶっちゃけここまでやってしまえば後はif文の中にやりたいことを記述してしまえばいいんですが、そもそもHeader/Footerのクリック時処理と言うのはいわば例外的な処理であって、普通はAdapterに詰め込んだデータに対して操作を行いたいものです。ですから、出来るだけ簡便に記述してガード節で抜けてしまうのがベストだと思っています。あくまで私はね?みんなは無理してやらなくていいからね?

もし無理してやるのであれば、Viewクラスに用意されているsetTag(Object)メソッドがオススメです。

これはViewに対してどんなオブジェクトでも突っ込むことが出来る、どこの動的言語だよみたいな便利メソッドです。ViewHolderパターンでもキャッシュに使われていますね。って言うか、本来何に使うんですかね?

当然どんなオブジェクトでも入れられるんですから、関数オブジェクトを突っ込まれても文句は言えないはずです。

public class TestActivity extends Activity {
    
    private View _footer;
    
    @Override
    public onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        
        ListView listview = (ListView) findViewById(R.id.listview);
        _footer = getFooterView();
        _footer.setTag(new V1<String>(){
            @Override
            public void call(String x) {
                Toast.makeToast(getApplicationContext(), x, Toast.LENGTH_SHORT).show();
            }
        });
        listview.addFooterView(_footer);
        
        String[] datas = {"test1", "test2"};
        
        ArrayAdapter<String> adapter
            = new ArrayAdapter<String>(getApplicationContext(), R.layout.adapter_test, datas);
        
        listview.setAdapter(adapter);
        
        listview.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                if(view.equals(_footer)) {
                    _footer.getTag().call("hoge");
                    return;
                }
            }
        });
    }
    
    public View getFooterView() {
        LayoutInflater layoutInflater = getLayoutInflater();
        return layoutInflater.inflate(R.layout.util_footer);
    }
}

まとめ

実際には何らかの値を渡して次のデータを取ってきてadapterに追加する、みたいになると思います。

わざわざViewに関数オブジェクトを突っ込むメリットがあるとしたら、特定のイベントを持ったHeader/Footer用Viewを返すUtilityメソッドが作りやすい、ぐらいでしょうか。

まぁそれで得られるものも微々たるものです。どっちかって言うとView#setTagで関数オブジェクトを突っ込んでおけるよって話がしたかっただけでした。

って言うかそもそも、addHeaderView / addFooterViewにクリック時のハンドラを渡せるオーバーロードがあればよかったんですがねぇ。