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のシーケンス図

2010年8月9日月曜日

AdapterViewでデータバインド

 AdapterView ← Adapter ← データソース

AdapterView
Adapterを用いてViewを生成し、表示する。

Adapter
データソースからデータを読み取る。Viewを生成し、データをViewに設定する。Viewの生成には、layout.xmlによるテンプレートを使用可能。
  • ArrayAdapter:配列をデータソースとして、各要素をそれぞれTextViewにして返す。
  • SimpleCursorAdapterCursorをデータソースとする。返す要素はTextView or ImageView。Cursorの列ごと対応するViewIDを指定することで、使用するテンプレートを変更可能。
データソース
Adapterにデータを提供する。
  • Cursor:行・列を持つデータ構造。move~で行移動、get~(colIndex)で現在の行のデータ取得。

[memo]
・CursorAdapter.newViewで新しいViewを作った場合、1行分一気にできる?
→指定したlayout.xmlをinflateしてるだけぽい。

・行移動はAdapterView任せ?
→newView・bindViewともに行移動はしない。

・newViewでViewを生成→bindViewでデータセットの流れが正しい?
→正しい。View.findViewByIDで列に対応するViewを取得し、データをセットしている。

参考:SimpleCursorAdapter.bindView, ResourceCursorAdapter.newView

実行時に生成したViewにstyleを適用

簡単な方法は発見できず。
代替案は、生成したいView用のlayout.xmlを用意し、LayoutInflaterでxmlからViewを生成する。

res/values/styles.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Cell">
        <item name="android:background">#CDBA96</item>
        <item name="android:textColor">#FFFAF0</item>
        <item name="android:textSize">15dip</item>
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:gravity">center</item>
    </style>
</resources>
res/layout/row.xml
<?xml version="1.0" encoding="utf-8"?>
<TableRow xmlns:android="http://schemas.android.com/apk/res/android">
    <TextView style="@style/Cell" android:text="Cell1" />
    <TextView style="@style/Cell" android:text="Cell2" />
</TableRow>
Activity.java
LayoutInflater li = getLayoutInflater();
li.inflate(R.layout.row, (ViewGroup) findViewById(R.id.parent), true);

2010年8月4日水曜日

layoutXMLファイルでViewのカスタム属性を定義

1. カスタム属性を定義する
res/values/attr.xmlで属性を定義する。
  <xml version="1.0" encoding="utf-8"?>
  <resources>
    <attr name="textValue1" format="string" />
    <attr name="textColor" format="color" />
  </resources>
attr.xmlを編集すると、R.attrクラス内に属性定数が自動的に作成される。
  public final class R {
      public static final class attr {
          public static final int textValue1=0x7f010000;
          public static final int textColor=0x7f010001;
      }
  }
定義した属性名は、AndroidManifest.xmlで設定されている名前空間 (=Rオブジェクトの名前空間) に属する。今回は com.testapp を想定。
ただ、組み込みで大量の属性が用意されている (R.attr) ため、大抵の場合は自前で用意しなくてもいい気がする。

2. 属性を設定する
res/layout/main.xmlでViewに属性を設定する。TestView要素の属性のうち、tes名前空間のものがユーザー定義、android名前空間のものが組み込み。
  <?xml version="1.0" encoding="utf-8"?>
  <LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tes="http://schemas.android.com/apk/res/com.testapp"
      android:orientation="vertical"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
      >
  <com.testapp.view.TestView
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
     
      android:text="aaa"
      android:textSize="15pt"
     
      tes:textValue1="test1"
      tes:textColor="#FFFFFF"
    />
  <com.testapp.view.TestView
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
     
      android:text="aaa"
      android:textSize="4pt"
     
      tes:textValue1="test2"
      tes:textColor="#FFFFFF"
    />
  </LinearLayout>

3. 属性を読み込む
res/values/attr.xmlで、属性の配列を定義する。
属性の配列は、複数の属性を一括して読み込むため?に、複数の属性をまとめたもの。

android:textおよびandroid:textSizeは、Viewクラスでは読み込めない属性なので、TestView側で読み込み処理を書く必要がある。よって、styleableオブジェクトに含める。
  <?xml version="1.0" encoding="utf-8"?>
  <resources>

    <attr name="textValue1" format="string" />
    <attr name="textColor" format="color" />

    <!--属性の配列の定義-->
    <declare-styleable name="TestViewSet">
   
      <!--組み込みの属性-->
      <attr name="android:text" />
      <attr name="android:textSize" />
     
      <!--ユーザー定義の属性-->
      <attr name="textValue1" />
      <attr name="textColor" />
     
    </declare-styleable>

    <!--複数定義可能なことを例示するために記載-->
    <declare-styleable name="TestViewSet2">
      <attr name="android:text" />
      <attr name="textColor" />
    </declare-styleable>
   
  </resources>
R.styleable内に定義した情報が反映されていることを確認する。
  public final class R {
    public static final class styleable {
       
      //context.obtainStyledAttributes用のattr値配列
      public static final int[] TestViewSet = {
        0x01010095, 0x0101014f, 0x7f010000, 0x7f010001
      };
     
      //obtainStyledAttributesで取得したTypedArray内での、各属性のindex値
      public static final int TestView_android_text = 1;
      public static final int TestView_android_textSize = 0;
      public static final int TestView_textColor = 3;
      public static final int TestView_textValue1 = 2;
     
      //複数定義すれば、複数作成される
      public static final int[] TestViewSet2 = {
        0x0101014f, 0x7f010001
      };
      public static final int ViewBase_android_text = 0;
      public static final int ViewBase_textColor = 1;
    };
  }
TestViewクラスで読み込む
  /**
   * コンストラクタ
   */
  public TestView(Context context, AttributeSet attrs, int defStyle)
  {
    super(context, attrs, defStyle);

    //このオブジェクト用の属性セットを取得
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TestViewSet, defStyle, defStyle);

    //android:text(string)を取得
    ta.getString(R.styleable.TestViewSet_android_text);
   
    //android:textSize(dimension)を取得
    ta.getDimension(R.styleable.TestViewSet_android_textSize, 15f);
   
    //textVaelue1(string)を取得
    ta.getString(R.styleable.TestViewSet_textVaelue1);

    //textColor(color)を取得
    ta.getColor(R.styleable.TestViewSet_textColor, Color.BLUE);
  }

2010年7月31日土曜日

XperiaでGmailアプリケーションを起動するとYouTubeが開く

再現手順は以下の通り。
  1. GmailやEMailの本文に記載されたURLから、YouTubeを起動する。
  2. Homeボタンでデスクトップに戻る
  3. Gmailを起動する
このとき、1. で中断したYouTubeが起動される。YouTube画面で「戻る」ボタンを押すと、Gmailの画面に戻る。

Androidアプリケーションはブラウザのタブで、インテントは同一タブで別アプリケーションを起動するイメージってことかな。最初は本気でバグったのかと思ったけど。

それにしても、Gmailボタンを押してYouTubeが起動されるのはおかしくない? Gmailボタンを押してるんだから、メールが読みたいに決まってる。そこでYouTube画面が出てきたら、バグったと思われても仕方ないような気がする。

仮に上記動作を良しとしても、YouTubeからGmailに戻る方法が、「戻る」ボタン連打か端末再起動しか無いのは不便すぎる。例えば、1. でYouTube起動後、複数の動画を巡回した場合、「戻る」ボタンを連打して最初の動画まで戻らないと、Gmailに戻れない。自分は「戻る」ボタン1回でGmailに戻れたから気づけたけど、動画巡回してたら今でもバグだと思ってたかもしれない。

現状のままだと、直観的じゃ無さ過ぎると思うので、改善案を3つほど考えてみた。どれか実装してくれないかなー。
  • Home画面からのアプリケーション起動は、アプリケーションごとに決められたエントリポイントから開始する。
  • インテントもHome画面からの起動と同様に、別プロセスとして起動する。
  • アプリケーションの任意終了機能を付ける。

テスト

テスト書き込み