プログラマってこんなかんじ??

アプリ作ったり歌ったりしてます

ListView の高速スクロール時ガイドを編集する(AbsListView#setFastScrollEnabled)

Android Advent Calendar 12/18 担当の @daichan4649 です。
豪華メンバーがものすごく有意義なネタを記載されている中で、
空気を無視して ListView について誰得なネタを書いてみるとします。


ListViewには項目が多い場合に 「高速スクローラ」 を表示する機能がありますよね。
このとき 「スクロール中に項目のどのあたりを表示しているか」 を示す、
ガイドみたいなヤツを表示することができたりもします。


高速スクローラ、ガイド表示の実装に必要なのはこの2つ。

  1. AbsListView#setFastScrollEnabled(true)
  2. 1のListViewに設定するAdapterに SectionIndexer をimplementsする


1で高速スクローラを有効にし、
2でガイド表示内容を実装する、と。
実装してみると実際のイメージはこんなかんじになります。


初期実装

この例ではガイドに 「画面内先頭要素のindex値」 をそのまま表示してます。
右側に表示されているスクローラをドラッグすると、
リスト先頭項目に合わせてガイド表示内容も変わっていく、という感じです。


本題に入ります。


このガイド。
普通にいじろうとすると 指定した文字列を表示する、くらいしかできない。
濃いグレーの正方形 は編集できない。


編集できないと言われるといじりたいじゃないですか。


ということで、framework のソース読んで試してみました。
とりあえず今回はこのグレーの正方形を変えるところを実験してみるとします。
いつものドロイド君アイコン画像に差し替えてみたり。


カスタム実装

変更OK!!


では、どんなかんじで差し替えたかを書いてみます。


まず、基本から。
AbsListView#setFastScrollEnabled(true) を呼び出すと
「高速スクローラとガイド」 を管理している FastScrollerインスタンス
AbsListView内で生成されます。
しかし、ガイドのインスタンスが FastScrollerクラス 内に隠蔽されていて、
外部から編集できないようになっていたりします。
↓の FastScrollerインスタンス の中をいじりたいのですが。。。

// AbsListView.java
public void setFastScrollEnabled(boolean enabled) {
    mFastScrollEnabled = enabled;
    if (enabled) {
        if (mFastScroller == null) {
            mFastScroller = new FastScroller(getContext(), this);
        }
    } else {
        if (mFastScroller != null) {
            mFastScroller.stop();
            mFastScroller = null;
        }
    }
}

とまぁ、どうしようもなかったのでリフレクションでいじりましたw
リフレクションでいじるポイント、タイミングは以下。

  1. AbsListView#setFastScrollEnabled(true) を呼び出す
  2. その直後にリフレクションで内部のFastScrollerインスタンスを差し替える


実際のソースはこんな感じになります。
https://gist.github.com/1384757

// 今回のポイントになるリフレクション部分だけ貼りつけてみます

@Override
protected void onCreate(Bundle savedInstanceState) {

    (抜粋)

    final ListView listView = (ListView) findViewById(R.id.listview);

    // 高速スクローラ有効化設定
    listView.setFastScrollEnabled(true);

    // AbsListView#setFastScrollEnabled 直後に実行
    Drawable overlay = getResources().getDrawable(R.drawable.ic_launcher);
    customizeFastScroller(listView, overlay);
}

private void customizeFastScroller(AbsListView listView, Drawable overlay) {
    try {
        // 新FastScrollerインスタンス生成
        Class<?> clazz = Class.forName("android.widget.FastScroller");
        Constructor<?> constructor = clazz.getConstructor(Context.class, AbsListView.class);
        Object newFastScroller = constructor.newInstance(this, listView);

        // ガイド用drawable(グレーの四角)を上書き
        Field fieldOverlay = clazz.getDeclaredField("mOverlayDrawable");
        fieldOverlay.setAccessible(true);
        fieldOverlay.set(newFastScroller, overlay);

        // FastScrollerインスタンス(オリジナル)を上書き
        Field orgFastScroller = AbsListView.class.getDeclaredField("mFastScroller");
        orgFastScroller.setAccessible(true);
        orgFastScroller.set(listView, newFastScroller);

    } catch (Exception e) {
        e.printStackTrace();
    }
}

見てわかるとおり、強引な手法ですw
この方法を使えば 「スクローラ画像」 自体も差し替え可能ですね。
それについてはまた後日書こうと思います。(gistにはコード載せてます)


最後に。
このような素晴らしい企画に参加させていただきまして、
@youten_redo さん、本当にありがとうございました!
明日(12/19)の担当は @patorash さんです!