ListViewの高速スクローラまわりをカスタマイズ(AbsListView#setFastScrollEnabled)
前回 のエントリに引き続き、さらに ListView の高速スクローラまわりをカスタマイズしてみました。
(ついでにタイトル変更)
環境
4.0(API Level14)
ガイド(overlay)部分が3Dぽい感じに、スクローラは細身な感じに。
ソースはこちら。(前回エントリで使用した部分も合わせてます)
https://gist.github.com/1384757
ListView の高速スクロール時ガイドを編集する(AbsListView#setFastScrollEnabled)
Android Advent Calendar 12/18 担当の @daichan4649 です。
豪華メンバーがものすごく有意義なネタを記載されている中で、
空気を無視して ListView について誰得なネタを書いてみるとします。
ListViewには項目が多い場合に 「高速スクローラ」 を表示する機能がありますよね。
このとき 「スクロール中に項目のどのあたりを表示しているか」 を示す、
ガイドみたいなヤツを表示することができたりもします。
高速スクローラ、ガイド表示の実装に必要なのはこの2つ。
- AbsListView#setFastScrollEnabled(true)
- 1のListViewに設定するAdapterに SectionIndexer をimplementsする
1で高速スクローラを有効にし、
2でガイド表示内容を実装する、と。
実装してみると実際のイメージはこんなかんじになります。
この例ではガイドに 「画面内先頭要素のindex値」 をそのまま表示してます。
右側に表示されているスクローラをドラッグすると、
リスト先頭項目に合わせてガイド表示内容も変わっていく、という感じです。
本題に入ります。
このガイド。
普通にいじろうとすると 指定した文字列を表示する、くらいしかできない。
濃いグレーの正方形 は編集できない。
編集できないと言われるといじりたいじゃないですか。
ということで、framework のソース読んで試してみました。
とりあえず今回はこのグレーの正方形を変えるところを実験してみるとします。
いつものドロイド君アイコン画像に差し替えてみたり。
では、どんなかんじで差し替えたかを書いてみます。
まず、基本から。
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
リフレクションでいじるポイント、タイミングは以下。
- AbsListView#setFastScrollEnabled(true) を呼び出す
- その直後にリフレクションで内部の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 さんです!
Android Make Days in 明星和楽 にスタッフとして参加してきました
11/11金 - 12土 で行われた Android Make Days in 明星和楽。
今回自分はスタッフとして参加させていただきました。
メイン作業は司会。
部屋が複数あるので3人で交代しつつ、とのことだけども。
こんなでかいイベントで司会とか!!不安でガクブル。
と、自分のことはさておき、まずはスタッフ視点を含めつつレポート書いてみようかと。
各セッションについての素晴らしいエントリはもういくつもあるし。
というわけで早速。
(写真多めですー)
11/11(金)
24Makeコンテスト。
場所は GuildCafe Costa。
金曜夜から24時間でアプリ作って土曜に発表、というクレイジーなイベントw
仕事終わって皆様の頑張る様子をチラ見しにきてみた。
来年は作る側で参加しようかなー。すごく楽しそうだった。
参加者の皆様の様子。
11/12(土)
9:00
スタッフメンバー集合。
場所は 福岡県Rubyコンテンツ産業振興センター。
自分は間違えて金曜の会場、GuildCafe Costa へ行ってしまうという醜態をさらすw
会場設営やったり。
スタッフミーティングやったり。
11:00
ホモ弁に予約していたスタッフメンバー分の弁当を受け取り行ったり。
司会させていただくセッションの講師の皆様と打ち合わせしたり。
弁当食べながら司会メンバーで打ち合わせの図。
twicca作者の @R246氏(青山さん) とパシャリ。
あんざいゆきさん との打ち合わせ。
自分はあんざいさんの大ファンなのでサインいただいちゃいました。
うん、自分きもいですねw
イベント開始前にみんなで交代で弁当タイム。
鹿。どや。
世界の みよしさん 発見!
13:00
開場。
参加者が続々。
すぐにメイン会場が埋まる。
受付の様子
美女ぞろい。
ステッカーたくさん。
オープニング、基調講演。
ブースの様子。
Arduino体験講座。
レッドブル様との交流w
LT会場。大盛況。
個人的にどストライクだったのが、
ブリリアントサービス様のNFCクエスト体験。
@R246氏、サイバーエージェント三島木さん と超強力なパーティ組んで挑んだものの。。
敗北('A`)
魔王強い。
ブースの皆様と。
19:00
閉会式。
全員で手を繋いでスタッフ一同挨拶。
19:30 - 21:00
懇親会。
乾杯。
わいわい。
三島木さんとあんざいさん
後片付け後、残っていたメンバーで集合写真。
終了後。
スタッフ数名で一緒に明星和楽Gates会場へ行ったあと、途中離脱して屋台へ移動。
屋台の後は酔っぱらってひとりで中洲をうろうろして川沿いで寝てた。(ひどい)
で、結局タクシーで5時すぎに帰宅。
イベントを終えて。
正直なところ、スタッフメンバーはイベント屋ではないので普段の自分の業務もある。
そんな状況下でも各メンバーが責任もってタスクをちゃんとこなし、
結果、イベントは大成功っていうのはやっぱりすごいことじゃないかと。
イベント終了後になんだかイロイロと考えてしまった。
いやー!!!本当に楽しかった!!
スタッフとして参加できて本当によかった!!
次回もぜひスタッフとして参加したいし、何かを発信する側にもなりたい。
改善すべき、反省すべき点はもちろんたくさんあるけれども、
イベント運営の難しさと楽しさを味わうことができて、
すごく貴重ないい経験ができた、参加できてよかった、楽しかった、というのが本音。
運営スタッフの皆様、講演者の皆様、参加された皆様、
本当にお疲れさまでした!!
Intentの作り方
Activity/Service起動用のIntentを作るとき、
皆どうやってるのだろう、とふと疑問。
例えば Activity#onCreate 内で
「Service(test.intent.TestService)起動用のIntent」 を作るとする。
基本的な作り方はこんなかんじになるのかな?
@Override public void onCreate(Bundle savedInstanceState) { 〜 // コンストラクタのみ で作る場合 Intent intent1 = new Intent(this, TestService.class); Intent intent2 = new Intent(getApplicationContext(), TestService.class); // コンストラクタ(引数なし)+setClassName で作る場合 String packageName = getPackageName(); String className = TestService.class.getCanonicalName(); Intent intent3 = new Intent(); intent3.setClassName(packageName, className); }
自分の場合は intent1、2 みたいにコンストラクタ1行で済ませることが多い。
web上のサンプル見てもこのパターンが多い気がする。
通常はこれでいいはず。楽だし。
ここで出た疑問。
ApplicationContext を渡す必要があるのだろうか。
Intent のソースを確認してみたところ、
本来は ApplicationContext を引数に渡す必要などなく、
最低限必要なのはこれ↓↓だけだった。
Intent作成に必要な情報
- アプリケーションのパッケージ名
- 起動するクラス名
1は AndroidManifest.xml の
起動するクラス(TestService)のパッケージ名ではない。
2は 起動するActivity/Service等のクラス名(パッケージ名まで含む)。
引数に渡された ApplicationContext は、内部で
「アプリケーションのパッケージ名」 を取得するために使ってるだけ。
まーつまり intent1、2、3 はどれも同じなので、
作りたい作り方でおk、ということでしたw
Intent.java(抜粋)
public Intent() { } public Intent(Context packageContext, Class<?> cls) { mComponent = new ComponentName(packageContext, cls); } public Intent setClassName(Context packageContext, String className) { mComponent = new ComponentName(packageContext, className); return this; }
ComponentName.java(抜粋)
public ComponentName(String pkg, String cls) { if (pkg == null) throw new NullPointerException("package name is null"); if (cls == null) throw new NullPointerException("class name is null"); mPackage = pkg; mClass = cls; } public ComponentName(Context pkg, String cls) { if (cls == null) throw new NullPointerException("class name is null"); mPackage = pkg.getPackageName(); mClass = cls; } public ComponentName(Context pkg, Class<?> cls) { mPackage = pkg.getPackageName(); mClass = cls.getName(); }
話変わって。
intentって文字列定義みたいなもんだから、
できれば 「static final」 定義で、static初期化子 内で作っておきたいなー、
なんて自分は思ってたりする。後から編集しないようなintentは特に。
でも 「アプリケーションのパッケージ名」 は
static初期化子 内で動的に取得できない。
ということは、こんなかんじで固定値定義するしかないのか??
private static final Intent intent; static { String packageName = "test.intent"; String className = TestService.class.getCanonicalName(); intent = new Intent(); intent.setClassName(packageName, className); }
うーん。微妙。
別スレッドでキュー管理(Handler, Looper, HandlerThread)
非同期処理を組んでると、
「別スレッド上でも Handler みたいなキュー管理がしたい」
って時があったりしますよねー。(きっと)
そんなとき、HandlerThread を使うとわりと簡単に実装できたりする。
HandlerThread は java.lang.Thread を継承したクラスで、
android標準の Handler と組み合わせて使うための仕組みが入ってたり。
具体的な使い方。
まずは Handler(defaultコンストラクタ) を使う場合。
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Handler handler = new Handler(); handler.post(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); }
実行すると当然 「main」 と出力される。
当然、mainスレ上でキューが実行される。
これは問題ないですね。
次に Handler+HandlerThread の場合。
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // 別スレ生成 -> 開始 HandlerThread handlerThread = new HandlerThread("other"); handlerThread.start(); Handler handler = new Handler(handlerThread.getLooper()); handler.post(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }); }
実行すると 「other」 と出力される。
キューが実行されてるスレッドが mainスレ から 別スレ に変更されます。
もちろんこの Handlerインスタンス は別スレ上で使用してOK。
手順をまとめるとこんなかんじ。
- 別スレッド(HandlerThread)を生成
- 別スレ開始
- HandlerThreadインスタンス から Looperインスタンス 取得
- Handlerインスタンスを生成(3で取得した Looperインスタンス を 引数指定)
- 4 で作成した Handlerインスタンス を使用(Handler#post 等)
今まで PriorityBlockingQueue とか使って、
自前のキュー管理クラス作ってた自分涙目。
キューをFIFOで実行するだけのキュー管理であれば、
このやり方でまったく問題なさそうですね。
framework ではどうやってるのかなーと思い、
Looper、Handler、HandlerThread のソースを読んでみた。
ポイントはこのあたり。
- コンストラクタ Handler(default) は「mainスレッド上で作成された」 Looperインスタンス を使用
- コンストラクタ Handler(Looper) は「引数で渡された」 Looperインスタンス を使用
- Looperインスタンスは Looper#prepare を呼び出すと生成される(ThreadLocal保持)
- HandlerThread は Thread を継承したクラス
- HandlerThread#run の中で Looper#prepare を呼び出している
Looperインスタンスの生成場所 の違い。
なるほど。