カスタムNumberPickerの作成

AndroidのDatePickerやTimePickerは日付・時刻の入力を行うためのコンポーネントであるが、数値を1単位でインクリメント・デクリメントするための機能しかない。
例えば、時刻を入力するとき、1分間隔で入力することは少なく5分とか10分間隔で入力することが多いが、標準のTimePickerで実現できなかった。
そのため、TimePicker内部で使用しているNumberPickerを自作して使用していた。
このとき、下記の問題が発生していた。

  • apkファイルの肥大化(NumberPickerが内部で使用するリソースファイルをapkファイルに持つため、apkファイルのサイズが大きくなる)
  • Android SDKからコピーしたリソースファイルを流用しているので、カスタムUIを使用している端末(Xperia、Desire等)と同じにならない(背景色とか)。

この問題を解決するために、プライベートリソースの指定(例:res/layout/*.xmlandroid:drawable="@*android:drawable/timepicker_down_normal" でカスタムUIが持っているシステムリソースを参照する方法を試したが、AndroidアプリのSDKバージョンとAndroid端末のバージョンが一致しないとリソースを取得できないため、使用を諦めていた。

しかし、KazzzさんのNumberPickerを再作成する - Kazzzの日記 のblogと yanzmさんのY.A.M の 雑記帳: Android Get color of status barのblogを参考にさせていただいて、リフレクションを使用してAndroid標準のNumberPickerからリソースを取得すればいいことが分かったので、早速適用してみた。

注意点は、NumberPickerのパッケージがAndroid 2.1以下とAndroid 2.2以上で違うので、SDKバージョンを見てリフレクションのターゲットclassを切り替えている。

リフレクションのターゲットclassの切替とカスタムNumberPickerへのリソースの適用ソースは、下記になる。

// source file: com.example.android.customnumberpicker.widget.NumberPicker

public class NumberPicker extends LinearLayout implements OnClickListener, OnFocusChangeListener,
        OnLongClickListener {

    private static final String NUMBER_PICKER_CLASS_NAME;

    static {
        final int sdkVersion = Build.VERSION.SDK_INT;
        // 8=Build.VERSION_CODES.FROYO
        if (sdkVersion < 8) {
            NUMBER_PICKER_CLASS_NAME = "com.android.internal.widget.NumberPicker";
        } else {
            // Android 2.1 or higher
            NUMBER_PICKER_CLASS_NAME = "android.widget.NumberPicker";
        }
    }

    protected void setWidgetResource() {
        ....
        
        try {
            final Context context = getContext();
            final ClassLoader cl = context.getClassLoader();
            final Class<?> clazz = cl.loadClass(NUMBER_PICKER_CLASS_NAME);
            final Constructor<?> constructor = clazz.getConstructor(Context.class);
            final Object obj = constructor.newInstance(context);
            final Class<?> c = obj.getClass();

            {
                final Field field = c.getDeclaredField("mIncrementButton");
                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }
                final ImageButton internalIncrementButton = (ImageButton) field.get(obj);
                mIncrementButton.setBackgroundDrawable(internalIncrementButton.getBackground());
            }

            {
                final Field field = c.getDeclaredField("mText");
                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }
                final EditText internalText = (EditText) field.get(obj);
                mText.setBackgroundDrawable(internalText.getBackground());
                mText.setTextColor(internalText.getTextColors());
                // TextSizeを適用すると Android 2.2 800x480(hdpi) で、テキストが大きくなってしまうので、コメントアウト。
                //mText.setTextSize(internalText.getTextSize());
            }

            {
                final Field field = c.getDeclaredField("mDecrementButton");
                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }
                final ImageButton internalDecrementButton = (ImageButton) field.get(obj);
                mDecrementButton.setBackgroundDrawable(internalDecrementButton.getBackground());
            }
            mButtonsBackgroundInitilized = true;
        } catch (final Exception ex) {
            Log
                .e(
                    TAG,
                    "com.android.internal.widget.NumberPicker internal button background resource got not.",
                    ex);
        }
    }
    
    ....
}

サンプルとして、n分間隔でインクリメント・デクリメントするTimePickerを作成した。下記のURLからダウンロード可能。
http://www1.axfc.net/uploader/Sc/so/154123.zip

上記のサンプルを、各Adnroid端末で実行したときのスクリーンショットが下記になる。
左がカスタムTimePicker、右がTimePickerになる。
縦幅が若干違うのと日付アイコンが違うが、殆ど同じイメージになった。



  • HTC Desire(Android 2.1, 800x480 (hdpi))