ブログ移転のお知らせ
あまりにも重いのではてなブログに移転しました。
今後こちらの更新は行いません。とりあえずText記事は全部向こうに移しました。Linkは迷っていますが、気が向いたらエクスポートします。
Alisa U Zemlji Chuda

izzy's playlists!

oozey mess
Show & Tell

Discoholic 🪩


Product Placement
Monterey Bay Aquarium
Game of Thrones Daily

⁂
Today's Document
One Nice Bug Per Day
Cosimo Galluzzi
d e v o n
KIROKAZE
sheepfilms
DEAR READER
dirt enthusiast
Peter Solarz

seen from United States
seen from Argentina
seen from Germany
seen from Singapore
seen from United States
seen from United States

seen from United States

seen from United States
seen from Jordan

seen from Malaysia
seen from United States
seen from United States

seen from United States
seen from United Kingdom

seen from United Kingdom

seen from United States

seen from Israel

seen from United States
seen from Germany

seen from United States
@outofmem
ブログ移転のお知らせ
あまりにも重いのではてなブログに移転しました。
今後こちらの更新は行いません。とりあえずText記事は全部向こうに移しました。Linkは迷っていますが、気が向いたらエクスポートします。

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch • No registration required • HD streaming
pupを使ってコンソール上でスクレイピングする
前書き
curlとかwgetとか使っているといっそコンソール上でスクレイピングしたくなることがよくあります。
自分でパーサを書くのも面倒だし何かないかなと探していたらpupなるものを見つけました。
動作
標準入力 or ファイルの内容をCSS Selectorで検索、抽出し、標準出力に表示します。
つまり、パイプを通し放題なわけです。便利ですね。
出力形式は
HTML
テキスト
json
のうちどれか一つを指定することが出来ます。また、あるElementのあるAttributeの値だけを出力、なんてことも出来ます。
勿論、pupで出力した結果をパイプしてgrepしたりtrして更に加工してもいいでしょう。pupの結果をpupにパイプしたりしてもいいと思います。
インストール
Release Pageにコンパイル済みの実行ファイルが置いてあります。
Go言語の環境がインストールされていればgo getで、Mac使ってるならbrewでもいいみたいです。
使い方
GitHubのQuick StartとExampleを見ておけば十分です。
出力形式の指定
Display Functionsと言うセクションに書いてあります。
text{} - 抽出したElementのテキスト
attr{attrkey} - 抽出したElementのattributeの値
json{} - 抽出したElementのjson
指定しなければHTML形式のまま出力されます。
attr{attrkey}を上手く活用すればXMLのパースもそこそこ容易に可能なので、コンソール上で動くRSSリーダーなんかも作れたりします。
オプション
オプション 省略形 動作 --color -c 出力結果を色付きで表示してくれる。 --file filePath -f filePathに指定したファイルを使用する。 --help -h ヘルプを表示する。 --indent count -i countに指定した数値の数だけインデントする。(html、jsonのみで有効) 指定しないとデフォルトのインデントが入る。 --number -n 出力された結果の番号を表示してくれる…らしいのだが、指定すると「そんなオプションはない」と怒られる。 ソースを見てみると確かにない。何だこれ。 --limit level -l level以上の要素を「...」として表示します。いっそ表示してくれないほうがありがたいんだが。 textも1レベルとしてカウントするので注意。 --charset charset N/A UTF-8やEUC-JP以外のものを読み込ませる時に使うのだと思われるが、どんな値が指定できるのかよくわからない。 内部ではcharset.Lookupにそのまま値を渡しているが、これまたどんな値が指定できるのかよくわからない。 試しに「Shift-JIS」を渡したら上手く行ったので、常識の範囲内で指定してあげて下さい。 --version N/A バージョンを表示します。
まとめ
そんなわけで、pupを入れればちょっとしたスクレイピングならコンソールだけで簡単に出来てしまいます。
一々何らかの言語でパースしてた人は試してみてはいかがでしょうか。
Windowsに入れておきたいツールや環境いろいろ
前書き
最近お仕事用の新しいPC(Windows 7)を受領したんですが、一から環境を作ってたら色々面倒だったのでメモしておきます。
プログラミング言語に関する環境は特に説明しません。欲しくなったら入れるスタンス。でもWindowsならExpressでいいからVisual Studio入れておきたいなぁ…。ちょっとしたものを作るのにもC#便利なんだよなぁ…。
ユーティリティツール
QTTabBar
エクスプローラーのタブ化。お仕事中はどうしても大量のフォルダを開いて作業することがあるので…。
個人的にはCloverの方が好きなんですが、セキュリティ面で色々怪しいのでこっちを使います。
Wheel Redirector
フォーカスしてないウィンドウでもマウスホイールによるスクロールを有効化してくれます。
地味ながらも、これがあるのとないのとでは作業効率がかなり違う。
Fiddler
ユーティリティツールでもなんでもないんですが…。
日常生活で「これどんな値をPostしてるんだろう?」と気になることがたまにあるので入れておきます。
AutoHotKey
仕事中に突然キーボードが壊れた時に…。今まで一回もそんなことになったことがないですが。
色々とスクリプトも書けて便利なのはわかるんですが、それはそれで面倒なんですよね。
CLCL
クリップボード拡張。一つ入れておくと便利。
エディタ
サクラエディタ
正規表現によるFind, Replace, Grepができるテキストエディタならなんでもいいです。
特にプラグインやマクロを入れなくても一通りの機能が全部入っているのでとりあえず入れます。
IME
Google日本語入力
MS IMEなんか使う理由がありません。
Google日本語入力に変更する際にIMEの切り替えショートカットキーを無効にしておきます。
ランチャ
Executor
キーワードやコマンドを入力して起動するランチャ。キーワードを直接Googleで検索することも出来る。ファイル検索も可能。
ランチャと言うよりは、Win+Rで出せる「ファイル名を指定して実行」の強化版と捉えるといい。
デフォルトだとProgram Filesが対象になってなくて使いにくいので自分で追加するのが吉。
日本語に対応してないのが残念。日本語のフォルダ名配下のファイルとかもダメっぽい。
キーワードに追加すれば問題ないし、結構簡単にぼんぼん追加出来るので我慢しませう。
CLaunch
Executorだと色んなアプリをまとめて起動するのが面倒なのでGUIのランチャも一つ入れておきましょう。
まぁなくてもいいっちゃいいんですが…。
コンソールまわり
コマンドプロンプトはうんこなのでちゃんとコンソールの環境も整えます。
Console 2
コンソールのタブ化etc。
個人的には「わざわざレジストリをいじらなくてもフォントが変更できる」って理由で使っています。
インストーラがないので適当にProgram Filesに突っ込んだ後、パスを通しておく必要があるのが若干面倒。
日本語を入力するにはパッチを当てる必要があるので忘れないようにしましょう。
また、日本語の表示時に変な動きをする場合はコマンドプロンプトのフォントの規定値を変更します。(ラスターフォント以外にすればよさそう)
Nyaos
Windows用のUNIXライクなシェル。タブキーでパスを補完してくれたり、自分でalias切れたりと必須と言って良いレベルです。
4.xからはUnicodeもサポートしているようなんですが、Go言語の環境を別途作成する必要があるのがなんとも…。
3.xでも全然使えるのでそっちをメインに使います。でもUnicodeのサポートほしい…。
MinGW
MinGWと言うか、MSYSのUNIXコマンドが欲しいだけですが…。
インストールが案外面倒ですが、あるのとないのでは全然違うので入れておきます。
cURL
理由はなくとも一応入れておきます。ほ、ほら、インターネット経由で何かを一括ダウンロードしたくなったりするじゃないですか。
MSIでインストールすればパスも通してくれるのでありがたいですね。
フォント関連
MacType
Macを使ったことはありませんが、文字が読みやすいことはいいことです。
割と簡単に導入できる上にちょっとした設定も簡単なので入れておきます。
設定ファイルの中身はとりあえずこんな感じ。
[General] Name=Default Icon=..\mactray.exe,013 HookChildProcesses=1 HintingMode=2 AntiAliasMode=4 shadow=1,1,1,0x0,0,0x0 MaxHeight=150 UseMapping=0 FontLink=1 FontSubstitutes=1 WidthMode=0 FontLoader=0 GammaMode=0 GammaValue=1.2 RenderWeight=1.3 Contrast=2.2 TextTuning=-1 TextTuningR=-1 TextTuningG=-1 TextTuningB=-1 NormalWeight=1 BoldWeight=0 ItalicSlant=0 LcdFilter=2 LoadOnDemand=1 CacheMaxFaces=16 CacheMaxSizes=16 CacheMaxBytes=10485760 EnableKerning=0 BolderMode=0 MaxBitmap=0 [UnloadDll] [exclude] [FontSubstitutes] [Individual] [excludeSub]
MeiryoKe_Console
メイリオの等幅フォント。Consolas用の和文フォントにします。
フォントを作成し終えたらレジストリはこんな感じにします。
Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontLink\SystemLink] "Consolas"=hex(7):6d,00,65,00,69,00,72,00,79,00,6f,00,4b,00,65,00,5f,00,36,00,\ 30,00,32,00,72,00,31,00,2e,00,74,00,74,00,63,00,2c,00,4d,00,65,00,69,00,72,\ 00,79,00,6f,00,4b,00,65,00,5f,00,43,00,6f,00,6e,00,73,00,6f,00,6c,00,65,00,\ 2c,00,31,00,32,00,38,00,2c,00,31,00,32,00,38,00,00,00,00,00 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontSubstitutes] "MS ゴシック,128"="MeiryoKe_Console,128" "MS Pゴシック,128"="MeiryoKe_PGothic,128" "MS Gothic,128"="MeiryoKe_Console,128" "MS PGothic,128"="MeiryoKe_PGothic,128" "MS UI Gothic,128"="Meiryo UI,128"
FontSubstituesはどっちでもいい気がするけどね…。
FireFoxのアドオン
ブラウザは仕事だとFireFoxを使います。理由はWeb関連のデバッグが一番やりやすいからです。それ以外の理由は特にありません。
Adblock Plus
別に広告にもトラッキングにも困ってませんが入れておきます。
FireGestures
デフォルトで入れとけよっていつも思うんですが。
FireBug
これもデフォルトであって良いレベル。
まとめ
とりあえずはこんなもんでしょうか。
後はまぁ、必要になったら適宜入れれば十分でしょう。
DialogFragmentの何らかの結果をActivity#onActivityResultへ渡す方法。
[Java]あなたとJavaとEnum
前書き
お久しぶりです。
お久しぶりですと言っても、定期購読している人は恐らく数人しかいないんじゃないでしょうか。技術ブログなんてそんなものです。
別に忙しかったわけじゃないんですが…いや…忙しかったのかな。忙しかったんですが色々遊んでもいました。単にネタにするものがなかっただけです。
最近は2~3年ぐらい前に作成して1年間ほったらかしにしていたAndroidアプリの改修をゴリゴリやっています。
とりあえず機能追加は一切しないでリファクタリングと簡単な障害対応メインで…と思ったらあまりのクソコードさに失禁しかけたので、ほとんど全コード書き直しています。しかし、2~3年前の自分を殺したくなるのは、エンジニアにとってはいい兆候です。(ポジティブ)
さて、今回はAndroidのパフォーマンスに関する手引きで「使ったらぶっ殺すからな」と言われていたEnumに関する記述がいつの間にか消えていたので、JavaでのEnumに関するあれこれを書いていきます。
JavaのEnumのしくみと使い方
JavaのEnumはJDKでもあんまり使われていません。色々な現場でも、ライブラリでも、使われているのを見ると「おっ、Enumが使われているぞ!?」って気分になります。C#なんかはどこでもバリバリ使っていて非常に楽なんですが。
Javaであんまり使われない理由、そしてAndroidで忌避されていた一番大きな理由は、あまりにもリッチすぎるからだと思われます。
とりあえずEnumの宣言方法の紹介も兼ねて適当なコードを載せておきましょう。
public enum TestEnum { TEST1, TEST2, TEST3, //最後がカンマで終わっていてもOK }
基本的にはこんな感じです。応用すると色々なことが出来すぎてしまうんですが、これは後で説明します。
上記のコードをコンパイルすると内部でこんなコードに変換されます。
final class TestEnum extends Enum<TestEnum> { private TestEnum(String name, int ordinal) { super(name, ordinal); } // コンストラクタのnameには列挙子の名前、ordinalには宣言された順番が入る public static final TestEnum TEST1 = new TestEnum("TEST1", 0); public static final TestEnum TEST2 = new TestEnum("TEST2", 1); public static final TestEnum TEST3 = new TestEnum("TEST3", 2); private static final TestEnum ENUM$VALUES[] = { TEST1, TEST2, TEST3 }; // 他にもvalueOf(String name)やvalues()のようなstaticメソッドが生成されるが割愛 }
と言うわけで、Enumを宣言すると中の列挙子の分だけインスタンスが作成されます。static finalなので「==」での比較が可能になるわけです。
TestEnum en = TestEnum.TEST1; if(en == TestEnum.TEST1) { // もちろんen.equals(TestEnum.TEST1)でもtrueを返す }
Enumもオブジェクトなので宣言時にNULLを入れておくことが出来ますが、なるべく初期値用の列挙子を用意しておくべきです。
と言うのも、Enumをswitchで使用する際にNULLだとNullPointerExceptionが発生するからです。
public enum TestEnum { TEST1, TEST2, TEST3, UNKNOWN, } // nullのEnumをswitchのconditionに使用するとNullPointerExceptionが発生する TestEnum en = null; switch(en) { case TEST1 : break; case TEST2 : break; case TEST3 : break; default : break; } // そこで、初期値用の列挙子「UNKNOWN」を用意しておく TestEnum en = TestEnum.UNKNOWN; switch(en) { case TEST1 : break; case TEST2 : break; case TEST3 : break; case UNKNOWN : // 初期値用の列挙子の場合はdefaultにフォールスルーしておけばよい default : break; }
そもそも何でswitchでNullPointerExceptionが発生するかなんですが、内部でEnum#ordinal()を呼び出しているからです。
また、上記の記事を読めばわかる通り、各ラベル内で自動でtry-catchを行います。そのため、Enumのswitchは普通のswitchよりもコストが高い、と言うことは覚えておきましょう。
Enumの値を定義する
いきなりデメリットばかり取り扱ったせいでこんなクソ機能いらないでしょ、と思うかもしれませんが、そんなことはありません。色々工夫すると非常に強力な機能に化けます。
JavaのEnumは各言語のEnum同様、列挙子に値を定義することが出来ます。ちょっと変わった形ですが、こんな感じです。
public enum TestEnum { // 値を定義する場合は最後がセミコロンで終わってないといけない TEST1(0), TEST2(1), TEST3(2); private final int value; // privateスコープでコンストラクタを定義する // (* public, protected, package-privateではダメ。 // 新しいインスタンスを外部から勝手に生成されたらEnumのメリットがなくなるので…。) private TestEnum(int value) { this.value = value; } public int getValue() { return this.value; } }
先ほどの話のおさらいになりますが、結局のところEnumの列挙子はクラスのインスタンスです。だから、コンストラクタで初期値を渡してもそれはJavaの仕様内の話になるので合法です。
初期値の型に制限はありませんし、コンストラクタに渡す引数の数にも制限はありませんし、コンストラクタのオーバーロードがいくつあろうと構いません。だってそれはJavaの仕様内の話だから。
値を返すメソッドetcや値を保持しておくフィールドは自分で定義しなきゃいけませんが、まぁ、そんなのlombok使っておけばいいじゃないですか。
また、EnumはSerializableを実装しているので、保持する値もSerializableであれば、そのままシリアライズ / デシリアライズが可能です。
今回の例ではvalueをfinalにしたり、setterを用意したりしませんでしたが、どちらも制限があるわけではないので、Enumなのに何らかのステートを持たせるなんてことも可能です。可能ですが、それはもう一般的なEnumからは逸脱しすぎている気がします。
Enumにメソッドを定義する
何度でも言いますが、Enumの列挙子はクラスのインスタンスです。クラスに対しメソッドを定義しても誰にも怒られません。って言うかさっきの例で思いっきりgetter書いてるし。
ちょっとエキセントリックなんですが、Enumにabstractメソッドを定義することも出来ます。じゃあそのabstractなメソッドはどこで実装するの?と言うと、宣言時です。
public enum TestEnum { TEST1 { @Override public String test() { return "TEST1"; } }, TEST2 { @Override public String test() { return "TEST2"; } }, TEST3 { @Override public String test() { return "TEST3"; } }; public abstract String test(); }
いわゆる匿名(無名)クラス的な扱いになるわけですね。この機能をフル活用すると、Enumなのにswitchで分岐せずabstractなメソッドを呼ぶだけ、とかそんなことも出来ます。
巷でテンプレートパターンと呼ばれているものはすべてEnumで代替可能です。代替すべきかどうかはおいといて。
また、メソッドが定義できると言うことは、インターフェースを実装することも可能だと言うことになります。Enumなのに別インターフェースとしても振舞えるわけです。気持ち悪いですね。
ただし、他のクラスを継承することはできません。なぜなら、すべてのEnumは暗黙的にEnum<E extends Enum<E»を継承しているからです。二重継承になってしまいます。
文字列とEnumの相互変換
個人的にEnumの一番の強みはここだと思っています。文字列 ⇔ Enumへの変換がめちゃくちゃ楽です。
特にjsonやXMLをパースする時に恐ろしい力を発揮します。
文字列→Enum
文字列からEnumへ変換するにはvalueOfメソッドを使用します。これにはjavaDocがありません。なぜなら、コンパイル時に自動生成されるstaticメソッドだからです。
// TestEnum.TEST1を文字列から生成する TestEnum en = TestEnum.valueOf("TEST1");
Enum#valueOf(Class, String)なんてものもありますが、リフレクションでもしない限りは使わないと思います。
Enum→文字列
これには二通りの方法があります。Enum#name()を使用する方法と、Enum#toString()を使用する方法です。
違いは非常に簡単で、nameはオーバーライド不可、toStringはオーバーライド可能なメソッドです。
public enum TestEnum { TEST1, TEST2, TEST3 { @Override public String toString() { return "TEST3だよ"; } }; } System.out.println(TestEnum.TEST1.name()) // => TEST1 System.out.println(TestEnum.TEST2.name()) // => TEST2 System.out.println(TestEnum.TEST3.name()) // => TEST3 System.out.println(TestEnum.TEST1.toString()) // => TEST1 System.out.println(TestEnum.TEST2.toString()) // => TEST2 System.out.println(TestEnum.TEST3.toString()) // => TEST3だよ
いろんな値⇔Enum
自分でメソッドを作ればいいじゃない。(完)
public enum TestEnum { TEST1(0), TEST2(1), TEST3(2), UNKNOWN(-1); private final int value; private TestEnum(int value) { this.value = value; } public int getValue() { return this.value; } public static TestEnum valueOf(int value) { // 暗黙に定義されるvalues()メソッドを使うことで全列挙子を取得することが出来る for(TestEnum e : TestEnum.values()) { if(value == e.getValue()) return e; } // ここはthrow new IllegalArgumentExceptionでもいいと思う // (暗黙に定義されるvalueOf(String)だとIllegalArgumentExceptionを返す) return TestEnum.UNKNOWN; } }
[2014/12/15追記:説明しようと思っていて忘れていたEnumMapとEnumSetについて追記]
Enumのためのコレクション
EnumMap
Enumには専用のMapの実装が用意されています。その名もEnumMapです。そのまんまじゃないか。
使い方はHashMapとほとんど変わりません。ただ、Keyの比較にordinalを使用しているので、HashMapより速いです。
しかしまぁ、Enum側でいくらでも紐付くプロパティを定義出来るので、実はそんなに使う機会がないと言う…。存在自体を忘れないようにしたいですね。(自戒)
EnumSet
EnumSetなるものも用意されています。
これはstaticメソッドからのみインスタンスを生成することが出来ます。よく使いそうなものだけ挙げておきます。
allOf
noneOf
of
range
そもそもあるEnumの全列挙子を取得したいのであればEnum#values()を使用すればいいのですが、Enumを使ってビット操作のようなことがしたい場合はこれを使うといいよ、と言われています。
「従来の int ベースの「ビットフラグ」に対する高品質かつ型保証された代替として十分に使用可能です。」とは書いてあるものの、そもそもビット操作をしなきゃいけない時点でEnumなんて使わなそうだし、そもそもJavaを(省略)
具体的にどうやってビット操作の真似事をするかと言うと、Setのadd / removeを使ってフラグの上げ下げをするだけです。なんだそれ。
Enumの実用例
仕組みと出来ることの紹介はこんなもんでいいでしょう。
次は実際にこんな風に使ったら中々便利だったよ、って言うのを少し紹介してみます。
Comparator<T>の実装
いま作っているものにこんなデータクラスがありまして。
@Data public class Subject implements Serializable { private static final long serialVersionUID = -6818315400153409211L; private String author; private String title; private int logNo; private Date postDate; private Date editDate; private int commentCount; private int point; private double rate; private String[] tag; private long key; private double size; private int odai; private String[] characters; private boolean isFavorited; }
この各プロパティでソートするためのComparator<T>をEnumで実装してみました。
public enum SubjectSort implements Comparator<Subject> { NUM { @Override public int compare(Subject l, Subject r) { return ((int) (l.getKey() - r.getKey())) * reverse(); } }, TITLE { @Override public int compare(Subject l, Subject r) { return l.getTitle().compareTo(r.getTitle()) * reverse(); } }, AUTHOR { @Override public int compare(Subject l, Subject r) { return l.getAuthor().compareTo(r.getAuthor()) * reverse(); } }, CREATED { @Override public int compare(Subject l, Subject r) { return l.getPostDate().compareTo(r.getPostDate()) * reverse(); } }, LASTUP { @Override public int compare(Subject l, Subject r) { return l.getEditDate().compareTo(r.getEditDate()) * reverse(); } }, EVAL { @Override public int compare(Subject l, Subject r) { return l.getCommentCount() - r.getCommentCount() * reverse(); } }, POINT { @Override public int compare(Subject l, Subject r) { return l.getPoint() - r.getPoint() * reverse(); } }, RATE { @Override public int compare(Subject l, Subject r) { return ((int) (l.getRate() - r.getRate())) * reverse(); } }, SIZE { @Override public int compare(Subject l, Subject r) { return ((int) (l.getSize() - r.getSize())) * reverse(); } }; @Setter protected boolean isReverse = false; protected int reverse() { return !isReverse ? 1 : -1; } @SuppressLint("DefaultLocale") public static SubjectSort fromQuery(String query) { return SubjectSort.valueOf(query.toUpperCase()); } }
後はCollections#sortにこのEnumを渡すだけです。
結局何処かに各Comparatorは宣言と実装しなきゃいけないんですが、Enumでまとめることによって可読性をあげることが出来ます。
また、元々これはとあるWebアプリの機能をそのまま移植しようと思っているもので、もしintent-filterか何かでURIが渡されてきた時にソート用のクエリも渡されてきたら、そのクエリからEnumを生成できるようにしています。
WebAPIを叩く
WebAPIを叩く時にも便利です。
以前、YOのAPIを叩いた時はこんなEnumを作成しました。
public enum YoAPI { YO("http://api.justyo.co/yo/", Request.Method.POST), YO_ALL("http://api.justyo.co/yoall/", Request.Method.POST), ACCOUNTS("https://api.justyo.co/accounts/", Request.Method.POST), SUBSCRIBERS_COUNT("https://api.justyo.co/subscribers_count/", Request.Method.GET); @Getter private final String value; @Getter private final int method; private YoAPI(String value, int method) { this.value = value; this.method = method; } }
後はVolleyのRequestにこのEnumを渡すだけ…ではどうにもなりませんが、エンドポイントやGET / POSTの区別なんかを書いておく分には十分すぎますし、固定で渡さなきゃいけないようなquery / paramsがあるなら持たせておいてもいいでしょう。
[2014/12/25追記]AndroidのArrayAdapterで使う
AndroidだとSpinnerで使うと超絶便利です。
こんなArrayAdapterを作っておきます。ジェネリクスの複数制限なんて生まれて始めて使いました。
// 2014/01/04 ジェネリクスの制限かける場所が間違っていたので修正 public class EnumSpinnerAdapter<T extends Enum<T> & AdaptableEnum> extends ArrayAdapter<T> { public static interface AdaptableEnum { String getDescription(); } private final LayoutInflater _layoutInflater; public EnumSpinnerAdapter(Context context, Set<T> enumSet) { super(context, 0, new ArrayList<T>(enumSet)); _layoutInflater = LayoutInflater.from(context); } public EnumSpinnerAdapter(Context context, T[] enums) { super(context, 0, enums); _layoutInflater = LayoutInflater.from(context); } static final EnumSpinnerViewHolder { final TextView description; EnumSpinnerViewHolder(View v) { this.description = (TextView) v.findViewById(R.id.txvDescription); } } @Override public View getView(int position, View convertView, ViewGroup parent) { EnumSpinnerViewHolder vh = null; View view = convertView; if (view == null) { view = _layoutInflater.inflate(R.layout.adapter_enum_spinner, null); vh = new EnumSpinnerViewHolder(view); view.setTag(vh); } else { vh = (EnumSpinnerViewHolder) view.getTag(); } val item = this.getItem(position); vh.description.setText(item.getDescription()); return view; } }
Enumはこんな感じ。
public enum UgigiMode implements AdaptableEnum { ALL("全体検索", "free"), TITLE("タイトル検索", "title"), AUTHOR("作者検索", "author"), TAG("タグ検索", "TAG"); @Getter private final String description; @Getter private final String value; private UgigiMode(String description, String value) { this.description = description; this.value = value; } }
Spinnerに値をセットするのはこれだけ。
val spinner = (Spinner) findViewById(R.id.spiMode); spinner.setAdapter(new EnumSpinnerAdapter<UgigiMode>(getApplicationContext(), UgigiMode.values()));
わざわざSet<T>を受け取れるようにもしてあるので、EnumSetのstaticメソッドを使ってもOK。
値の取得もこれだけ。
val spinner = (Spinner) findViewById(R.id.spiMode); val mode = (UgigiMode) spinner.getSelectedItem();
後はswitchで条件分岐してもいいし、Enumに自分で定義したメソッドを呼び出してもいいし、Intent#putExtraでそのまま別画面に渡してもOK。本当にめちゃめちゃ便利です。
まとめ
とまぁ、アイデア次第で色々と出来てしまいます。
public static finalなフィールドが多すぎてやってられなくなったりしてきたら、Enumを使って大富豪的プログラミングに興じてみるのはいかがでしょうか。
参考
Java列挙型メモ(Hishidama’s Java enum Memo)
J2SE 5.0 Tiger 虎の穴 Typesafe Enum
Javaのenumは継承できないけどインタフェースが継承できる - No Programming, No Life

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch • No registration required • HD streaming
[Atom]やっておきたい設定+入れておきたいパッケージメモ
記事を読む前に
この記事は非常に古いです。【Atom】入れておきたいパッケージメモ2015年版と言う記事を新たに書いたので、そっちを読んで下さい。
前書き
最近Atomを入れました。
入れたはいいんですが、なんかこう、微妙です。とても。まず名前がイケてないよね。Atomて。データ形式かよって。
とは言え、プロジェクト管理機能がついてるテキストエディタが一つ欲しかったのでまぁそれなりに頑張って使ってみようと思います。使ってみようとは思うんですが、デフォルトのままだと普通に使いにくいので適当に環境が復元できるようメモしておきます。
って言うか、プロジェクト管理機能が必要ない文章ならサクラエディタで十分なんですけどね。Markdownのプレビューが出来るって言われてもテーブル書いた時ぐらいしか確認したくないし…。
覚えておきたいショートカット
Ctrl + , - 設定画面
「パッケージ動かねーぞボケが!」と言った事象から「パッケージ動かねーぞボケが!」と言った事象まで様々なことを解決するために頻繁に呼び出します。
正直、一番使っているショートカットが恐らくこれなんですが、それはそれでどうなんでしょう?私はコーディングがしたいのであって設定がしたいわけじゃないんですが。
Ctrl + Shift + P - コマンドパレット
やりたいことはなんとなくわかってるんだけどどうすればいいのかわからないときに使うショートカット。それなりに解決するが、たまに解決しない。
Ctrl + Shift + F - プロジェクト単位のFind and Replace
開いているプロジェクト内のファイルを一括でgrepしてくれます。
Replace後のプレビューも自動で表示されてくれるのが中々ありがたいです。終わった後Escするのが面倒ではありますが。
後は特にない…と言うか、追加したパッケージに設定されているショートカットばっかりなので、この二つを覚えておけば大概どうにかなることがわかりました。
入れないと辛すぎるパッケージ
勿論個人の感想です。
Project Manager
頻繁に使うプロジェクトを登録することが出来ます。
保存方法は「プロジェクトを開く」→[Packages]→[Project Manager]→[Save Project]
呼び出すときはCtrl + Shift + Alt + P
Japanese Wrap
これを入れないと日本語の禁則処理をまともに行ってくれません。
ただまぁ、日本語の長文を書きたいならサクラエディタの方がよっぽど便利なんですが、それを言ってしまったらおしまいです。
Docblockr
ブロックコメントを自動生成してくれます。
かなり便利…と言うか、これなしでブロックコメントを書こうとすると死ぬほど辛いです。
Save Session
前回終了時に開いていたプロジェクトやファイルを自動で再開してくれます。
基本入れておいて損はないです。Atomを何かの間違いでメモ帳代わりに使うつもりがあるならおすすめしませんが。
入れておくとそこそこ便利なパッケージ
Autocomplete Plus
デフォルトのコード補完はCtrl + Spaceを押さないと出てこないんですが、これを入れると入力中にバンバン候補をだしてくれます。
あまりにもバンバン出すぎて邪魔って言うか、お前人がコメント書いてるときに出てくんなよって言うか、plain textでも出てくるとかいい加減にしろよとか、候補と完全一致する単語を入力すると候補からその単語が消えて全く別の単語がサジェストされるのは流石にどうかしているとか、言いたいことは色々あるんですが、まぁあると便利です。
MinimapとMinimap Find And Replace
コードのミニマップをちょろっと出してくれます。
これ自体は便利ともどうでもいいともなんとも言えないんですが、周辺プラグインのMinimap Find And Replaceは中々便利です。
Git Plus
元々AtomはGitに中途半端に対応しているんですが、中途半端すぎていらいらしてくるので、精神衛生上これを入れておくのが無難だと思われます。
Command Toolbar
「たまに使うんだけどキーバインドを覚えるほどではない」みたいなものを登録しておくと捗ります。
わりかしどうでもいい or 趣味的なもの
File Icons
アイコンがちょっとオシャレになります。デフォルトよりはファイルが一目で見分けられるので意外と便利です。
VS Syntax Theme
Visual Studio風のSyntax Theme。テーマなのでパッケージではないんですが。
「Visual Studio風を名乗っている癖にデフォルトのフォントがConsolasじゃない」と言う理由でissueないしはpull requestを送ろうかと思ったんですが、自分で勝手に改変できるのでやめておきました。
おまけ - フォントの変え方とか
SettingsのFont Familyを変えればいいとか、styles.lessを書き換えればいいとか、諸説あるんですが、一番手っ取り早いのはThemeをAtomで開いてfont-familyを設定している箇所をgrep、自分の好きなフォントに差し替える、が一番早いです。
早いですって言うか、それ以外だとThemeに上書きされてしまうっぽいですね。
デフォルトの日本語フォントはちょっとひどすぎるので、それなりのフォントに差し替えないと段々生きるのが辛くなってきます。一番最初に直してしまいましょう。
Themeに限らず、パッケージで「なんだこのクソみたいなキーバインドは」と言うような、何らかの不満を抱いたらSettingsを開き[Filter package]から該当パッケージを選択、[Open in Atom]ってボタンを押せば好き勝手編集できてしまうのでさっさとそうしましょう。
おまけその2 - 高速化
なんか起動がクソ重いんだけど!と思ったらコマンドパレットを開き、Timeと打ち込んでみましょう。TimeCop: Viewなるものが出てくるはずです。
TimeCopくんは起動にかかった総時間や各プラグインの起動時間を表示してくれます。あんまり恩恵を享受できてないのに起動がクソ重いプラグインなんかはさっさとアンインストールしてしまいましょう。
また、Settings -> Editor Settingsの「Use Hardware Acceleration」あたりのチェックを外すといきなり爆速になったりします。この辺は環境によって違うと思いますが、色々試してみるにこしたことはありません。
まとめ
この文章はサクラエディタで書きました。
DBに登録されているViewの定義を列挙するクエリ。
SELECT m.definition FROM sys.views v INNER JOIN sys.sql_modules m ON m.object_id = v.object_id WHERE name = 'Example_1'
SELECTでnameを指定すればViewの名前を列挙することも出来る。
Gsonを使うときのProguard設定例。
[Android]Volley各種設定メモ
前書き
ちょっとVolleyを使いたい要件があるんですが、公式のドキュメントを読んでも「いや知りたいのはそこじゃねーよ」と言うものばかりなのでメモしていきます。
JavaDocはこれを参考にしています。正直このJavaDocが生成されたのがいつ時点のビルドかわからないのでもっと色々追加されてるのかもしれないんですが、ちょっとそこまで追う気力はないです。
覚えておきたい前知識
重要なオブジェクトはこの辺です。
RequestQueue(JavaDoc)
Request<T>(JavaDoc)
Network(JavaDoc)
BasicNetwork(JavaDoc)
HttpStack(JavaDoc)
HttpClientStack(JavaDoc)
HurlStack(JavaDoc)
RequestQueueはその名の通りRequestをキューイングするクラスです。ExecutorServiceみたいなものだと思っておけばいいです。
Request<T>もその名の通りHTTPリクエストに関する諸々を設定するクラスです。URLだとか、GET/POSTメソッドだとかをコンストラクタで指定します。
deliverResponseメソッドは明らかにレスポンスに関わる部分なんですが、まぁここにあったほうが便利ですね。
NetworkはRequestを受け取って実行しNetworkResponseを返す、いわばアダプタです。実装はBasicNetworkが用意されていますし、これで十分です。
と言うか、Volley#newRequestQueue経由でインスタンスを作成するとNetworkの指定すらできません。
HttpStackはBasicNetworkで使用される、実際の通信部分です。実装としてはHttpClientStackとHurlStackが用意されています。Hurlは「HttpURL」の略だと思われます。
HurlStackはperformRequest内でHttpURLConnectionを作成しているんですが、Gingerbread(API Level 9)より前はHttpUrlConnectionにバグがあるので、minSdkVersionによってはちゃんと分岐させないと痛い目を見そうです。
Header / Post paramsの設定はRequest<T>のメソッドをオーバーライドする
HeaderとかPostのparamとか追加したいと思うんですが、その手のものはRequestのgetterをオーバーライドすることで渡せます。まぁ、1リクエスト毎に渡せた方がありがたいので、そうですねって感じ。
使いたいRequestをnewする際にオーバーライドするだけでもいけます。
RequestのJavaDoc眺めてsetterがなかったらそいつ、って認識。具体的にはこの辺のメソッド。
getHeaders
getParams
エンコード指定が必要ならこの辺も。特にgetParamsEncodingはデフォルトだとUTF-8を返すようになってるので注意したほうが良さそうです。
getParamsEncoding
getBodyContentType
リトライやタイムアウト設定はRetryPolicyを使う
タイムアウトの設定なんかはRequest#setRetryPolicyでRetryPolicyを渡せばOKです。
DefaultRetryPolicyのコンストラクタが便利です。デフォルト値はソース見たほうが早いです。
自己署名証明書によるHTTPS通信設定
HurlStackを使うかHttpClientStackを使うかで微妙に変わってきます。
細かい設定方法なんかは本旨にそぐわないので割愛。(めんどくさいんだよねあれ…。)
HurlStackを使う場合
コンストラクタでSSLSocketFactoryを渡せるので渡してあげましょう。
参考:Android - Volleyで自己署名証明書を使ったHTTPS通信 - Qiita
HttpClientStackを使う場合
どっちにしたってコンストラクタでHttpClientを渡さなきゃいけないのでいつも通りにやればOK。
参考:
Android: Trusting SSL certificates | Antoine Hauck’s blog
CA証明書とandroidの危うい関係 3 - Kazzzの日記
Cookieの設定とか取得とか
Cookieを使用する場合はRequestQueueのthreadPoolSizeを1にする必要があるらしいので、気をつけましょう。クソ面倒ですね。
設定も取得もCookieManagerを使うのがベストでしょう。リクエストを飛ばした後のHttpClientが手に入るならCookieStoreも併用したいところですが、残念ながらこれは手に入りません。レスポンスヘッダからちまちま取得します。
具体的にはこんなコードをRequest内で書けばOK。
public abstract class CookieRequest<T> extends Request<T> { // コンストラクタの実装とかは省略 // Cookieの設定 @Override public Map<String,String> getHeaders() throws AuthFailureError { Map<String,String> header = null; CookieManager cm = CookieManager.getInstance(); // 設定されたURLからCookieが取得できないか試行する String cookie = cm.getCookie(getUrl()); if(cookie != null) { header = new HashMap<String, String>(); String[] cookies = cookie.split(";"); for(String c : cookies) { c = c.trim(); String[] cs = c.split("="); // ヘッダにCookieを詰め込む header.put(cs[0], cs[1]); } } return header != null : header ? Collections.emptyMap(); } // Cookieの取得 @Override protected Response<T> parseNetworkResponse(NetworkResponse response) { Map<String, String> headers = response.headers; if(headers != null) { CookieManager cm = null; for(Entry<String, String> e : headers.entrySet()) { if(!"Set-Cookie".equars(e.getKey())) continue; if(cm == null) cm = CookieManager.getInstance(); cm.setCookie(getUrl(), e.getValue()); } } return super.parseNetworkResponse(response); } }
OKと言ったもののまだ動かしてないので本当にこれでいいのか微妙です。そのうち試します。
まとめ
HttpClient / HttpURLConnectionを後から変更できるようにしてくれ頼む。
参考
Android’s HTTP Clients | Android Developers Blog
Volleyでhttp headerを追加設定する - Qiita
Android - Volleyで自己署名証明書を使ったHTTPS通信 - Qiita
Android: Trusting SSL certificates | Antoine Hauck’s blog
CA証明書とandroidの危うい関係 3 - Kazzzの日記
Android - VolleyでCookieを設定してマルチリクエストをする方法 - Qiita
Android - VolleyでCookieを使い回す - Qiita
4. クッキーとセッション | TECHSCORE(テックスコア)
サイトデザイン変更のお知らせ
久しぶりにCSSを一部書き直しました。とは言えちょっとした変更なのでガラッと変わったわけでもないんですが。
最近はあのファッキンオブファッキンCSSプロパティことfloatを使わなくてもそれなりに見れる2カラムレイアウトが作れるんですね。いい時代になったもんだ。
逆に言えばCSSの知識はその辺で止まったままです。メディアクエリとかちんぷんかんぷんなんですが、色々面白いことが出来るんですね。
数年前に「レスポンシブデザイン」なんてのがバズワードになってたこともつい先日知りました。いやはやWebデザイナさんたちも大変だ。(他人事)
それでなくとも「Internet Ex某の互換性を保つためにはこれが必要である」とおまじないじみたmetaタグや条件付きコメント、乱立するjavascriptによる拡張ライブラリのあれこれを覚えたりとか、「CSSでこのプロパティを設定すると子要素のこのプロパティが無視されるから親要素にこのプロパティを入れないと思ったようにレンダリングされない」とか、「このHTML/JavaScriptの機能はGoogle C某、Fire某、Internet Ex某までは対応している。ただしI某のこのバージョンでは一部しか対応してない。」とか、「ちょっとこの画像をアニメーションさせてよ」と頼まれjQ某と言うあまりにも巨大になりすぎて一つの環境になってしまったライブラリの使い方を覚えなきゃいけないだとか、こう、列挙してみると思っていたより悲惨でびっくりしたんですが、その上「max-width / max-heightがこれぐらいの場合は大体スマートフォンでこれぐらいの場合は大体タブレットかPC」なんてことも覚えなきゃいけないんですね。あ、オシャレなフォントとそれを適用する方法なんてのも覚えなきゃいけないですね。
そりゃあWebデザイン関係のtipsを紹介しているブログも新興宗教の教祖みたいな口調にもなりますよ。こんなのずっとやってたら間違いなく生きてて辛いでしょうし。
でも最近はBootstrapなんてものもありますし、多少はマシなのかもしれないですね。私はJava SE6のころのJavaDocみたいな無骨さが好きなので「是非使ってみよう!」みたいな気持ちにならないまま今日に至ります。
何の話をしてたんだっけ。そう、デザイン。サイトデザイン。珍しくブラウザの互換性ってものを少し考えてみました。結果、現状維持でいくことにしました。
と言うのも、このサイトはIE7までならちゃんと見れることを開発者ツールの互換モードで確認できたからです。IE6はまぁ、1秒でも早く死んで欲しいのが全世界の総意なので死んでください。って言うかIE7で見れるようなサイトも見れないブラウザってもうそろそろ石油が精製できるレベルですよ。
ついでに2014年10月にこのサイトを訪れた人のブラウザをGoogle Analyticsで調べてみました。現時点で7,114セッションとなっています。
一番多いのはChromeでした。3,394セッションです。全体の40~50%。私は基本的にChromeを使わないので、みんな知らない人です。
バージョンに関してはあまりに細かすぎてよくわかりません。37~38が大体3,000セッションを占めているので、あんまり気にしなくてよさそうです。
次に多いのがIE。1,959セッション。
バージョンはIE10が突出して951、次点がIE8で434。IE9と11が拮抗しており、それぞれ292と228。IE7は49セッション、IE6は7セッション。IE6は全体から見て0.1%未満です。そろそろ絶滅してはいかがか。
FireFoxは3位でした。トータルで1,313セッションです。
バージョンで見ていくと、32が918セッション、33が222セッション。この時点でFirefoxの8割を占めていますね。
それ以降も31、24、29...と、比較的高い数字が並びます。まぁ14ぐらいでも普通に見れる(ただし死ぬほど重い)ので、Firefoxに関しては一切気にしていません。
4位がSafariで347セッション。5位がOperaの71セッション。後はもう雀の涙って感じですね。Lunascapeが1セッションだけありましたが、UAをみたら「6.9.1.27377 (using Trident 7.0)」とのこと。バージョンとしては結構高いみたいですが、どんな風に表示されるんでしょう。
なんだかとりとめのない話になってしまいましたが、これで終わりです。

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch • No registration required • HD streaming
[JavaScript]marked.jsを無理矢理拡張してオレオレパーサーを作る
前書き
最近例のクソアプリのマニュアルを書いているんですが、なんだかHTMLを書くのが面倒になってきました。
GitHub Pagesですし、jekyllを使えば比較的簡単にMarkdown記法でガリガリ書けるらしいんですけど、使い方を覚えるのが面倒です。また、Rubyの環境がないと書けないのもちょっと嫌です。
どうしようかなぁと色々探してみたらmarked.jsなるものを使えばとても簡単にMarkdownをHTMLに変換してくれると言うことを知り、じゃあ使ってみようか、となったわけです。
が、そのまま使うとなると色々と面倒なことがあるので、無理矢理拡張してオレオレパーサーにしてしまいました。
marked.jsの使い方
非常に簡単です。コード例を読むだけでも十分でしょう。
<!doctype html> <html> <head> <meta charset="utf-8"/> <title>Marked in the browser</title> <script src="lib/marked.js"></script> </head> <body> <div id="content"></div> <script> document.getElementById('content').innerHTML = marked('# Marked in browser\n\nRendered by **marked**.'); </script> </body> </html>
marked.jsを読み込んだ後、適当なStringをmarked関数に渡せば変換してくれます。
もちろん適当なStringは適当なmdファイルとして別途保存しておき、XHRで読み込んでもいいでしょう。と言うか、HTMLに埋め込む方が辛いので、普通はそうすると思います。
問題点
とりあえず適当なページのHTMLを紹介しておきましょう。
<!DOCTYPE html PUBLIC ""> <html lang="ja"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="chrome=1"> <meta name="description" content="Tumbling-dice.github.io : "> <link href="../stylesheets/stylesheet.css" rel="stylesheet" type="text/css" media="screen"> <script src="../javascripts/jquery-1.11.1.min.js" type="text/javascript"></script> <script src="../javascripts/create-index.min.js" type="text/javascript"></script> <title>包丁で刺されるはたてちゃんアラーム マニュアル - Twitter連携アカウント管理画面(AccountListActivity)</title> </head> <body> <!-- HEADER --> <div class="outer" id="header_wrap"> <header class="inner"> <a id="forkme_banner" href="https://github.com/tumbling-dice/Hatate">View on GitHub</a> <h1 id="project_title"><a href="../index.html">包丁で刺されるはたてちゃんアラーム</a></h1> <h2 id="project_tagline">マニュアル - Twitter連携アカウント管理画面(AccountListActivity)</h2> </header> </div> <!-- MAIN CONTENT --> <div class="outer" id="main_content_wrap"> <nav class="inner" id="sidebar"> <ol id="index"> <li><a href="#layout">レイアウト</a></li> <li><a href="#guide">利用者向けガイド</a> <ol> <li><a href="#guide-select">アカウント選択</a></li> <li><a href="#guide-add">アカウント追加</a></li> <li><a href="#guide-delete">アカウント削除</a></li> </ol> </li> <li><a href="#summary">(開発者向け)機能概要</a></li> <li><a href="#source">(開発者向け)ソース</a></li> </ol> </nav> <section class="inner" id="main_content"> <div id="layout"><h3><a name="layout" href="#layout" class="anchor"><span class="octicon octicon-link"></span></a> 1.レイアウト</h3> <p style="margin-left: 1em;">TODO:stub</p> </div> <div id="guide"><h3><a name="guide" href="#guide" class="anchor"><span class="octicon octicon-link"></span></a> 2.利用者向けガイド</h3> <div id="guide-select" style="margin-left: 1em;"><h4>2-1.アカウント選択</h4> <p style="margin-left: 1em;">チェックボックスにチェックが入っているアカウントはすべてTwitter連携対象アカウントになります。</p> <p style="margin-left: 1em;">連携対象からはずしたい場合はチェックをはずせばOKです。</p> </div> <div id="guide-add" style="margin-left: 1em;"><h4><a name="guide-add" href="#guide-add" class="anchor"><span class="octicon octicon-link"></span></a> 2-2.アカウント追加</h4> <p style="margin-left: 1em;">オプションメニューから「追加」を選択することで連携対象にしたいアカウントを追加することが出来ます。</p> </div> <div id="guide-delete" style="margin-left: 1em;"><h4><a name="guide-delete" href="#guide-delete" class="anchor"><span class="octicon octicon-link"></span></a> 2-3.アカウント削除</h4> <p style="margin-left: 1em;">もう使用したくないアカウントをロングタップすると削除確認ダイアログが表示されます。</p> <p style="margin-left: 1em;">問題なければOKをクリックして削除してください。</p> </div> </div> <div id="summary"><h3><a name="summary" href="#summary" class="anchor"><span class="octicon octicon-link"></span></a> 3.(開発者向け)機能概要</h3> <p style="margin-left: 1em;">TODO:stub</p> </div> <div id="source"><h3><a name="source" href="#source" class="anchor"><span class="octicon octicon-link"></span></a> 4.(開発者向け)ソース</h3> <ul> <li><a href="https://github.com/tumbling-dice/Hatate/blob/master/src/inujini_/hatate/AccountListActivity.java">AccountListActivity.java</a></li> <li><a href="https://github.com/tumbling-dice/Hatate/blob/master/res/layout/activity_list.xml">activity_list.xml</a></li> </ul> </div> </section> </div> <!-- FOOTER --> <div class="outer" id="footer_wrap"> <footer class="inner"> <p>Published with <a href="http://pages.github.com">GitHub Pages</a></p> <p id="last_update">Last Update: 2014/10/28</p> </footer> </div> </body> </html>
このHTMLをvue.jsを使って一種のテンプレート化してしまいます。
mdファイルの名前をクエリで渡すだけで使い回せるようにするためです。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset='utf-8'> <meta http-equiv="X-UA-Compatible" content="chrome=1"> <meta name="description" content="Tumbling-dice.github.io : "> <link rel="stylesheet" type="text/css" media="screen" href="../stylesheets/stylesheet.css" /> <script type="text/javascript" src="../javascripts/jquery-1.11.1.min.js"></script> <script type="text/javascript" src="../javascripts/vue.js"></script> <script type="text/javascript" src="../javascripts/marked.min.js"></script> <title>包丁で刺されるはたてちゃんアラーム マニュアル - {{title}}</title> </head> <body> <!-- HEADER --> <div id="header_wrap" class="outer"> <header class="inner"> <a id="forkme_banner" href="https://github.com/tumbling-dice/Hatate">View on GitHub</a> <h1 id="project_title"><a href="../index.html">包丁で刺されるはたてちゃんアラーム</a></h1> <h2 id="project_tagline">マニュアル - {{title}}</h2> </header> </div> <!-- MAIN CONTENT --> <div id="main_content_wrap" class="outer"> <nav id="sidebar" class="inner" v-html="index"></nav> <section id="main_content" class="inner" v-html="content"></section> </div> <!-- FOOTER --> <div id="footer_wrap" class="outer"> <footer class="inner"> <p>Published with <a href="http://pages.github.com">GitHub Pages</a></p> <p id="last_update">Last Update: {{lastUpdate}}</p> </footer> </div> </body> </html>
後はハイライトした部分の情報をMarkdown形式で記述すれば…と思ったんですが、ある部分はサイドバーに、またある部分は本文に、またある部分はタイトル、最終更新日として…と考えていくと、1ファイルに収めるのが非常に難しいことに気づきます。
かと言ってバカ正直に別ファイルに分けてしまうと管理が非常に面倒です。ここをどうにかしていきましょう。
Rendererの動作をオーバーライドする
marked.jsのオプションとして「Overriding renderer methods」なんてものが紹介されています。
Markdown記法をパースし、HTMLに変換する瞬間のメソッドを上書きできるようです。
// サーバサイドだとこれが必要 // var marked = require('marked'); var renderer = new marked.Renderer(); renderer.heading = function (text, level) { var escapedText = text.toLowerCase().replace(/[^\w]+/g, '-'); return '<h' + level + '><a name="' + escapedText + '" class="anchor" href="#' + escapedText + '"><span class="header-link"></span></a>' + text + '</h' + level + '>'; }, console.log(marked('# heading+', { renderer: renderer }));
<h1> <a name="heading-" class="anchor" href="#heading-"> <span class="header-link"></span> </a> heading+ </h1>
具体的にどのようなイベントをフックできるかは「Block level renderer methods」と「Inline level renderer methods」を見ておきましょう。
あくまでレンダリングのイベントなのでHTMLタグを自前で作らなきゃいけないんですが、デフォルトではどうやってるのかはソースを見ないとわかりません。
2014/10/30現在だと749行目あたりからRendererのprototypeを設定しています。その辺を掘っていけばいいでしょう。
これを上手く活用するとちゃんとしたMarkdown記法でありながら全然関係ないHTMLを返せるばかりか、特定のパターンの場合は一切レンダリングしないでテキストだけ受け取るなんてことも可能です。
タイトルでは「拡張」とか「パーサー」とか言ってますが、全くの嘘偽りであり、「marked.jsのRendererのメソッドをオーバーライドしてオレオレレンダリングを行う」が正しいです。
実際にやってみる
先ほどのHTMLの内容をMarkdown記法に書き換えました。
#Twitter連携アカウント管理画面(AccountListActivity) <index> 1. [レイアウト](#layout) 2. [利用者向けガイド](#guide) 1. [アカウント選択](#guide-select) 2. [アカウント追加](#guide-add) 3. [アカウント削除](#guide-delete) 3. [(開発者向け)機能概要](#summary) 4. [(開発者向け)ソース](#source) </index> ##layout TODO:stub ##guide ###guide-select チェックボックスにチェックが入っているアカウントはすべてTwitter連携対象アカウントになります。 連携対象からはずしたい場合はチェックをはずせばOKです。 ###guide-add オプションメニューから「追加」を選択することで連携対象にしたいアカウントを追加することが出来ます。 ###guide-delete もう使用したくないアカウントをロングタップすると削除確認ダイアログが表示されます。 問題なければOKをクリックして削除してください。 ##summary TODO:stub ##source * [AccountListActivity.java](https://github.com/tumbling-dice/Hatate/blob/master/src/inujini_/hatate/AccountListActivity.java) * [activity_list.xml](https://github.com/tumbling-dice/Hatate/blob/master/res/layout/activity_list.xml) [LastUpdate](2014/10/30)
実際にこれをパースしてViewModelを作ってみましょう。
$(document).ready(function () { // クエリからmdファイルの名前を取得する var query = window.location.search; query = query.substr(1, query.length); var queryItems = query.split("&"); var fileName; for (var i = 0, length = queryItems.length; i < length; i++) { var item = queryItems[i].split("="); var key = item[0]; if (key == "q") { fileName = item[1] ? window.decodeURIComponent(item[1]) : undefined; break; } } $.ajax({ url: "./" + fileName + ".md" }).success(function (data) { var renderer = new marked.Renderer(); var idx; renderer.html = function (html) { // indexタグがなかったらそのまま if (html.indexOf("<index>") == -1) { return html; } // indexタグで囲まれた部分をちゃんとパースする idx = marked(html.replace("<index>", "").replace("</index>", "")); // レンダリングする必要がなければ空文字を返せばOK return ""; }; var lastUpdate; var indexes = {}; renderer.link = function (href, title, text) { // textが「LastUpdate」だったら最終更新日を取得する if (text == "LastUpdate") { lastUpdate = href; return ""; } // hrefが#から始まる場合はナビゲーションなので、 // 見出しを保存しておく if (href.substr(0, 1) == "#") { indexes[href] = text; } return "<a href=\"" + href + "\" title=\"" + title + "\">" + text + "</a>"; }; var projectTagline; renderer.heading = function (text, level) { switch (level) { case 1: // 見出しレベルが1だったらタイトル projectTagline = text; return ""; case 2: case 3: // 見出しレベルが2~3 // かつ // ナビゲーションのhrefと同じtextだったら // ナビゲーション用のアンカーを作成する var hash = "#" + text; if (indexes[hash]) { // ナビゲーションのタイトルはハッシュから取得する var idxTitle = indexes[hash]; var $header = "<h" + (level + 1) + ">"; $header = $header + "<a name=\"" + text + "\" href=\"" + hash + "\" class=\"anchor\">" + "<span class=\"octicon octicon-link\"></span></a>" + idxTitle + "</h" + (length + 1) + ">"; return $header; } return "<h" + level + ">" + text + "</h" + level + ">"; default: return "<h" + level + ">" + text + "</h" + level + ">"; } }; renderer.paragraph = function (text) { return "<p style=\"margin-left:1em;\">" + text + "</p>"; }; // markedの第三引数にcallbackを設定できる marked(data, { renderer: renderer }, function (err, content) { new Vue({ el: "html", data: { title: projectTagline, index: idx, content: content, lastUpdate: lastUpdate } }); }); }); });
まとめ
とまぁ、こんな感じにRendererのメソッドを書き換えることでやりたい放題できます。
なるべくMarkdown記法内で完結できるようにすると楽ですが、どうにもならなかったら独自のタグを作ってRenderer.htmlで無理矢理変換してしまうのがオススメです。何も考えなくて済むので。
かと言って独自タグだらけにするとただのHTMLになるんですが…。
参考
HTML5 - Marked.js で Markdown をクライアント側でパースして表示する - Qiita
[Android]ListViewを使うための基礎知識(1)
前書き
Androidアプリを作り始めた人が必ずつまづくListViewのバッドノウハウを書き溜めていこうと思います。
AdapterViewのしくみ
全体的にそうなんですが、ListViewはListViewのドキュメントだけ読んでいても全然わかりません。AbsListViewのドキュメントもあわせて読む必要があります。AdapterViewのドキュメントも読んでおくとなおよしです。
と言うか、結構な人がAdapterViewについて理解していません。さらっと解説しておきましょう。
Class Overviewには
An AdapterView is a view whose children are determined by an Adapter.
と書かれています。って言うかそれしか書いてありません。とりあえず何の役割を持っているかだけでも知っておきましょう。
View生成の委譲
AdapterViewを継承しているViewは中で表示するViewのことを一切知りません。中のViewはAdapterが生成することになっているからです。
そう、あの忌まわしきgetViewメソッドによって生成されます。
クリックイベントの管理
AdapterViewはどんなデータがどんなレイアウトで表示されているかなど全く知りませんが、何番目のアイテムがクリックされたのかぐらいはわかります。
だから以下のクリックイベントを管理するListenerを登録することができます。
AdapterView.OnItemClickListener
AdapterView.OnItemLongClickListener
AdapterView.OnItemSelectedListener
各Listenerのメソッドのシグネチャを見てみると必ずこうなっています。
AdapterView<?> parent
View view
int position
long id
parentはそのListenerが登録されているAdapterViewです。viewはクリックされたview、positionはクリックされたアイテムの表示位置。idはAdapter#getItemId(int)で取得した値です。
たまにListView+OnItemClickListenerのサンプルとして、改めてonItemClick内でListViewを取得するものが紹介されているんですが、そんなことする必要はありません。parentがそれです。キャストすれば普通に使えます。
ListView listView = (ListView) findViewById(R.id.listview); listView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { ListView lv = (ListView) parent; //TODO: click event } });
もっと言えば、単にpositionからアイテムを取得するだけならAdapterView#getItemAtPositionを使えばいいです。返ってくるのがObjectなのが悲しいですね…。
ListView listView = (ListView) findViewById(R.id.listview); listView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Object item = parent.getItemAtPosition(position); // 以下と全く同じ処理。下手したらfindViewByIdを使ってるだけ遅いかも。 // ListView lv = (ListView) findViewById(R.id.listview) // ListAdapter adapter = lv.getAdapter(); // Object item = adapter.getItem(position); } });
Adapterのしくみ
Adapterの種類
じゃあ次はAdapterを見ていきましょう。と言ってもAdapterのサブクラスが既に大量に用意されているので、それぞれどんな時に使うべきか整理しておきましょう。
Adapter 概要 BaseAdapter インターフェースであるAdapterの基本的な実装。 (ただし実際にimplementしているのはListAdapterとSpinnerAdapter) SpinnerAdapter Spinner用のAdapter。 ListAdapter ListViewで表示するために最適化されたAdapter。 HeaderViewListAdapter ヘッダ用のViewを持ったListAdapter。 使われることはまずないし、public classとして存在している理由もわからない。 きっとこれを作った人がパッケージプライベートの存在を知らなかったのだろう。 WrapperListAdapter ListViewのaddHeaderView / addHeaderViewを呼ぶと内部のAdapterが勝手にこれに置き換わる。 ArrayAdapter<T> List構造の表示に特化。 List構造ならなんでもいいので汎用性があまりにも高い。 使い方さえ覚えてしまえば他のAdapterを一切使わなくても済む。 CursorAdapter CursorをListViewに紐付けることに適したAdapter。 ContentProcider経由で取得したデータを扱う時に使うと便利。 アプリ内のみで使用するDBでも適用できるが、「_id」と言うカラムが存在しないテーブルには使えないので注意。 また、Support Libary版が存在するのでimport時には注意。 ResourceCursorAdapter CursorAdapterをレイアウトXMLと紐付るためのAdapter。 これもSupport Libary版が存在する。 SimpleAdapter staticなコレクションを表示するのに最適。 シンプルと銘打っている割に面倒臭い。 SimpleCursorAdapter 簡単に使えるような努力をした形跡が見られるCursorAdapter。 簡単には使えない。 当然Support Library版が用意されている。
ちなみに、今回は説明しませんがExpandableListAdapterはAdapterの系譜ではありません。Group(親)とChild(子)の概念が必要になるからですね。
Adapter#getViewとconvertView
それじゃあ次にAdapter#getViewを見ていきましょう。
これは先ほど説明した通り、AdapterViewで表示するViewを生成するためのメソッドです。Adapterの最も重要な役割と言っても過言ではありません。
パラメータとしてこのような要素が渡されてきます。
position
表示するitemの位置
convertView
以前まで表示されていたView
parent
getViewで生成されたViewの親となるViewGroup
基本的な流れとしては
convertViewを使いまわすか判断する
使いまわせない場合はLayoutInflaterで使用するViewを生成する
positionとAdapter#getItemを使って表示すべきアイテムを取得する
1で取得したViewと2で取得したアイテムの情報を紐付ける
となります。
convertViewが使いまわせるかどうかの判定は非常に大事です。LayoutInflaterによる生成はかなり高コストなため、出来る限り使用しないよう気をつける必要があります。
じゃあどうやって判断するかですが、nullかどうか見てあげればOKです。
public View getView(int position, View convertView, ViewGroup parent) { View v = convertView; if(v == null) { v = _inflater.inflate(R.layout.adapter_test, null); } //TODO: get item and bind view properties return v; }
このconvertViewには画面上から表示しきれなくなったViewがやってきます。そのため、初回生成時にはnullとなっています。
表示しきれなくなったViewであっても(普通は)同じレイアウトリソースから生成されたViewでしょう。なので、内部で持っているViewの情報だけ書き換えてあげれば使いまわせるわけです。
やりようによってはアイテムが持っている諸々(フラグとか)からレイアウトリソースを決めることも出来ますが、convertViewを上手く使いまわすための工夫は入れておくべきでしょう。
public View getView(int position, View convertView, ViewGroup parent) { View v = convertView; MyData item = (MyData) super.getItem(position); int resourceId = item.isA() ? R.layout.adapter_a : R.layout.adapter_b; if(v == null) { v = _inflater.inflate(resourceId, null); // このViewのResource IDをTagとして持たせておく v.setTag(resourceId); } else { // convertViewがnullでなければResource IDを取得する int prevResourceId = (int) v.getTag(); if(resourceId != prevResourceId) { v = _inflater.inflate(resourceId); v.setTag(resourceId); } } //TODO: bind view properties return v; }
各AdapterのgetView
折角だからArrayAdapterのソースでも覗いてみましょう。
ArrayAdapterのコンストラクタではレイアウトファイルのIDとデータを表示するためのTextViewのIDを渡すことができます。
このコンストラクタで渡されたIDとitemsを使って表示してくれるわけですね。
ListView listView = (ListView) findViewById(R.id.listview); String[] items = {"a", "b", "c"}; ArrayAdapter<String> adapter = new ArrayAdapter<String>(getApplicationContext() // 表示したいレイアウトXMLのID , R.layout.adapter_array // レイアウトXMLの中にあるTextViewのID , R.id.textview // 表示するデータ , items); listView.setAdapter(adapter);
public View getView(int position, View convertView, ViewGroup parent) { return createViewFromResource(position, convertView, parent, mResource); } private View createViewFromResource(int position, View convertView, ViewGroup parent, int resource) { View view; TextView text; if (convertView == null) { // ここのresourceがR.layout.adapter_array view = mInflater.inflate(resource, parent, false); } else { view = convertView; } try { // mFieldIdがR.id.textview if (mFieldId == 0) { text = (TextView) view; } else { text = (TextView) view.findViewById(mFieldId); } } catch (ClassCastException e) { Log.e("ArrayAdapter", "You must supply a resource ID for a TextView"); throw new IllegalStateException( "ArrayAdapter requires the resource ID to be a TextView", e); } T item = getItem(position); if (item instanceof CharSequence) { text.setText((CharSequence)item); } else { text.setText(item.toString()); } return view; }
SimpleAdapterではもうちょっと手の込んだことをしています。
SimpleAdapterのコンストラクタは中々複雑です。
List<? extends Map<String, String>> dataList = new List<? extends Map<String, String>>(); // ひとつめのデータ HashMap<String, String> data1 = new HashMap<String, String>(); data1.put("key1", "value1-1"); data1.put("key2", "value1-2"); dataList.add(data1); // ふたつめのデータ HashMap<String, String> data2 = new HashMap<String, String>(); data2.put("key1", "value2-1"); data2.put("key2", "value2-2"); dataList.add(data2); ListView listView = (ListView) findViewById(R.id.listview); String[] keys = {"key1", "key2"}; SimpleAdapter adapter = new SimpleAdapter(getApplicationContext() // 表示するデータ , dataList // 表示したいレイアウトXMLのID , R.layout.adapter_simple // Mapに登録したKEYの配列 , keys // KEYと紐付けるViewのIDの配列 , new int[]{ R.id.textview_key_1, R.id.textview_key_2 }); listView.setAdapter(adapter);
public View getView(int position, View convertView, ViewGroup parent) { return createViewFromResource(position, convertView, parent, mResource); } private View createViewFromResource(int position, View convertView, ViewGroup parent, int resource) { View v; if (convertView == null) { // ここのresourceがR.layout.adapter_simple v = mInflater.inflate(resource, parent, false); } else { v = convertView; } bindView(position, v); return v; } private void bindView(int position, View view) { // mDataがdataList final Map dataSet = mData.get(position); if (dataSet == null) { return; } final ViewBinder binder = mViewBinder; final String[] from = mFrom; final int[] to = mTo; final int count = to.length; for (int i = 0; i < count; i++) { // R.id.textview_key_1, R.id.textview_key_2のどちらかを取得 final View v = view.findViewById(to[i]); if (v != null) { // "key1", "key2"のどちらかを取得 final Object data = dataSet.get(from[i]); String text = data == null ? "" : data.toString(); if (text == null) { text = ""; } boolean bound = false; if (binder != null) { bound = binder.setViewValue(v, data, text); } if (!bound) { // 取得したViewの種類によって何をするのか振り分けている if (v instanceof Checkable) { if (data instanceof Boolean) { ((Checkable) v).setChecked((Boolean) data); } else if (v instanceof TextView) { setViewText((TextView) v, text); } else { throw new IllegalStateException(v.getClass().getName() + " should be bound to a Boolean, not a " + (data == null ? "<unknown type>" : data.getClass())); } } else if (v instanceof TextView) { setViewText((TextView) v, text); } else if (v instanceof ImageView) { if (data instanceof Integer) { setViewImage((ImageView) v, (Integer) data); } else { setViewImage((ImageView) v, text); } } else { throw new IllegalStateException(v.getClass().getName() + " is not a " + " view that can be bounds by this SimpleAdapter"); } } } } }
とまぁ、見ての通り全然シンプルじゃないです。っつーかいちいち全部同じkeyを持ったMapを作らせるとか正気とは思えませんね。
SDKに事前に用意されているものでこれですから、汎用的なAdapterを作るのは相当難しいと考えていいです。そして作るべきではありません。
自作AdapterとViewHolderパターン
「独自のデータクラスを使いたい」「独自のレイアウトを使いたい」と言う場合は、何かしらのAdapterを継承し、getViewをオーバーライドしなければなりません。
継承するのにオススメなのは断然ArrayAdapterです。
まず、データソースがなんであれ、自分で変換してListに詰め込むぐらいならどうにでもなるでしょう。
また、addやremove、sortと言ったコレクション操作ライクなメソッドや、アイテムからpositionを割り出すgetPositionのようなメソッドはArrayAdapterだけが唯一最初から実装しています。
独自クラスの場合はequalsやhashCodeをオーバーライドしておけばさらに便利になりますし、Serializableを実装しておけば「クリックされたアイテムを他Activityに渡すIntent」のようなものも簡単に作れます。
そんなわけで、ArrayAdapterを継承したものを作ってみましょう。
public class MyData { private int id; private String name; // getter / setterは省略 }
public class MyAdapter extends ArrayAdapter<MyData> { private final LayoutInflater _inflater; public MyAdapter(Context context, List<MyData> objects) { // resourceのIDを受け取らない場合、 // 親のコンストラクタは0を指定しておけばOK super(context, 0, objects); _inflater = LayoutInflater.from(context); } public MyAdapter(Context context, MyData[] objects) { super(context, 0, objects); _inflater = LayoutInflater.from(context); } @Override public View getView(int position, View convertView, ViewGroup parent) { View v = convertView; if(v == null) { v = _inflater.inflate(R.layout.adapter_test, null); } MyData data = super.getItem(position); ((TextView)v.findViewById(R.id.txtId)).setText(String.valueOf(data.getId()); ((TextView)v.findViewById(R.id.txtName)).setText(data.getName()); return v; } }
getView内で使うLayoutInflaterはコンストラクタで作ってしまいましょう。
また、Contextは後から取得できないので必要なら保持しておきましょう。
ただし(Viewの生成を除く)Contextが必要な処理をAdapter内に記述するのは基本的にはよくないことです。あくまでもAdapterは「AdapterViewで表示するViewを生成する」ためのオブジェクトですから、Viewの表示以外のことをするべきではありません…。
また、「何度も何度もfindViewByIdをするのはコスト的に問題がある」と言う事で、ViewHolderパターンと呼ばれるものを使うことがほとんどです。
public class MyAdapter extends ArrayAdapter<MyData> { private final LayoutInflater _inflater; static class ViewHolder { TextView id; TextView name; } public MyAdapter(Context context, List<MyData> objects) { // resourceのIDを受け取らない場合、 super(context, 0, objects); _inflater = LayoutInflater.from(context); } public MyAdapter(Context context, MyData[] objects) { super(context, 0, objects); _inflater = LayoutInflater.from(context); } @Override public View getView(int position, View convertView, ViewGroup parent) { View v = convertView; ViewHolder vh = null; if(v == null) { v = _inflater.inflate(R.layout.adapter_test, null); vh = new ViewHolder(); vh.id = (TextView)v.findViewById(R.id.txtId); vh.name = (TextView)v.findViewById(R.id.txtName); v.setTag(vh); } else { vh = (ViewHolder) v.getTag(); } MyData data = super.getItem(position); vh.id.setText(String.valueOf(data.getId()); vh.name.setText(data.getName()); return v; } }
事前にViewの参照だけを保持しておくことで更にコストを低減させることができます。
やってみるとわかるんですが、かなり高速化されます。ListView、と言うか、Adapterを使うのであれば必ず覚えておきましょう。
(一旦の)まとめ
とりあえず今回はAdapterに焦点を絞って説明しました。DataSetObserverの話とかもしたかったんですが、字数が全然足りないですね。
また時間がある時に思いつくままに書こうと思います…。
[Android][Twitter4J]Oauth認証をServiceで行う(あるいは、Serviceの結果をBroadcastReceiverで受け取る)
前書き
AndroidでTwitter4Jを使ってOauth認証を行う方法は以前[Android][Twitter4J]AndroidでTwitter4Jを使うときのノウハウまとめ(前半)と言う記事で紹介したんですが、何と言うか、単刀直入に言うとバグっています。
具体的にどのような事象が発生するかと言うと、色々あるんですが、一番困ったのはstartActivityForResultを使ってOauth認証後のアクセストークンを手に入れることが出来ないと言うところです。そんなわけで今回は「Oauth認証後にちゃんとActivityにコールバックが送られる処理」を作ることを目標にします。
そもそもの話
そもそも、以下の理由から考えてもOauth認証のために1つActivityを作ること自体が間違っている気がします。
アプリ内の画面を遷移させる必要が全くない
HTTP通信が発生するので絶対に非同期処理にする必要がある
こう言ったものを処理するにはService、しかも確実に非同期で処理する必要があるならば、IntentServiceで実装するのが筋ってもんじゃないでしょうか。
ServiceからActivityへコールバックを送るための仕組み
これまた以前に[Android]Serviceとのプロセス間通信でデータを送受信すると言う記事でちょっとだけ書きました。
左記の記事ではContext#bindServiceを使う方式をメインに紹介しましたが、今回は別にバインドしなくていいのでContext#startServiceを使います。そしてServiceから1回だけコールバックしてもらいたい場合はBroadcastReceiverを使うのが普通です。
方式としてはこんなイメージです。
Activityを起動 ↓ ActivityのどこかでActivityとBroadcastReceiverを紐付ける ↓ ActivityのどこかでServiceを起動する ↓ Service内でOauth認証を行う ↓ Oauth認証後、ServiceからBroadcastReceiverを起動する ↓ Activityに紐付いたBroadcastReceiverが起動する ↓ 成し遂げたぜ。
BroadcastReceiverの継承と登録
先にBroadcastReceiverを作ってしまいましょう。基本的にはonReceiveだけ実装すればOKです。
public abstract class CallbackBroadcastReceiver extends BroadcastReceiver { public static final String ACTION_CALLBACK = "twitter4j.auth.action.callback"; public static final String KEY_DATA = "data"; static class Data implements Serializable { AccessToken token; Exception exception; boolean isSuccess; static Data create(AccessToken token) { Data data = new Data(); data.token = token; data.isSuccess = true; return data; } static Data create(Exception exception) { Data data = new Data(); data.exception = exception; data.isSuccess = false; return data; } } public static Intent createIntent(Data data) { Intent i = new Intent(); i.setAction(ACTION_CALLBACK); i.putExtra(KEY_DATA, data); return i; } public static IntentFilter createIntentFilter() { IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_CALLBACK); return filter; } @Override public void onReceive(Context context, Intent intent) { Data data = (Data) intent.getSerializableExtra(KEY_DATA); try { if(data.isSuccess) { onSuccess(data.token); } else { onError(data.exception); } } finally { getApplicationContext().unregisterReceiver(this); } } public abstract void onSuccess(AccessToken token); public abstract void onError(Exception exception); }
ServiceからはCallbackBroadcastReceiver#DataをIntent経由で送ってもらうことを想定しています。Oauth認証に成功していたらonSuccessへ、失敗していたらonErrorへ委譲します。
Activityとの紐付けはContext#registerReceiverを使います。
// 適当なところからのonClick想定 public void test(View v) { registerReceiver(new CallbackBroadcastReceiver() { @Override public void onSuccess(AccessToken token) { // TODO:成功時処理 } @Override public void onError(Exception exception) { // TODO:失敗時処理 } }, CallbackBroadcastReceiver.createIntentFilter()); // TODO: startService }
このように、BroadcastReceiverはAndroidManifestに記述しなくても使用することが出来ます。ただしその場合は必ずIntentFilterを作成する必要があるので、適当に独自のActionを定義しておきましょう。
Activity側とService側でActionが一致していないと当然コールバックとして機能しないので、BroadcastReceiver内に適当なstaticメソッドを作っておくと捗ります。
// Serviceでつかう public static Intent createIntent(Data data) { Intent i = new Intent(); i.setAction(ACTION_CALLBACK); i.putExtra(KEY_DATA, data); return i; } // Activityでつかう public static IntentFilter createIntentFilter() { IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_CALLBACK); return filter; }
IntentServiceの継承
IntentServiceの実装にあたって、とりあえず必要なものは以下の通りです。
引数無しのコンストラクタ
onHandleIntent
後者はabstractメソッドなので当然必須なんですが、前者はなんで必要かと言うと、ないと起動しないからです。と言うのも、Serviceのコンストラクタは引数なしのものしかないのに対し、IntentServiceはStringを一つ受け取るコンストラクタしかないからです。
当然Serviceを立ち上げる側はそのServiceを引数なしのコンストラクタで作ろうとするんですが、IntentServiceとしては引数なしのコンストラクタが定義されてないので作れないってわけですね。設計的にどうなのって気持ちはありますが我慢しましょう。
public class OauthService extends IntentService { public OauthService() { super("OauthService"); } public OauthService(String name) { super(name); } @Override protected void onHandleIntent(Intent intent) { // FIXME: implement } }
Oauth処理を組み込む
それじゃあonHandleIntent内にOauth認証に関する処理を記述していきましょう。consumer_keyとconsumer_secretはIntentで渡すようにします。
public class OauthService extends IntentService { public static final String KEY_CONSUMER_KEY = "consumerKey"; public static final String KEY_CONSUMER_SECRET = "consumerSecret"; public OauthService() { super("OauthService"); } public OauthService(String name) { super(name); } public static Intent createIntent(String consumerKey, String consumerSecret, Context context) { Intent intent = new Intent(context, OauthService.class); intent.putExtra(KEY_CONSUMER_KEY, consumerKey); intent.putExtra(KEY_CONSUMER_SECRET, consumerSecret); return intent; } @Override protected void onHandleIntent(Intent intent) { if(intent.hasExtra(KEY_CONSUMER_KEY)){ startOauth(intent); } else { if(intent.getData == null) { throw new IllegalStateException(); } getAccessToken(intent); } } /** * Oauth認証開始 * @param intent */ private void startOauth(Intent intent) { String consumerKey = intent.getStringExtra(KEY_CONSUMER_KEY); String consumerSecret = intent.getStringExtra(KEY_CONSUMER_SECRET); String callbackUri = "oauth://callback"; Configuration conf = new ConfigurationBuilder() .setOAuthConsumerKey(consumerKey) .setOAuthConsumerSecret(consumerSecret) .build(); OAuthAuthorization oauth = new OAuthAuthorization(conf); oauth.setOAuthAccessToken(null); String uri; try { uri = oauth.getOAuthRequestToken(callbackUri).getAuthorizationURL(); } catch (TwitterException e) { e.printStackTrace(); // error callback CallbackBroadcastReceiver.Data data = CallbackBroadcastReceiver.Data.create(e); sendBroadcast(CallbackBroadcastReceiver.createIntent(data)); return; } try { serialize(oauth, "oauth.dat"); } catch (IOException e) { e.printStackTrace(); // error callback CallbackBroadcastReceiver.Data data = CallbackBroadcastReceiver.Data.create(e); sendBroadcast(CallbackBroadcastReceiver.createIntent(data)); return; } Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(uri)); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(i); } /** * AccessToken取得 * @param intent */ private void getAccessToken(Intent intent) { OAuthAuthorization oauth = null; try { oauth = deserialize("oauth.dat"); } catch(Exception e) { e.printStackTrace(); // error callback CallbackBroadcastReceiver.Data data = CallbackBroadcastReceiver.Data.create(e); sendBroadcast(CallbackBroadcastReceiver.createIntent(data)); return; } String verifier = intent.getData().getQueryParameter("oauth_verifier"); AccessToken accessToken; try { accessToken = oauth.getOAuthAccessToken(verifier); } catch (TwitterException e) { e.printStackTrace(); // error callback CallbackBroadcastReceiver.Data data = CallbackBroadcastReceiver.Data.create(e); sendBroadcast(CallbackBroadcastReceiver.createIntent(data)); return; } // success callback CallbackBroadcastReceiver.Data data = CallbackBroadcastReceiver.Data.create(accessToken); sendBroadcast(CallbackBroadcastReceiver.createIntent(data)); } private void serialize(OAuthAuthorization obj, String fileName) throws IOException { try { FileOutputStream fos = openFileOutput(fileName, MODE_PRIVATE); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(statuses) } finally { if(fos != null) { if(oos != null) oos.close(); fos.close(); } } } private OAuthAuthorization deserialize(String fileName) throws Exception { try { FileInputStream fis = openFileInput(path); ObjectInputStream ois = new ObjectInputStream(fis); return (OAuthAuthorization) ois.readObject(); } finally { if(fis != null) { if(ois != null) ois.close(); fis.close(); } deleteFile(fileName); } } }
AndroidManifestはこんな感じになります。android:nameのパッケージ名は適当に書き換えてください。
<service android:name="inujini_.hatate.service.OauthService" android:exported="false"> <intent-filter> <action android:name="android.intent.action.View" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="oauth" android:host="callback"/> </intent-filter> </service>
ちょっとトリッキーなところとして、OAuthAuthorizationのシリアライズ / デシリアライズがあります。
TwitterのOauth認証画面へ遷移するときと、Oauth認証が終わってOauthServiceへリダイレクトされた後のOAuthAuthorizationは必ず同じものを使う必要があります。しかし、仕組み上どうしてもそれぞれ別のOauthServiceインスタンスを使わざるをえません。
static変数で持っておくなどの対処も考えられますが、うっかりGCされた日には「Twitter側では認証できたのにアプリ側でエラーになったからクソ」と罵られても文句は言えませんし、再現性もない可能性があるので、いっそのこと一回ファイルに吐き出してしまったほうが安全かなと。
中継用のActivityをつくる
で、実際に動かしてみると、動きません。実際にはOauth認証が成功するところまで行きますが、コールバック用URIにリダイレクトする際に404になってしまいます。
Twitter側でOauth処理が完了するとandroid.intent.category.BROWSABLEとしてoauth://callback~と言うURIが投げられるのですが、Serviceがそれを捕まえてくれません。どうもServiceはandroid.intent.category.BROWSABLEのIntentを無視するみたいです。
いやまぁ…。「BROWSABLE」なのにServiceで受け取るってこと自体、普通に考えたら間違いなくおかしなことなんですが…。
仕方ないのでリダイレクトを受け取ってServiceに投げ直すためだけのActivityを作成します。
public class OauthActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = new Intent(getApplicationContext(), OauthService.class); intent.setData(getIntent().getData()); intent.putExtra(OauthService.KEY_FINISHED_OAUTH, true); startService(intent); finish(); } }
Manifestはこんな感じ。
<activity android:name="inujini_.hatate.OauthActivity"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="oauth" android:host="callback"/> </intent-filter> </activity> <service android:name="inujini_.hatate.service.OauthService" android:exported="false" />
結局Activityを作らなきゃいけないのが悲しいですね。
まとめ
後は実例…と思ったんですが、いつものクソアプリのソースでお茶を濁します。
NotificationActivity.java
AccountListActivity.java
また、今回作ったものの完全版も紹介しておきます。この記事内のコードは叩き台の要素が強いので、多少リファクタリングする予定です。
OauthService.java
CallbackBroadcastReceiver.java
OauthActivity.java
Oauth認証に限らず、何らかのIntentを交互に飛ばしあうような処理ならこの手段が使えるはずです。ちょっと具体的な例は思いつきませんが、アプリから自前のHTTPサーバへ飛ばす→ブラウザで操作させる→操作結果をアプリに反映させる、とか、そんなことも出来るはずです。
いい加減Tumblrの制限に飽き飽きしてきたのではてブロあたりに移行しようか検討中。
するとなると一回Movable Typeのフォーマットに変換しないといけないんだけど、ざっと仕様を見た感じだとそんなに難しくなさそう。
[Android]アイテムクリック時にイベントを発生させるListPreferenceを作成する
前書き
相変わらずこのような愚にもつかないクソアプリを作っています。まぁ、楽しいんですが…。それなりには。
設計上Preferenceを多用するのが一番楽だと判断しましたし、それは恐らく間違ってないんですが、PreferenceそのものがAndroid SDKにおけるBad Partの一つと言っても過言ではないので、ちょっと気を利かせたPreferenceを自作しようとすると結構面倒です。
と言うか、毎回継承しようとするクラスのソースを読まないと動作が理解できないって言うのはフレームワークとして普通に大失敗だと思うんですが、どうなんでしょう。どうなんでしょうって言うか、いい加減にして欲しいんですけどね。
今回の要件
Notificationのプロパティとしてvibrateと言うものがあります。読んで字の如く、通知する際に端末を振動させてくれます。そして型を見てもらえばわかる通りlongの配列を渡す必要があります。
具体的にどうやって指定するかと言うと、off,on,off,on…の周期をミリ秒の配列として渡してやればOKです。see alsoとして挙げられているVibrator#vibrateにそんな説明が書いてあります。
Notification n = new Notification(); n.vibrate = new long[]{ 0, 1000, 500, 1000}; // NotificationManagerからNotificationを発行するところは省略
このバイブのパターンをListPreferenceでユーザに設定させたい、と言うのが今回の趣旨です。
ListPreferenceの使い方
まぁこれぐらいなら普通のListPreferenceでも設定できるんですけどね。とりあえずドキュメントを読んでみましょう。
ListPreference独自のAttrとしてandroid:entriesとandroid:entryValuesがあります。これはどちらもStringArrayのリソースIDを指定する必要があります。(StringArray以外のarrayは渡すことができないので注意)
実例がないとわかりにくいと思うので他の部分で使っているものを紹介しておきましょう。LEDの色をListPreferenceで設定させています。
arrays.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string-array name="LightColorList"> <item>赤</item> <item>青</item> <item>緑</item> </string-array> <string-array name="LightColorValues"> <item>0xffff0000</item> <item>0xff0000ff</item> <item>0xff00ff00</item> </string-array> </resources>
main.xml(抜粋)
<ListPreference android:title="光の色" android:summary="@string/summary_light" android:entries="@array/LightColorList" android:entryValues="@array/LightColorValues" android:key="lightColor" android:dependency="isLight"/>
このように設定すると画面上は赤、青、緑のリスト(LightColorList = android:entries)が表示されますが、実際に設定されている値はそれに対応するカラーコード(LightColorValues = android:entryValues)になっている、と言った按配です。
実際に値を取得する時はこんな感じになります。
public static long getLightColor(Context context) { SharedPreference pref = PreferenceManager.getDefaultSharedPreferences(context); return Long.decode(pref.getString("lightColor", "0xffff0000")); }
ListPreferenceのダメなところ
今回の要件から言えばダメなところが二つあります。
entryValuesの型がStringArrayで固定されている
リストから選択すると即ダイアログが閉じてしまう
特に前者の方は引っかかったことがある人も多いんじゃないでしょうか?IntegerArrayやTypedArrayだって受け取れそうなものなのに、と言いたいところなんですが、ちゃんと考えてみると結構難しいんですよこれが。
なんでこうなっているのかを知るにはListPreferenceのソースからコンストラクタを見てみるのが一番早いでしょう。
private CharSequence[] mEntries; private CharSequence[] mEntryValues; public ListPreference(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ListPreference, 0, 0); mEntries = a.getTextArray(com.android.internal.R.styleable.ListPreference_entries); mEntryValues = a.getTextArray(com.android.internal.R.styleable.ListPreference_entryValues); a.recycle(); }
とまぁ、ここで型を決め打ちしてしまっています。これを対処するとなると何らかの型情報をattrsで渡すしかなさそうですし、mEntries / mEntryValuesの型はObject[]にしないといけません。(Javaの配列は共変であり、Object[]にString[]もInteger[]も突っ込めてしまうが絶対にやるべきではない。)
じゃあジェネリクスにすれば、と思うんですが、残念ながらxmlに記述する際に型パラメータを指定することは出来ませんし、結局何かしらの型情報を渡さないと最終的にSharedPreferenceに保存できないです。
どうするにせよ型の問題をどうにかしようとするとListPreferenceを一から作り直さなくてはなりません。そして一から作り直したにしてはあまりにもお粗末なものが出来上がることが目に見えています。ここは涙を飲んで諦めましょう。まぁ、Stringならどうとでもなりますしね。
後者を直すのはさほど難しくないです。と言うか、選択された時点でダイアログを閉じてしまっても問題がないことの方が多いんですが、いちいちパターンを選択する→プレビューで試してみる→パターンを選び直す…と言う作業をユーザにやらせるのはちょっとクールじゃないってだけなので、ぶっちゃけ対処しなくてもいいレベルです。それじゃ話にならないのでちゃんと直しますけども。
ListPreferenceの実装を調べる
と言うわけで、もう一度ListPreferenceのソースに戻ってその辺の制御をしているところを探してみます。ListPrederenceはDialogPreferenceを継承しているので、恐らくAlertDialog.BuilderにListViewか何かを突っ込んでるのかなとあたりをつけていくと、onPrepareDialogBuilderと言うメソッドにぶつかります。
@Override protected void onPrepareDialogBuilder(Builder builder) { super.onPrepareDialogBuilder(builder); if (mEntries == null || mEntryValues == null) { throw new IllegalStateException( "ListPreference requires an entries array and an entryValues array."); } mClickedDialogEntryIndex = getValueIndex(); builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { mClickedDialogEntryIndex = which; /* * Clicking on an item simulates the positive button * click, and dismisses the dialog. */ ListPreference.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE); dialog.dismiss(); } }); /* * The typical interaction for list-based dialogs is to have * click-on-an-item dismiss the dialog instead of the user having to * press 'Ok'. */ builder.setPositiveButton(null, null); }
ListViewは現れませんでしたがAlertDialog.Builder#setSingleChoiceItemsを使って制御しているようです。このメソッドで渡しているDialogInterface.OnClickListenerの中身を見ていきましょう。
new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { mClickedDialogEntryIndex = which; /* * Clicking on an item simulates the positive button * click, and dismisses the dialog. */ ListPreference.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE); dialog.dismiss(); } }
mClickedDialogEntryIndexは選択されたリストアイテムのindexです。実際に値を保存する時に使います。(と言うか、それ以外で使われない)
ここのdialog.dismissだけ消せればなぁ、と思うんですが、どうしようもありません。
さて、DialogPreferenceで表示されているdialogがdismissされるとonDialogClosedと言うメソッドに飛びます。わざわざcallChangeListenerを呼び出していることから推測するに、ListPreferenceではここで値を保存しているようですね。
@Override protected void onDialogClosed(boolean positiveResult) { super.onDialogClosed(positiveResult); if (positiveResult && mClickedDialogEntryIndex >= 0 && mEntryValues != null) { String value = mEntryValues[mClickedDialogEntryIndex].toString(); if (callChangeListener(value)) { setValue(value); } } }
このpositiveResultとは何ぞや、って言うのは、DialogPreferenceのソースを見に行く必要があります。
public void onDismiss(DialogInterface dialog) { getPreferenceManager().unregisterOnActivityDestroyListener(this); mDialog = null; onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE); } /** * Called when the dialog is dismissed and should be used to save data to * the {@link SharedPreferences}. * * @param positiveResult Whether the positive button was clicked (true), or * the negative button was clicked or the dialog was canceled (false). */ protected void onDialogClosed(boolean positiveResult) { }
ListPreferenceがonPrepareDialogBuilderでやっていた内容を思い出しましょう。ListPreference.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE);とbuilder.setPositiveButton(null, null);なんてコードがありました。リストのアイテムを選択した瞬間にDialogPreference#mWhichButtonClickedをDialogInterface.BUTTON_POSITIVEにし、dialog.dismissすることで強制的にpositiveResultをtrueにしているわけです。うーん。アドホック感漂ってますね。
ListPreferenceを継承して作り直す
これでListPreferenceが何をやっているのかなんとなくわかりました。次はこれをどう直していくかを考えましょう。
極端な話、onPrepareDialogBuilderで設定しているDialogInterface.OnClickListenerを差し替えてしまう、あるいは、何らかの形で委譲できるようにしてしまえば目的は達成されるように見えます。
単純にDialogInterface.OnClickListenerを受け取れるようにするならこんな感じですね。ただ、onDialogClosedのpositiveResultをtrueで貰えるようにしなきゃいけないので、PositiveButtonはちゃんと作成します。
public class EventableListPreference extends ListPreference { @Accessors(prefix="_") @Setter private DialogInterface.OnClickListener _onClickListener; public EventableListPreference(Context context) { super(context); } public EventableListPreference(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { val entries = super.getEntries(); val entryValues = super.getEntryValues(); if (entries == null || entryValues == null) { throw new IllegalStateException( "EventableListPreference requires an entries array and an entryValues array."); } val entryIndex = super.findIndexOfValue(super.getValue()); builder.setSingleChoiceItems(entries, entryIndex, _onClickListener); builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // いるのかな…? EventableListPreference.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE); dialog.dismiss(); } }); } }
とは言えこのままだと動きません。ListPreference#mClickedDialogEntryIndexが変わらないので、どれだけ選択しても最終的にはずっと同じ値で保存されます。仕方ないので同じようなプロパティをこっちでも所持し、onDialogClosedをオーバーライドして全く同じことをするようにします。
また、DialogInterface.OnClickListenerを受け取るようにするとやっぱり意味がないので、別のコールバック用インターフェースを作成します。
public class EventableListPreference extends ListPreference { public interface OnChosenListener { public void onChosen(int index, String entry, String entryValue); } private int _selectedEntryIndex; @Accessors(prefix="_") @Setter private OnChosenListener _onChosenListener; public EventableListPreference(Context context) { super(context); } public EventableListPreference(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { val entries = super.getEntries(); val entryValues = super.getEntryValues(); if (entries == null || entryValues == null) { throw new IllegalStateException( "EventableListPreference requires an entries array and an entryValues array."); } builder.setSingleChoiceItems(entries, super.findIndexOfValue(super.getValue()) , new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { _selectedEntryIndex = which; if(_onChosenListener != null) { _onChosenListener.onChosen(which , EventableListPreference.super.getEntries()[which].toString() , EventableListPreference.super.getEntryValues()[which].toString()); } } }); builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { EventableListPreference.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE); dialog.dismiss(); } }); } @Override protected void onDialogClosed(boolean positiveResult) { val entryValues = super.getEntryValues(); if (positiveResult && _selectedEntryIndex >= 0 && entryValues != null) { val value = entryValues[_selectedEntryIndex].toString(); if (callChangeListener(value)) { super.setValue(value); } } } }
onPrepareDialogBuilderもonDialogClosedもsuperを呼び出す必要はありません。ListPreferenceの方はそもそも呼び出したくないですし、DialogPreferenceの方はソースを見ればわかる通りどちらも空っぽです。
まとめ
後は実例…と思ったんですが、PreferenceActivityからfindPreferenceでEventableListPreferenceを取得し、setOnChosenListenerを呼び出すだけです。その中でentryValueをlongの配列に変換し、Vibratorをゴニョゴニョすればいけます。
arrays.xmlに記述するバイブのパターンはこんな風にでもしておけばいいでしょう。
<resources> <string-array name="VibePatternList"> <item>1</item> <item>2</item> </string-array> <string-array name="VibePatternValues"> <item>0,1000,500,1000</item> <item>500,1500,1000,1500</item> </string-array> </resources>
参考
Java配列メモ(Hishidama’s Java Array Memo)

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch • No registration required • HD streaming
[Android]PreferenceActivityで使えるPreferenceを自作する
前書き
つい先日、むしゃくしゃしてこのようなアプリを作成、公開しました。
このアプリ自体は内輪ネタの極みなので死ぬほどどうでもいいんですが、色々と個人的に初めての試みがあり、その辺のノウハウをメモしていきたいなと。
と言うわけで、今回は自分でPreferenceを継承し、PreferenceActivityから呼べるクラスを作成してみます。
デフォルトで使用できるPreference
androidではSDK上にいくつかの便利なPreferenceが既に用意されています。PreferenceのKnown Direct Subclassesから引っ張ってみると、こんな感じですね。
DialogPreference
RingtonePreference
TwoStatePreference
更に(RingtonePreferenceはちょっと違うけど)上記のKnown Direct Subclassesを継承して作られているのが以下のPreferenceです。
CheckBoxPreference
EditTextPreference
ListPreference
MultiSelectListPreference
SwitchPreference
逆に言えば、上記の中で欲しい機能がなければ自分で実装するしかありません。ただし、見た目上の問題だけであれば、setLayoutResourceやsetWidgetLayoutResourceである程度差し替えることが可能です。(違いは後述)
で、今回はSeekBarを持ったPreferenceを作成してみたいと思います。これぐらいデフォルトで実装しておいてほしいものです。
Preferenceのレイアウト
そもそもPreferenceを継承して何かを作るとき、まず最初にどれから作っていけばいいのか、と言うのは、中々難しい問題です。ただ、他のPreferenceと合わせた時に違和感のあるレイアウトだと困ります。と言うわけで、レイアウト関連から考えていきましょう。
Preferenceのデフォルトレイアウトについてはこの記事が非常によくまとまっています。そして結局のところ、preference.xmlかpreference_holo.xmlのどちらかが選ばれます。
ただし、setLayoutResourceが呼び出されていると、このリソースへの参照ごと書き換わります。つまり、前述の2ファイルはそのPreferenceには一切適用されないわけです。
ただ、Preferenceを継承するなら必ずXMLのソースは一読しておくべきです。例えば、setLayoutResourceを使用しつつ、Preferenceに設定されたtitleやsummary、iconなどを自動で設定してもらいたい場合は、それぞれidとして
android:id/title
android:id/summary
android:id/icon
を指定しておけばよい、と言うことがわかります。ListActivityにおけるandroid:id/listと同じ要領です。
また、setWidgetLayoutResourceを呼び出すとtitleやsummaryが入っているRelativeLayoutの下のLinearLayoutに突っ込まれる、と言うこともわかります。これを頭にいれておくと無駄なコードを書かなくて済むようになる…かもしれません。
じゃあこのデフォルトレイアウトはPreferenceのどこで生成されるのかと言うと、onCreateViewです。ソースを確認してみましょう。
protected View onCreateView(ViewGroup parent) { final LayoutInflater layoutInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); final View layout = layoutInflater.inflate(mLayoutResId, parent, false); if (mWidgetLayoutResId != 0) { final ViewGroup widgetFrame = (ViewGroup)layout.findViewById(com.android.internal.R.id.widget_frame); layoutInflater.inflate(mWidgetLayoutResId, widgetFrame); } return layout; }
このソースを見れば、setLayoutResourceを使用しつつsetWidgetLayoutResourceも使用するにはandroid:id/widget_frameを持ったViewGroupがないといけない、と言うこともわかりますね。ライブラリとして公開するつもりがあるなら覚えておきたい事項です。
ただし、setWidgetLayoutResourceで設定するViewはtitleやsummaryの右側に表示されてしまいます。(CheckboxPreferenceのチェックボックスの位置)できれば下にもって行きたいです。
そこで、onCreateViewをオーバーライドしてtitleやsummaryが格納されているRelativeLayoutに対し動的に子ビューを追加して対処していきます。ただ、将来的なアップデートで使えなくなる可能性がなきにしもあらずですし、そもそももっとスマートな解決法がありそうな気がするんですが、わかりませんでした。(断念)
それじゃあ以上を踏まえて今回用のレイアウトファイルを作ってみましょう。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content"> <SeekBar android:id="@+id/seekbar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="10" /> <TextView android:id="@+id/txvValue" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginLeft="10dip" android:layout_weight="1" android:textSize="18sp" /> </LinearLayout>
次に実際にonCreateViewをオーバーライドしてみましょう。setLayoutResource / setWidgetLayoutResourceを呼び出すタイミングもここがベストなんじゃないでしょうか。
@Override protected View onCreateView(ViewGroup parent) { //setWidgetLayoutResource(R.layout.seekbar_preference); //return super.onCreateView(parent); // parentはListViewなので、動的に子ビューを追加する場合は // super.onCreateViewで手に入るViewを使う val root = (LinearLayout) super.onCreateView(parent); for (int i = 0, size = root.getChildCount(); i < size; i++) { val v = root.getChildAt(i); if(!(v instanceof RelativeLayout)) continue; val r = (RelativeLayout) v; val seekbarLayout = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.seekbar_preference, null); // 動的にRelativeLayout内のViewの位置を設定するには // RelativeLayout.LayoutParams#addRuleを使用する val params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.FILL_PARENT , RelativeLayout.LayoutParams.WRAP_CONTENT); // summaryはinternalなidなのでgetIdentifierで強引に取得する params.addRule(RelativeLayout.BELOW, Resources.getSystem().getIdentifier("summary", "id", "android")); // marginはseekbarLayoutのLayoutParamsから移し変えてもOK. val density = getContext().getResources().getDisplayMetrics().density; // 10dp params.topMargin = (int) (10f / density + 0.5f); seekbarLayout.setLayoutParams(params); r.addView(seekbarLayout); break; } return root; }
自作Attributeと表示データのバインディング(onBindView)
実際にPreferenceに設定されている値を画面上に表示するのはonCreateViewではなくonBindViewでやるべき、とドキュメントには書かれています。(onCreateViewで値を設定するとこんなバグが発生する可能性がある。)なので、onCreateViewではレイアウト構造の作成までにとどめておき、実際にSeekBarの初期値などを設定するのはonBindViewでやりましょう。
ただ、そうした初期設定なんかを取得するには自作Attributeがあると便利です。この辺は割と頑張ってViewを自作したことがある人なら大体知っていると思いますし、調べるといっぱい出てくるので詳しい説明は割愛します。とりあえずは以下の三つの記事を読んでおけばOKです。
【Android】独自Viewを作成する - It’s now or never
AndroidのStyleとかThemeとかAttrとかStylableとか。 - Qiita
[Android]Custom View の作成と AttributeSet と declare-styleable | DevAchieve
で、とりあえず欲しいのはSeekBarの最大値ぐらいです。また、何故かdefaultValueは後から取得できないので、コンストラクタで取得する必要があります。
ここで注意しなければならないのはandroid.R.styleable配下の値はJavaのコードではどうやっても取得できないと言うことです。もしもandroid.R.styleable配下のattrを取得したい場合はnameを「android:○○」の形で指定する必要があります。
そんなわけでこんなものを適当に作っておきます。
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="SeekBarPreference"> <attr name="max" format="integer" /> <attr name="android:defaultValue"/> </declare-styleable> </resources>
そろそろコンストラクタとか色々準備し始めましょう。ついでにonBindViewも記述します。
public class SeekBarPreference extends Preference implements OnSeekBarChangeListener { @Accessors(prefix="_") @Getter @Setter private int _max; @Accessors(prefix="_") @Getter @Setter private int _currentValue; public SeekBarPreference(Context context, int max) { super(context); _max = max; } public SeekBarPreference(Context context, int max, int currentValue) { super(context); _max = max; _currentValue = currentValue; } public SeekBarPreference(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public SeekBarPreference(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs); } private void init(Context context, AttributeSet attrs) { @Cleanup("recycle") val t = context.obtainStyledAttributes(attrs, R.styleable.SeekBarPreference); // get max value _max = t.getInt(R.styleable.SeekBarPreference_max, 0); // get default value // constractor can not get persisted value // because SharedPreferences(PreferenceManager) has not initialize yet. _currentValue = t.getInt(R.styleable.SeekBarPreference_android_defaultValue, 0); } @Override protected View onCreateView(ViewGroup parent) { // set persited value val currentValue = super.getPersistedInt(-1); _currentValue = currentValue != -1 ? currentValue : _currentValue; val root = (LinearLayout) super.onCreateView(parent); for (int i = 0, size = root.getChildCount(); i < size; i++) { val v = root.getChildAt(i); if(!(v instanceof RelativeLayout)) continue; val r = (RelativeLayout) v; val seekbarLayout = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.seekbar_preference, null); val params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.FILL_PARENT , RelativeLayout.LayoutParams.WRAP_CONTENT); params.addRule(RelativeLayout.BELOW, Resources.getSystem().getIdentifier("summary", "id", "android")); val density = getContext().getResources().getDisplayMetrics().density; // 10dp params.topMargin = (int) (10f / density + 0.5f); seekbarLayout.setLayoutParams(params); r.addView(seekbarLayout); break; } return root; } @Override protected void onBindView(View view) { super.onBindView(view); ((TextView) view.findViewById(R.id.txvValue)).setText(String.format("%d/%d", _currentValue, _max)); val seekBar = (SeekBar) view.findViewById(R.id.seekbar); seekBar.setMax(_max); seekBar.setProgress(_currentValue); seekBar.setOnSeekBarChangeListener(this); } @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { //TODO: onProgressChanged } @Override public void onStartTrackingTouch(SeekBar seekBar) {} @Override public void onStopTrackingTouch(SeekBar seekBar) { //TODO: onStopTrackingTouch } }
AttributeSetを受け取るコンストラクタではinitメソッドに飛ばしてしまいます。PreferenceにはgetPersistedで始まるprotectedなメソッドを使うことで自身に設定されている値を取得できるので、persistedで取得できなかったらdefaultValueを使いたいんですが、コメントにも書いてある通りコンストラクタの時点ではPreferenceが所持しているSharedPreferences(を取得するPreferenceManager)が初期化されていないためpersistedは取得できません。仕方ないのでonCreateViewで取得するようにします。
onBindViewは割と見たまんまなので割愛します。後はOnSeekBarChangeListenerのメソッドであるonProgressChangedとonStopTrackingTouchを実装してしまえばクリア…なんですが、一つ問題があります。onCreateView / onBindViewで使用するViewは後から取得できません。getViewと言ういかにもなメソッドはあるんですが、こんな実装です。
public View getView(View convertView, ViewGroup parent) { if (convertView == null) { convertView = onCreateView(parent); } onBindView(convertView); return convertView; }
これはPreferenceGroupAdapter(is BaseAdapter)と言うPreferenceActivity(is ListActivity)が持つAdapterから呼ばれるものであり、基本的に我々が呼び出すものではありません。正直これはpackage privateでもよかったんじゃないかってレベルのメソッドです。
本来であれば、Preferenceで表示されているデータが変更された場合はnotifyChangedを呼ぶのが筋です。これを呼び出すとこんな形でメソッドが連鎖していきます。
Preference#notifyChanged ↓ PreferenceGroupAdapter#notifyDataSetChanged ↓ PreferenceGroupAdapter#getView ↓ Preference#getView ↓ (Preference#onCreateView) ↓ Preference#onBindView
結局のところonBindViewを呼び直すことになるので、ちゃんと値が変更されたように見えるわけです。それはそれでいいんですが、「SeekBarの値をちょっといじる度に今表示されているView(Preference)を再更新させるの?」と考えると、流石に無駄が多すぎる気がします。
と言うわけで、onBindViewが呼ばれたらViewの参照を弱参照として保持しておくことにします。更に、findViewByIdの負担を減らすためにViewHolderも用意します。
private WeakReference<TextView> _txvValue; private WeakReference<SeekBar> _seekBar; private static final class SeekBarPreferenceViewHolder { public final TextView txtValue; public final SeekBar seekBar; public SeekBarPreferenceViewHolder(View view) { txtValue = (TextView) view.findViewById(R.id.txvValue); seekBar = (SeekBar) view.findViewById(R.id.seekbar); } } @Override protected void onBindView(View view) { super.onBindView(view); val tag = view.getTag(); SeekBarPreferenceViewHolder vh = null; if(tag != null && tag instanceof SeekBarPreferenceViewHolder) { vh = (SeekBarPreferenceViewHolder) tag; } else { vh = new SeekBarPreferenceViewHolder(view); view.setTag(vh); vh.seekBar.setOnSeekBarChangeListener(this); } vh.seekBar.setProgress(_currentValue); vh.seekBar.setMax(_max); vh.txtValue.setText(String.format("%d/%d", _currentValue, _max)); if(_txvValue != null) _txvValue.clear(); _txvValue = new WeakReference<TextView>(vh.txtValue); if(_seekBar != null) _seekBar.clear(); _seekBar = new WeakReference<SeekBar>(vh.seekBar); } public void setMax(int max) { _max = max; changeTextView(); if(_seekBar == null) return; val seekBar = _seekBar.get(); if(seekBar != null) { seekBar.setMax(max); seekBar.setProgress(_currentValue); } } public void setCurrentValue(int currentValue) { _currentValue = currentValue; changeTextView(); if(_seekBar == null) return; val seekBar = _seekBar.get(); if(seekBar != null) seekBar.setProgress(currentValue); saveValue(_currentValue); } private void changeTextView() { if(txtView == null) return; val txtView = _txvValue.get(); if(txtView != null) txtView.setText(String.format("%d/%d", _currentValue, _max)); } private void saveValue(int v) { if(super.callChangeListener(v)) super.getEditor().putInt(super.getKey(), v).commit(); }
Preferenceに値をセットする
当然シークバーを動かしただけで勝手に保存してくれるわけではないので、自分で保存する動きを実装する必要があります。
と言ってもそんなに難しくないです。getEditorメソッドを呼び出せばSharedPreferences.Editorが手に入りますし、自分自身のKeyはgetKeyを呼ぶだけです。
([2014/10/15追記] persistで始まるメソッドを使うことで上記内容と同じ処理を行ってくれます。)
ついでに、値を変更する処理の前にcallChangeListenerを呼び出してあげると紳士的です。
@Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { _currentValue = progress; changeTextView(); } @Override public void onStartTrackingTouch(SeekBar seekBar) {} @Override public void onStopTrackingTouch(SeekBar seekBar) { _currentValue = seekBar.getProgress(); saveValue(_currentValue); } private void saveValue(int v) { if(super.callChangeListener(v)) // super.getEditor().putInt(super.getKey(), v).commit(); super.persistInt(v); }
まとめ
まぁ大体こんなところでしょうか。他のところは親クラスとしてのPreferenceがよきにはからってくれます。
最後に全体のコードを紹介して終わりにしようかと思いましたが、よくよく考えてみれば全ソースコードをGitHubにあげているのでそっちを見てもらうことにしましょう。文字数も足りないし。
SeekBarPreference.java
layout/seekbar_preference.xml
values/attr_seekbar_preference.xml
参考
Yukiの枝折: Android:Preference項目のレイアウトファイル
Android Preferenceの内容が他の項目に変わる | memorandum
【Android】独自Viewを作成する - It’s now or never
AndroidのStyleとかThemeとかAttrとかStylableとか。 - Qiita
[Android]Custom View の作成と AttributeSet と declare-styleable | DevAchieve
ViewGroupを継承した独自クラスにおいて、XML上の子Viewを制御するにはViewGroup#addView(View, int, ViewGroup.LayoutParams)をOverrideしてやればいいらしい。