2010年8月20日金曜日

Viewの状態を保存する

画面の向きが変わったときなどにViewが再作成されるが、
その時に状態を保存しておき、再作成後に復帰させる。

ActivityのonSaveInstanceState(Bundle) を使う方法は随所で解説されているが、カスタムView側で保存する方法もあったのでメモ。

状態の保存は、View.onSaveInstanceStateをオーバーライドして行う。
このメソッドは、状態を保存したParcelableオブジェクトを返す。

状態の復元は、View.onRestoreInstanceStateをオーバーライドして行う。
onSaveInstanceStateで保存したParcelableを引数で渡してくれるので、適宜復元する。

Parcelableオブジェクトは、Bundleでも動作するが、TextViewやAbsListViewではView.BaseSavedStateを継承して独自クラスを作っている。どれが推奨されているかは不明。

注意点としては、内部的に、ViewのIDをキーとしてParcelableが保存されるので、
ViewのIDが無いと保存できないことと、必ず一意にしておくこと。
もしIDが重複していると、最後に実行したonSaveInstanceStateの情報しか保存されない。

これはスタイル属性全般の注意点だが、カスタムViewを作成した時に、
コンストラクタで渡されたAttributeSetをそのまま子ビューに渡すと、スタイル属性が全て子ビューに渡ってしまう。
そのため、もし放置しておくと、自分自身と子Viewの全てが同じIDを持つことになってしまう。
この状態になると、当然だがonSaveInstanceStateはマトモに機能しない。

このスタイル属性からのID重複で丸一日ハマった・・・。

サンプルコードはこんな感じ。ソースをまんま貼り付けると、えらく間延びするなぁ。


    /**
     * 画面の向きの変更時など、オブジェクトが再作成される時に状態を保存する
     * @see android.view.View#onSaveInstanceState()
     */
    @Override
    protected Parcelable onSaveInstanceState()
    {
        Parcelable parent = super.onSaveInstanceState();

        /*
         * BaseSavedState拡張版
         */
        {
            SavedState saved = new SavedState(parent);

            saved.first = 1;
            saved.second = "Test";
            saved.third = true;

            return saved;
        }

        /*
         * Bundle版。クラス実装が不要なので手軽?
         */
//        {
//            Bundle b = new Bundle();
//
//            b.putParcelable("Parent", parent);
//            b.putString("myData", "myData");
//
//            return b;
//        }
    }
    /**
     * onSaveInstanceStateで保存した状態を復元する
     * @see android.view.View#onRestoreInstanceState(android.os.Parcelable)
     */
    @Override
    protected void onRestoreInstanceState(Parcelable state)
    {
        /*
         * BaseSavedState拡張版の復元
         */
        {
            if(!(state instanceof SavedState))
                return;

            SavedState saved = (SavedState)state;

            super.onRestoreInstanceState(saved.getSuperState());

            int first = saved.first;
            String second = saved.second;
            boolean third = saved.third;
        }

        /*
         * Bundle版の復元
         */
//        {
//            if(!(state instanceof Bundle))
//                return;
//
//            Bundle b = (Bundle)state;
//
//            super.onRestoreInstanceState(b.getParcelable("Parent"));
//            String s = b.getString("myData");
//        }
    }

    /**
     * onSaveInstanceStateで保存する状態を保持するためのクラス
     */
    public static class SavedState extends View.BaseSavedState
    {
        private int first;
        private String second;
        private boolean third;

        /**
         * Parcelから状態を復元するためのコンストラクタ
         */
        public SavedState(Parcel in)
        {
            /*
             * 必ず書いた順序で読み込むこと!
             */
            super(in);

            first = in.readInt();
            second = in.readString();
            third = in.readInt() == 0 ? false : true;
        }

        /**
         * 状態の保存用コンストラクタ
         */
        public SavedState(Parcelable superState)
        {
            super(superState);
        }

        /**
         * Parcelにデータを書き込む
         * @see android.view.AbsSavedState#writeToParcel(android.os.Parcel, int)
         */
        @Override
        public void writeToParcel(Parcel out, int flags)
        {
            super.writeToParcel(out, flags);

            out.writeInt(first);
            out.writeString(second);

            // booleanはそのまま書けないので、intなどに変換する
            out.writeInt(third ? 1 : 0);
        }

        /**
         * Parcelableの仕様で、ファクトリの実装が必要
         */
        public static final Parcelable.Creator CREATOR =
            new Parcelable.Creator()
        {
            public SavedState createFromParcel(Parcel source)
            {
                return new SavedState(source);
            }

            public SavedState[] newArray(int size)
            {
                return new SavedState[size];
            }
        };
    } 



2010年8月19日木曜日

カスタム属性をenumやビットフラグとして定義

たまたま見つけたのでメモ。xmlファイル記述時に検証してくれるので地味に便利かも。

enum
<attr name="enumList">
    <enum name="VAL0" value="0" />
    <enum name="VAL1" value="1" />
    <enum name="VAL2" value="2" />
</attr>

ビットフラグ
<attr name="flagList">
    <flag name="LEFT" value="1" />
    <flag name="TOP" value="2" />
    <flag name="RIGHT" value="4" />
    <flag name="BOTTOM" value="8" />
    <flag name="LEFTRIGHT" value="5" />
    <flag name="TOPBOTTOM" value="10" />
    <flag name="ALL" value="15" />
</attr>

2010年8月18日水曜日

ソースコード内で生成したViewインスタンスにStyleを適用

layout.xmlではstyle属性でStyleを適用できるが、ソース内で直接newしたViewに適用する方法が少しややこしかったのでメモ。

1. 適用したいstyleを作る
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="test">
        <item name="android:textSize">20dip</item>
        <item name="android:textColor">#FF0000</item>
    </style>
</resources>
2. styleを指定するための属性を作る
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <attr name="testStyle" format="integer|reference" />
</resources>
3. Activityに適用しているテーマに、2で作成した属性を使ってstyleへのリファレンスを指定する。
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="MyTheme" parent="@android:style/Theme">
        <item name="testStyle">@style/test</item>
    </style>
<resources>
4. コンストラクタに属性IDを渡す
TextView tv = new TextView(context, null, R.attr.testStyle);

Viewのコンストラクタでは、内部的にResources.Theme.obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)が呼ばれている。
このobtainStyledAttributesのdefStyleAttr引数は、上記1~4の通り動作する。defStyleRes引数は、styleリソースのIDを指定できるので、上記のR.style.testを直接引数に指定できる。

Viewのコンストラクタが、defStyle引数をobtainStyledAttributesのdefStyleAttrとdefStyleResの両方に渡してくれればstyleリソースのID直接指定もできると思うんだけど、ソースを見ると何故かdefStyleResに0を渡している。
1855     public View(Context context, AttributeSet attrs, int defStyle) {
1856         this(context);
1857
1858         TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View,
1859                 defStyle, 0);
わざわざテーマに指定するよりも、styleのIDを直接指定できた方が便利なんだけどなー。というか、defStyle引数の説明見ても、styleリソースID指定できるはずなんだけど。。。
「This may either be an attribute resource, whose value will be retrieved from the current theme, or an explicit style resource.」

2010年8月13日金曜日

日時とLocale、TimeZone関係のメモ

Date
日時を表すクラス。
new Date()で現在日時を返す。
1970/1/1 00:00:00 GMT からの経過msをlong値で取得可能 (getTime())。
このlong値は、TimeZoneが変わっても値は変わらない(というか、DateにTimeZoneという概念が無く、全部GMTとして処理するだけ?)

Locale
場所を表すクラス。
Locale.getDefault()で、システムに設定されている場所を返す。

TimeZone
タイムゾーンを表すクラス。
日本は"JST"、グリニッジ標準時は"GMT"。TimeZone.getDefault()で、システムに設定されているタイムゾーンを返す。

Calendar
カレンダーを表す抽象クラス。
LocaleやTimeZoneによる、月や週などの差異を吸収する。日時の増減を行う場合は、このクラスを使う。

DateFormat
日時の書式を表すクラス。
Localeによる、日時の表現の違いを吸収する。また、指定のTimeZone表現への変換も行う。日時の入出力を行う場合は、このクラスを使う。

android.text.format.DateFormat
androidで定義されているDateFormat。
androidでは、OSで日時の表示順序などを設定できる。そういったOS設定を考慮した書式を扱うことができる。
  • DateFormat.getDateFormat(context)
  • DateFormat.getMediumDateFormat(context)
  • DateFormat.getLongDateFormat(context)
  • DateFormat.getTimeFormat(context)

Time
androidで定義されている、Calenderに代わるクラス。

MotionEventに関するメモ

ViewのTouchイベントやTrackballイベントでは、MotionEventクラスによってポインタの情報が与えられる。※XperiaにはTackballは無い

以下、 MotionEventについて調べたことのメモ。

マウスポインタには、メインとサブがある。
マルチタッチ対応はAPI Level 5以降のため、複数ポインタに関する記述はAPI Level 5以降でのみ有効。

ポインタのイベントは、ACTION~のイベントコードで表される。
複数ポインタが存在する場合、一部のイベントは、ACTION_POINTER~のイベントコードで表される。これは、UPやDOWNなど位置以外の情報は、イベントコードでしか通知できないためである。
(複数ポインタがある状態でACTION_UPが来ても、どのポインタがUPされたか判別できない。)

ポインタのイベントコードは、getAction()で取得する。
戻り値をACTION_MASKとandすることで、イベントコードを取得できる。
また、API Leve 8以降では、getActionMasked()を用いると、ACTION_MASKとのandを省略できる。
API Level 1~4では、ACTION_MASKが存在しない。その場合は、getAction()の戻り値をそのまま使用する。

複数ポインタが存在する場合は、ポインタにIndexが付与されている。
ACTION_POINTER~イベントでは、下記のいずれかでポインタIndexを取得する。
  • getActionIndex() ※API Leve 8以降
  • API Level 5~7:(getAction() & ACTION_POINTER_ID_MASK ) >> ACTION_POINTER_ID_SHIFT
  • API Leve 8以降:(getAction() & ACTION_POINTER_INDEX_MASK ) >> ACTION_POINTER_INDEX_SHIFT

マウスポインタの位置は、通知されたイベントコードに関わらず毎回更新されている。
そのため、*常に*全ポインタの位置が検査されるべきである。
ポインタ数は、0 <= n < getPointerCount()。
API Level 1~4では、ポインタは常に1つだけのため、getPointerCount()は存在しない。

getEventTime()では、イベントが生成された時間を取得できる。
戻り値のlongは、UNIX時間(1970最初からの経過ms)。

getEdgeFlags()では、今回のイベントでViewの描画領域の端と接触したかどうかを取得できる。
戻り値は上下左右を表すビットフラグの集合。

API Level 5以降では、過去のACTION_MOVEイベントも記憶している。
getHistorical~(pinterIndex, pos)で取得可能。
posは履歴のIndex。最新のposは、getHistorySize()-1。

API Level 5以降の一部の機種では、画面が押された際の圧力を取得できる。
プログラム上では、getPressure~で取得可能。
通常は 0(圧力無し) ~ 1(通常圧力) の範囲のfloatだが、機種によっては1以上を返す可能性もある。

API Level 5以降の一部の機種では、押された画面領域サイズを取得できる。
プログラム上では、getSize~で取得可能。

recycle()が実装されているが、ハンドラ側では呼ばない。
もし特殊な理由で残しておきたい場合は、
MotionEvent.obtain(MotionEvent)やMotionEvent.obtainNoHistory(MotionEvent)でコピーを作成できる。作成したコピーはrecycle()する必要がある。

2010年8月12日木曜日

インテントとインテントフィルタ

Intentには明示的なインテントと暗黙的なインテントの2種類存在する。
参考:Intents and Intent FiltersのIntent Resolution

明示的なインテント
コンポーネント名を指定されたインテント。通常は、同一アプリ内のActivityを起動するために使用する。Intent(Context packageContext, Class<?> cls) など、コンテキストとクラスオブジェクトを指定してインスタンスを生成する。

明示的なインテントは、インテントフィルタは使用せずに、指定されたコンポーネント名で解決される。同一アプリケーション内のActivityを起動するだけなら、Actionなどは設定不要。

暗黙のインテント
コンポーネント名を指定しないインテント。通常は、他アプリへのインテントなど、コンポーネント名が事前に判らない場合に使用される。

暗黙のインテントは、インテントフィルタを用いて解決される。

インテントフィルタ
そのActivityを起動できるインテントを制限する。

インテントのAction・Category・Dataの内容を指定し、指定された内容のインテント以外はActivityを起動できないようにする。

インテントフィルタは、AndroidManifest.xml内に<intent-filter>で<action><category><data>を組み合わせて設定する。<action><category><data>は複数指定可能。また、<intent-filter>自体も複数指定可能。

主なインテント定数
参考:Intent

・ACTION.MAIN
データのやり取りをせずに、単にActivityを起動する。

・ACTION.VIEW
データを表示する。データはIntentに含まれるURIで指定される。

・Category.LAUNCHER
ランチャから起動可能

・Category.DEFAULT
参考:Note Pad ExampleのDEFAULTの解説

下記2つの例外を除き、全てのインテントに設定されているカテゴリ。暗黙のインテントを受け取る場合は、必ずフィルタに入れる。

- 明示的なインテント
- ACTION.MAIN と Category.LAUNCHER の組み合わせ

ACTION.MAIN と Category.LAUNCHER
参考:アクティビティとタスク

ランチャから起動可能なアクティビティに設定されるフィルタ。タスクのエントリポイントとなる。
この組み合わせが指定された場合は、起動モードはSingleTaskかSingleInstanceに設定することが望ましい。起動モードがDefaultかSingleTopの場合、ランチャから起動されるたびに新しいタスクが起動されてしまうため。

HandlerThread・Handler・Messageの使い方

1. 新規スレッドでLooperのメッセージループを開始

HandlerThreadThreadを継承しているため、
インスタンスを作ると新規スレッドが生成される。

HandlerThread.run()はオーバーライドされており、
Looperを使用してメッセージキューの生成→メッセージループの実行を行っている。

そのため、HandlerThread.start()でメッセージループが開始される。

2. Looperを関連付けたHandlerの生成

new Handler(HandlerThread.getLooper()) で、Looperのメッセージキューを使用するHandlerを生成する。
同一Looperに対して、Handlerは複数生成可能。

1~2のシーケンス図


3. Handlerに関連付いたMessageの生成

Handlerに関連付いたMessageを生成する (Message.obtain())。
Handlerは新規スレッドに関連付いているため、このMessageも別スレッドで処理される。
※Messageは処理後にLooper.loopでrecycle()されるため、ユーザーはrecycle()しない。

Messageの処理フローは、以下のような感じ。
if(Message.Runnable != null)
{
    //MessageにRunnableが関連付いていれば、Runnableを実行
    call Message.Runnable();
    return;
 }
 else
 {
    //HandlerにCallbackが関連付いていれば、Callbackを実行
    if(Handler.Callback != null &amp;&amp; (call Handler.Callback()) == false)
    {
        //HandlerにCallbackが関連付いていない or Callbackの戻り値がfalseなら、
        //Handler.handleMessageを実行
        call Handler.handleMessage();
    }
 }
3のシーケンス図