70
76

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GunosyAdvent Calendar 2014

Day 9

listSelector のハマりどころ

Last updated at Posted at 2014-12-11

listSelector とは

Android の ListView や GridView で、タッチ時の表示をカスタマイズするときには、listSelector 属性 を指定するやり方が一般的です。これは、StateListDrawable と呼ばれる Drawable を listSelector 属性に指定すると、各要素の選択時に、その Drawable が使われる、というものです。

以下の動画で、左はデフォルト、右は listSelector を指定したものです。
demolistviewdefault.gif demolistviewcustom.gif

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

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 が適用されます。

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>

参考文献

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

listSelector のハマりどころ

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

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

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

demolistviewpic.gif

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


(ListView Tips & Tricks #3: Create Fancy ListViews - Cyril Mottier より)

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

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 にしてみましょう。

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

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

3. listSelector はタップされた要素にしか描画されない

これも、初めて listSelector を使うときにハマりがちな点です。listSelector で以下のように記述したとしましょう。

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

これは、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)がありますが、AbsListView のコードで、以下のように、mSelector が AbsListView 内に一つしかないことも、この考えを裏付けています。

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 ではなく、以下のようにリスト要素の背景に指定する方が良いでしょう。

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より)

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

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 で解説されていますが、以下のように listSelectorshape ではなく color 要素を使うのは避けた方が良いです。

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 MottierList selector の項でも「バグ」と指摘されています)。

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

まとめ

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

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

70
76
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
70
76

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?