listSelector のハマりどころ

# `listSelector` とは

Android の ListView や GridView で、タッチ時の表示をカスタマイズするときには、[listSelector 属性](http://developer.android.com/reference/android/widget/AbsListView.html#attr_android:listSelector) を指定するやり方が一般的です。これは、[StateListDrawable](http://developer.android.com/guide/topics/resources/drawable-resource.html#StateList) と呼ばれる Drawable を `listSelector` 属性に指定すると、各要素の選択時に、その Drawable が使われる、というものです。

以下の動画で、左はデフォルト、右は `listSelector` を指定したものです。
![demolistviewdefault.gif](https://qiita-image-store.s3.amazonaws.com/0/36306/8914ddf9-aceb-c51e-081a-f1133263b1ba.gif) ![demolistviewcustom.gif](https://qiita-image-store.s3.amazonaws.com/0/36306/a172843a-1f08-fac5-78a5-d3132f0baa5f.gif)

右のように、タッチ時およびフォーカス時の背景色を変えたいときは、以下のような drawable を作成します。タップされたときに背景色がオレンジ(`#F08000`)になるように、また、フォーカスされたときに背景色が薄いオレンジ色(`#44F08000`)で、外枠が同じオレンジになるようにしています。

```xml:res/drawable/listselector.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape android:shape="rectangle">
            <solid android:color="#F08000"></solid>
        </shape>
    </item>
    <item android:state_focused="true">
        <shape android:shape="rectangle">
            <solid android:color="#44F08000"></solid>
            <stroke android:color="#F08000" android:width="2dp"/>
        </shape>
    </item>
</selector>
```

これを、以下のように ListView や GridView の `listSelector` に指定すれば、カスタム selector が適用されます。

```xml:res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ListView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:listSelector="@drawable/listselector"/>
</LinearLayout>
```

# 参考文献
この記事で参考にした文献です。

- [Touch Feedback | Android Developers](http://developer.android.com/design/style/touch-feedback.html) Android のタッチフィードバックについてのガイドラインです。
- [Drawable Resources | Android Developers](http://developer.android.com/guide/topics/resources/drawable-resource.html#StateList) `StateListDrawable` について。`listSelector` に指定する drawable を書くときに参考になります
- [ListView Tips & Tricks #3: Create Fancy ListViews - Cyril Mottier](http://cyrilmottier.com/2011/08/08/listview-tips-tricks-3-create-fancy-listviews/) ListView に関する tips を連載した記事の第3回ですが、この回は `listSelector` について説明されています。他の記事も参考になりますので、良かったらどうぞ。
- [Android - ListViewのlistSelectorを使ってタップしたときに色を変える - Qiita](http://qiita.com/sassy_watson/items/061d370ca6d11d42b7e7) 下で解説する「`select` の子要素に `color` を使うべきではない」という話について触れています


# `listSelector` のハマりどころ

さて、この `listSelector` にお世話になることが多いのですが、いくつか独特の癖があります。ここでは、そのような `listSelector` のハマりどころを5つ紹介したいと思います。

## 1. `listSelector` で指定された drawable は、セルの背後に描画される

リストの各要素に画像を表示したいとしましょう。まずは、リスト要素を表すレイアウトファイルに ImageView を足します。そうすると、リストに画像が並びます。そこでタップしてみると、以下のように `listSelector` で指定したタッチフィードバックがテキスト部分にしか適用されないことに気付くでしょう。

![demolistviewpic.gif](https://qiita-image-store.s3.amazonaws.com/0/36306/2c88afd2-beed-2678-512a-eb50c646c879.gif)

この原因は、ListView の描画順にあります。ListView は、下図のように、`listSelector` が ListView の各要素よりも下で描画されるのです。デフォルトでは、リスト要素の背景色は透明なので、`listSelector` は期待したとおりに動きます。しかし、背景が不透明なものが1つでもリスト要素に登場すると、この問題に当たってしまいます。

![](http://cyrilmottier.com/media/2011/08/listview-tips-tricks-3-create-fancy-listviews/draw_order_s.png)
([ListView Tips & Tricks #3: Create Fancy ListViews - Cyril Mottier](http://cyrilmottier.com/2011/08/08/listview-tips-tricks-3-create-fancy-listviews/) より)

この問題は、リスト要素に指定したレイアウトファイルで以下のように背景色を指定してしまった場合も同様です。その場合は、いっさい `listSelector` が表示されなくなります。

```xml:res/layout/list_item_simple.xml
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/text1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white"
```

## 2. `drawSelectorOnTop` を指定すると前面に表示される
さて、それでは、どのように解決したら良いでしょうか。1つの方法は、各要素の一番上に透明の View を被せて、タップ時に、その View の背景色を変更することです。

もう1つの方法は、ListView の `drawSelectorOnTop` 属性を使う方法です。この属性が `true` のときは、selector は ListView の上に表示されます。さきほどの例で、この属性を `true` にしてみましょう。

```xml:res/layout/activity_main.xml
    <ListView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:listSelector="@drawable/listselector"
        android:drawSelectorOnTop="true"/>
```

![demolistviewpic2.gif](https://qiita-image-store.s3.amazonaws.com/0/36306/2cde9520-b3e5-f593-7ba6-ff59fd18846b.gif)

ご覧のように、無事に selector が画像の上に表示されるようになりました。一点、注意点としては、selector で指定する背景色を透過にしておくことです。そうしないと、タップ時にリスト要素が塗り潰されてしまいます。

## 3. `listSelector` はタップされた要素にしか描画されない
これも、初めて `listSelector` を使うときにハマりがちな点です。`listSelector` で以下のように記述したとしましょう。

```xml:res/drawable/listselector.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape android:shape="rectangle">
            <solid android:color="#F08000"></solid>
        </shape>
    </item>
    <item android:state_focused="true">
        <shape android:shape="rectangle">
            <solid android:color="#44F08000"></solid>
            <stroke android:color="#F08000" android:width="2dp"/>
        </shape>
    </item>
    <item>
        <shape android:shape="rectangle">
            <solid android:color="#FFFFFF"></solid>
        </shape>
    </item>
</selector>
```

このとき、タップしていないとき(クリックもフォーカスもしていないとき)は、最後の要素で指定した背景色が白(`#FFFFFF`)の設定が使われると思うかもしれません。残念ながら、そうなりません。タップが終わった直後は期待通りに白くなります。しかし、最初に ListView を表示したときには、背景色は白ではなく、デフォルトの透過色になっています。

![demolistviewwhite.gif](https://qiita-image-store.s3.amazonaws.com/0/36306/469c5c7a-310d-b20b-388b-e69b467938ba.gif)

これは、`listSelector` が、そもそも選択された要素の背景を指定するものだからです(API ドキュメントには `Drawable used to indicate the currently selected item in the list.` と書かれています)。リスト中で選択されていない要素には、`listSelector` が使われないため、デフォルトの透過色になってしまうのです。

StackOverflow にも似たような質問と回答([Changing background color of ListView items on Android - Stack Overflow](http://stackoverflow.com/questions/4730691/android-listview-state-list-not-showing-default-item-background))がありますが、AbsListView のコードで、以下のように、`mSelector` が AbsListView 内に一つしかないことも、この考えを裏付けています。

```java:android/widget/AbsListView.java
    void positionSelector(int position, View sel) {
        if (position != INVALID_POSITION) {
            mSelectorPosition = position;
        }
        ...
        // Update the selector drawable.
        final Drawable selector = mSelector;
        if (selector != null) {
            selector.setBounds(selectorRect);
        }
```

従って、もし非選択時のリスト要素の背景を透過色以外にしたいなら、StateListDrawable を `listSelector` ではなく、以下のようにリスト要素の背景に指定する方が良いでしょう。

```xml:res/layout/simple_list_item.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="@drawable/listselector"
    android:orientation="horizontal">
    ...
</LinearLayout>
```

ただし、この方法を取るときは、すべての要素が同じレイアウトを利用しているか、あるいはそれぞれのレイアウト間で同じ背景が指定されているか確認しましょう。さもないと、一部の要素だけタッチフィードバックや外観が変わってしまうことになります。

## 4. `selector` 要素の子要素は、上から順に評価され、最初に一致したものが使われる

これは、`listSelector` に限った話ではありませんが、StateListDrawable は公式ドキュメントにも書かれているように、上から順に評価され、最初に一致したものが使われます。

> During each state change, the state list is traversed top to bottom and the first item that matches the current state is used—the selection is not based on the "best match," but simply the first item that meets the minimum criteria of the state.

([Drawable Resources | Android Developers](http://developer.android.com/guide/topics/resources/drawable-resource.html#StateList)より)

従って、以下のような書き方をすると、タップ時に真っ白になってしまうので気を付けましょう。最初の要素がすべてを受けてしまうからです。

```xml:res/drawable/listselector.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="rectangle">
            <solid android:color="#FFFFFF"></solid>
        </shape>
    </item>
    <item android:state_pressed="true">
        <shape android:shape="rectangle">
            <solid android:color="#F08000"></solid>
        </shape>
    </item>
    <item android:state_focused="true">
        <shape android:shape="rectangle">
            <solid android:color="#44F08000"></solid>
            <stroke android:color="#F08000" android:width="2dp"/>
        </shape>
    </item>
</selector>
```

## 5. `item` に ColorDrawable は指定しない方が良い
[Android - ListViewのlistSelectorを使ってタップしたときに色を変える - Qiita](http://qiita.com/sassy_watson/items/061d370ca6d11d42b7e7) で解説されていますが、以下のように `listSelector` に `shape` ではなく `color` 要素を使うのは避けた方が良いです。

```xml:res/drawable/listselector.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <color android:color="#F08000" />
    </item>
    <item android:state_focused="true">
        <color android:color="#44F08000" />
    </item>
</selector>
```

Android 2.x では、タップ時に ListView 全体がその色で塗り潰されてしまうことが知られています([ListView Tips & Tricks #3: Create Fancy ListViews - Cyril Mottier](http://cyrilmottier.com/2011/08/08/listview-tips-tricks-3-create-fancy-listviews/) の **List selector** の項でも「バグ」と指摘されています)。

そもそも、ColorDrawable には境界が存在しない(`Drawable#getIntrinsicHeight()` が `-1` を返す)ので、`selector` がキャンバスいっぱいに描画されてしまうのでしょう。`listSelector` を使うときは ColorDrawable を使わないようにしましょう。

# まとめ
以上のように、`listSelector` は、リスト全体にタッチフィードバックを適用したいときは非常に便利ですが、ちょっと癖があるので、気を付けないと訳の分からないバグに悩まされてしまいます。そんなときは、上に述べた内容をぜひ思い出してみてください。

本記事の内容がいくばくかでもお役に立てれば幸いです。