TimePickerの実装に闇があった(解決済み)


Lollipopの前身であるAndroid L Previewが登場してから、もうすぐ1年が経とうとしています。皆さんもLollipop触ってますか?

今回は、LollipopのAndroid SDKコードでたまたま見かけたアレな状況を紹介したいと思います。

TimePickerの新UI

TimePickerはAPI Level 1のときから存在する、最も古いUIコンポーネントの1つです。

Lollipopでは、従来のロール方式での時刻選択に加えて、新たに時計の文字盤を模したUIが追加されましたね。これらはモード選択によって、どちらも利用できるようになっています。

近年のNexusユーザーにとっては、そろそろ慣れ親しんだコントロールになってきていることでしょう。

spinnerモード
<TimePicker
    ...
    android:timePickerMode="spinner" />
clockモード
<TimePicker
    ...
    android:timePickerMode="clock" />

ハックしようと覗いてみたら

TimePickerを使っているとよくあるのが、「時計をN分刻みで選択できるようにしたい」という要件です。

これについては、StackOverflowを始めとして、いくつかの記事が公開されています。私はこちらの記事を参考にして、これまでTimePickerをハックしてきました。

簡単に言うと、「TimePickerはmMinuteSpinnerというNumberPicker型フィールドを持っているから、リフレクションでぶっこ抜いて制御しようぜ!」というやつです。

内部実装が従来とは違う

前述のとおり、TimePickerは必ずしもNumberPickerを並べただけのコントロールではなくなってしまったので、実装コードも大幅に変わっています。具体的には、SpinnerモードとClockモードのDelegateを作り、前述のandroid:timePickerModeの値に応じて内部実装を切り替えているのです。

ということで、前述のハックはLollipopでは動きません。一工夫する必要がありそうです。(この辺の話はまた別の機会に)

名は体を表・・・さない!?

新しいTimePickerのコードを掘り返していったところ、奇妙なものを見つけました。


* https://github.com/android/platform_frameworks_base/blob/android-5.0.0_r1/core/java/android/widget/TimePickerClockDelegate.java
* https://github.com/android/platform_frameworks_base/blob/android-5.0.0_r1/core/java/android/widget/TimePickerSpinnerDelegate.java

あ・・・ありのまま、今、起こった事を話すぜ!

「Clock用DelegateがNumberPickerを持ち、Spinner用Delegateが文字盤ビューを持っていた」

な・・・何を言っているのか、わからねーと思うが 

おれも、何を見たのか、わからなかった・・・

頭がどうにかなりそうだった・・・デザインパターンだとか命名規則だとか

そんなチャチなもんじゃあ、断じてねえ

もっと恐ろしいものの片鱗を、味わったぜ・・・

API Level 21 の闇

というわけで、名前と実装が逆転しているのを見つけてしまったわけですが、実際問題、レイアウトXMLに対して timePickerMode を指定する分には、ちゃんと動作しています。クラス名が逆なのだから、動作も逆になってしまいそうなものですが・・・

と不思議に思いながら、TimePickerがDelegateを切り替えている部分に何となく目を向けてみました。


https://github.com/android/platform_frameworks_base/blob/android-5.0.0_r1/core/java/android/widget/TimePicker.java#L87-L97

・・・

ん!?

こらこらこらこらー!!?

命名ミスったまま進んでしまったのか、何かのタイミングでRename系リファクタリングのミスがあったのかは知りませんが、宣言のたすき掛けによって、見事に前述の矛盾を誤魔化しています。

API Level 22 で直ってるよ

流石にこのままで放置されるはずもなく、現行最新版のAPI Level 22では、この問題は修正されています。

TimePicker.java
switch (mode) {
    case MODE_CLOCK:
        mDelegate = new TimePickerClockDelegate(
                this, context, attrs, defStyleAttr, defStyleRes);
        break;
    case MODE_SPINNER:
    default:
        mDelegate = new TimePickerSpinnerDelegate(
                this, context, attrs, defStyleAttr, defStyleRes);
        break;
}

ファイル名を取り替えるだけといえばだけなんですが、Git的には実装が丸々消されて書き換えられた感じになるので、修正コミットが大惨事になってました。

"Swap names for clock delegates so they are correct"
3 changed files with 1,444 additions and 1,444 deletions.
https://github.com/android/platform_frameworks_base/commit/daf33ed85353ab7d7a7668dd0e3f9a66f0d5583f

大変だなあ(他人事)

まとめ

面白いもの見つけちゃったなあと思いました(小並感)