Help us understand the problem. What is going on with this article?

ButterKnife で ListView に OnItemClick を適用するときの注意

More than 5 years have passed since last update.

ButterKnife、便利ですよね。Activity が沢山のなんちゃら Listener を implements しなくても、アノテーションを付けるだけでその辺を勝手によしなにしてくれますし、findViewById の手間も @InjectView で解決できます。

さて、ListViewにはOnItemClickListenerという、ListViewの中のアイテムをクリックした時のハンドラがあります。
ButterKnife を用いると、以下のようにサクッと書くことが出来るようになりますね。

public class ListViewActivity extends Activity {
    @InjectView(R.id.listview)
    ListView listview;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_listview);
        ButterKnife.inject(this);

        // adapter をセットする
    }

    @OnItemClick(R.id.listview)
    public void onListItemClick(int position) {
        Log.v("ListView", "selected pos=" + position);
    }
}

onItemClickメソッドの引数は、AdapterView.OnItemClickListener#onItemClick(AdapterView<?>, View, int, long)の引数の全部またはどれか好きなものを指定できます。おそらく最もよく使うのは、どのpositionのものを選択したかを判定したいので、第三引数のpositionなのではないでしょうか。今回もそれを使っています。

ListViewのアイテムをクリックすると、選択した位置がログに出力されるはずです。

ここで、以下のような実装をしてみたとします。

public class ListViewActivity extends Activity {
    @InjectView(R.id.listview)
    ListView listview;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_listview);
        ButterKnife.inject(this);

        // ListView にヘッダを追加する
        TextView label = new TextView(this);
        label.setText("List header!");
        listview.addHeaderView(label);

        // adapter をセットする
    }

    @OnItemClick(R.id.listview)
    public void onListItemClick(int position) {
        Log.v("ListView", "selected pos=" + position);
    }
}

ヘッダに View を追加しました。簡単ですね。

ListViewのアイテムをクリックすると、選択した位置がログに出力されるはずです。ヘッダもクリック出来るようになっていると思います。
が、よく見てみると、positionがヘッダを起点に0からカウントされていることがわかると思います。

つまり、セットしたListAdapterのアイテムのpositionではなく、追加したヘッダの分だけずれたpositionが引数に渡されてきます。

ヘッダやフッタをListViewにセットした場合、最初にListView#setAdapter(ListAdapter)でセットしたListAdapterではない別のアダプタが内部的に使用されます。つまり、ListView#getAdapter()をすると、その内部的に使用されているアダプタが返ってくることになります。これはHeaderViewListAdapterと呼ばれ、最初にセットしたListAdapterはこのHeaderViewListAdapterが内部に保持しています。よって、positionから正しい(ListAdapterが持つアイテムの0から始まる)位置を知るには、以下のような実装をしてあげないといけません。

public class ListViewActivity extends Activity {
    @InjectView(R.id.listview)
    ListView listview;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_listview);
        ButterKnife.inject(this);

        // ListView にヘッダを追加する
        TextView label = new TextView(this);
        label.setText("List header!");
        listview.addHeaderView(label);

        // adapter をセットする
    }

    @OnItemClick(R.id.listview)
    public void onListItemClick(int position) {
        //ヘッダやフッタを追加した ListView から getAdapter するときは必ずこうする
        HeaderViewListAdapter wrapper = (HeaderViewListAdapter) listview.getAdapter();

        // 正しい位置を計算するため、ヘッダの数だけ position がズレるので引き算する
        int itemPosition = position - wrapper.getHeadersCount();
        Log.v("ListView", "selected pos=" + itemPosition);

        // ズレた分の引き算で itemPosition が負数になる場合は position がヘッダの位置だったと分かる
        if (itemPosition < 0) {
            return;
        }

        // 選択されたアイテムを引くときはこれを使う
        ListAdapter wrapped = wrapper.getWrappedAdapter();
        Object selectedItem = wrapped.getItem(itemPosition);
    }
}

勿論、フッタがある場合はそれも考慮する必要がありますし、両方あるときはそれに応じて計算が必要です。

KeithYokoma
Android, Java, Perl, Scala, Play Framework, CoffeeScript, Smalltalk, JavaFX, Groovy, AWS, Docker
http://keithyokoma.hatenablog.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away