【ListView Tips Vol.2】忙しい人のためのStickyListHeadersListViewの使い方 〜基本編〜


AndroidのViewの中でも頻出かつクセのある **ListView**関連のTips集Vol.2


カテゴリ毎にHeaderが追従するタイプのListViewである、`StickyListHeadersListView`の基本的な使い方と使用上の注意点。

## はじめに

使い方については以下の本家GitHubを参考にしましょう。

[emilsjolander/StickyListHeaders](http://github.com/emilsjolander/StickyListHeaders)


とはいえ、

* そんなの読んでる時間ないよ！
* 英語きっついわー。。。
* すぐにでもStickyListHeadersの世界を体感したい！

という忙しい人は以下をお読みください。



## ライブラリの登録
`build.gradle.xml`に以下を追加する。

```build.gradle.xml
dependencies {
    compile 'se.emilsjolander:stickylistheaders:2.6.0'
}

```

これで準備は完了！

「あれっ？使えないぞ？」
という人は、落ち着きましょう。

**ネットにつないで、一度BuildしてデータをDLしてくる必要があります。**
（本家サイトでは、`git clone`のやり方も書いてあります）


## `StickyListHeadersListView`の基本構造
StickyListHeadersListViewはその名の通りカスタムListViewです。
なので、ListViewの基本的な使い方に不安がある人は、まずそちらを復習しましょう！
（ここでは割愛します）

#### ■ データBindはListViewと同じ

ListViewにデータを入れ込む代表的な方法としてAdapter経由で行うものがありますが、StickyListHeadersListViewでも同じ。
基本は`StickyListHeadersAdapter`をimplementsしたAdapterを使いましょう。
<br>

#### ■ 何が違うか -> Headerがsticky

このViewを使いたい人には自明ですが、StickyListHeadersListViewはHeader部分がカテゴリ毎に追従してきます。
ListViewでは`getView`に処理を書けば大体OKですが、StickyListHeadersListViewではHeaderの動きを制御する **`getHeaderId`**、HeaderのViewを扱う **`getHeaderView`**にきちんと処理を書かないとうまく動きません。
<br>

#### ■ Headerがstickyになる仕組み
詳細は後述しますが、`getHeaderId`にて、アイテム行がどのHeaderグループに所属するのかを判定しています。

例えば、


|リストPosition|`getHeaderId`の返り値（※1）|Headerグループ|Header生成（※2）|
|:-:|:-:|:-:|:-:|
|0|1|A|◯|
|1|1|A|-|
|2|1|A|-|
|3|2|B|◯|
|4|2|B|-|
|5|3|C|◯|
|6|4|D|◯|
|7|4|D|-|
|8|4|D|-|
|9|4|D|-|
|10|5|E|◯|
|11|5|E|-|

のようになります。
`getHeaderId`の返り値は連続していなくて良いですが、 **`long`型**である必要があります。
キャストされるので、`int`型あたりが返っていれば大丈夫です。

※1: 返り値は適当です（long型であればなんでも）
※2: 「-」は生成されたものが生き残るので再生成はされない

仕組み自体は簡単ですね。
<br>
**つまり、Stickyに動かしたい範囲（＝所属させたいHeaderグループ）毎に、同じ`getHeaderId`を返すようにすればよい！！**

ということです！

## 実装方法

#### ■ ファイルと役割
以下のファイルを参考にしてください。

|ファイル名|役割|
|:-:|:-:|
|StickyActivity.java|RootActivity|
|StickyAdapter.java|データバインド用Adapter|
|main_sticky.xml|StickyActivityのレイアウト|
|sticky_header_row.xml|StickyListのHeaderレイアウト|
|sticky_item_row.xml|StickyListのItemレイアウト|

注意点は`StickyAdapter.java`における`getHeaderId`の扱いです。
<br>

#### ■ ファイル詳細

まずはRootActivityです。

``` StickyActivkty.java
public class StickyActivity extends Activity {

    private StickyListHeadersListView mListView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_sticky);

        mListView = (StickyListHeadersListView) findViewById(R.id.sticky_listview);
    }

    @Override
    protected void onResume() {
        super.onResume();

        StickyAdapter adapter = new StickyAdapter(this, android.R.layout.simple_list_item_1, createSampleArray());
        mListView.setAdapter(adapter);
    }

    // Adapterに渡すサンプルListを生成します
    private List<String> createSampleArray() {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            list.add("position: " + i);
        }
        return list;
    }
}
```
<br>

次はデータを入れ込むAdapterを見てみましょう。
注意点はこれまでお話してきた通りです。

``` StickyAdapter.java
public class StickyAdapter extends ArrayAdapter<String> implements StickyListHeadersAdapter {

    private LayoutInflater mInflater;

    public StickyAdapter(Context context, int resource, List<String> itemList) {
        super(context, resource, itemList);

        mInflater = LayoutInflater.from(context);
    }

    // Headerのレイアウトを定義します
    @Override
    public View getHeaderView(int i, View view, ViewGroup viewGroup) {
        HeaderViewHolder holder;

        if (view == null) {
            view = mInflater.inflate(R.layout.sticky_header_row, null);
            holder = new HeaderViewHolder(view);
            view.setTag(holder);
        } else {
            holder = (HeaderViewHolder) view.getTag();
        }
        holder.textView.setText("Header: " +getHeaderItem(i));

        return view;
    }

    // 【重要】Headerグループ毎に同じ値を返すようにしましょう
    // ここではItemポジションを5で割った商が同じになるものをグループにします（5個/グループ）
    // getHeaderItemを参考にしましょう
    @Override
    public long getHeaderId(int i) {
        return getHeaderItem(i);
    }

    // getItemにならい、Header版も作る優しさが持てるといいですね
    // 各アイテムのHeaderはポジションを5で割った商です
    // getHeaderIdを参考にしましょう
    public int getHeaderItem(int position) {
        return position / 5;
    }

    // おなじみのgetViewです
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ItemViewHolder holder;

        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.sticky_item_row, parent, false);
            holder = new ItemViewHolder(convertView);
            convertView.setTag(holder);
        } else {
            holder = (ItemViewHolder) convertView.getTag();
        }

        // Adapterに渡されたテキストを入れます
        holder.textView.setText(getItem(position));

        return convertView;
    }

    public static class HeaderViewHolder {
        TextView textView;

        // コンストラクタ内でidバインドを行なうとスッキリします
        public HeaderViewHolder(View view) {
            textView = (TextView) view.findViewById(R.id.header_textview);
        }
    }

    public static class ItemViewHolder {
        TextView textView;

        // コンストラクタ内でidバインドを行なうとスッキリします
        public ItemViewHolder(View view) {
            textView = (TextView) view.findViewById(R.id.item_textview);
        }
    }
}
```

<br>
お疲れ様です。
あとはレイアウト関連をちゃちゃっと片付けましょう。

``` main_sticky.xml
<?xml version="1.0" encoding="utf-8"?>
<se.emilsjolander.stickylistheaders.StickyListHeadersListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/sticky_listview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" />

```

<br>

``` sticky_header_row.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:background="@android:color/holo_red_light"
    android:orientation="vertical">

    <TextView
        android:id="@+id/header_textview"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:gravity="center_vertical"
        android:text="test"
        android:textColor="@android:color/white"
        android:textSize="14sp" />

</LinearLayout>
```

<br>

``` sticky_item_row.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="56dp"
    android:orientation="vertical">

    <TextView
        android:id="@+id/item_textview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_vertical"
        android:text="item"
        android:textSize="16sp" />
</LinearLayout>
```

## 実装結果
以下の様な挙動ができたら成功です！
（marginなど各種数値は最低限しかセットしていません）

<br>

![sticky_sample_app.gif](https://i.gyazo.com/34b52b63f186ada05cbfd4e901726182.gif)

<br>

それではStickyな世界を楽しみましょう！

※注意
レイアウトはもっとカッコよくしましょう。
サンプルはダサくて泣けます。。笑
<br>

## ListView Tips集

* [【ListView Tips Vo.1】ListViewの子要素（アイテム）のClickがおかしくなる 〜主な原因と対処法〜](http://qiita.com/taisho/items/44dfd83e19dc16b9a831)
* 【ListView Tips Vol.2】忙しい人のためのStickyListHeadersListViewの使い方 〜基本編〜
* [【ListView Tips Vol.3】ListViewでアイテムアニメーションしよう！ 〜View.startAnimationに関する考察〜](http://qiita.com/taisho/items/efdc04099ced5692dd1c)
* [【ListView Tips Vol.4-1】ButterKnifeで効率よくIDバインドする 〜findViewByIdもOnClickも、もういらない（仮）〜](http://qiita.com/taisho/items/5f66712d96fd0683a8ca)