2012/07/04

Android:UPナビゲーションをカスタマイズする

前回の投稿ではJellyBeanでのUPナビゲーションについて触れました。

今回は前回に残課題とした
「UPナビゲーションでparentActivityを起動するIntentに介入する」
の調査結果となります。

後述しますが、UPナビゲーションについてのより深い考察は下記サイトを参照。
BackとUPナビゲーションデザイン
BackとUPナビゲーションの提供
UPナビゲーションについて
タスクとバックスタック


●動機

AndroidManifest.xmlに定義されたActivity要素のparentActivityName属性で提供される
UPナビゲーション。
しかし、デフォルトの挙動ではアプリへの要求を満たせない場合があります。
例えば、単に親Activityを起動するのではなく、起動Intentにextra値を持たせて親Activity
の状態に関与したい場合などです。


●拡張されたAPI

JellyBean以降、Activityクラスが拡張されてUPナビゲーションに介入するためのメソッド
が追加されました。
主要なメソッドを下記に取り上げます。


public boolean onNavigateUp ()
このメソッドはユーザがアクションバーのUPナビゲーションを選択する度に呼ばれます。

もし、AndroidManifest.xmlのparentActivityNameで親Activitを指定していれば標準の
UPナビゲーションが自動で実行されます。
もし"親Activityチェーン"に沿う任意のActivityが、Intentにexstra値を必要とするなら
ば、onPrepareNavigateUpTaskStack(TaskStackBuilder)メソッドをオーバーライドする必
要があります。

UPナビゲーションのカスタマイズにあたっては、下記を参照しておきましょう。
BackとUPナビゲーションデザイン
タスクとバックスタック
TaskStackBuilderクラス
・Activity.getParentActivityIntent()
・Activity.shouldUpRecreateTask(Intent)
・Activity.navigateUpTo(Intent)
・SDKのAppNavigationサンプルアプリ

Return:
trueの場合、ナビゲーションは正常に完了し、自Activityはfinishされます。

---

public void onPrepareNavigateUpTaskStack (TaskStackBuilder builder)
異なるタスクからのUPナビゲーションで生成される合成タスクスタックを準備します。

このメソッドはonCreateNavigateUpTaskStackによって構築されたIntentを持つ
TaskStackBuilderを受け取ります。
もし、新たなタスクを起動する前にIntentにextra値を追加したい場合は、このメソッド
をオーバーライドして、ここでデータを追加する必要があります。

Param:
builder
onCreateNavigateUpTaskStackによってIntent構築済みのTaskStackBuilder。

---

public void onCreateNavigateUpTaskStack (TaskStackBuilder builder)
UPナビゲーションにより、異なるタスクが生成される場合に呼ばれます。
# つまりはshouldUpRecreateTask==trueの状態。

このメソッドのデフォルト実装は、AndroidManifest.xmlでparentActivityNameに指定し
た親ActivityをTaskStackBuilderの親Activityチェーンに追加します。

このメソッドは、getParentActivityIntent()で取得したIntentが
shouldUpRecreateTask(intent)でfalseを返すものである場合、onNavigateUp()から呼ば
れるのが標準の動作です。

もし、通常とは異なる方法でタスクスタックを生成した場合はこのメソッドをオーバーラ
イドできます。

Param:
builder
空のTaskStackBuilderです。
これに目的のタスクスタックを表現するIntentを追加します。

---

public Intent getParentActivityIntent ()
このActivityがAndroidManifest.xmlでparentActivityNameに指定した親Activityを起動
するためのIntentを取得します。

親Activityを起動するIntentを変更するにはこのメソッドをオーバーライドします。
super.getParentActivityIntent()を呼べば、純粋なUPナビゲーション用のIntentが作成
されます。

Return:
このActivityの親として定義されたActivityを起動するためのIntent。
有効な親がいない場合はnullを返します。

---

public boolean shouldUpRecreateTask (Intent targetIntent)
引数のtargetIntentを使用したUPナビゲーションを実現する時に、タスクを再生成すべき
かどうかを判定します。
false(タスクを生成すべきでない場合)は、同引数をとるnavigateUpTo(Intent)を呼び出
すことで正しくナビゲーションされます。

Params:
targetIntent
UPナビゲーションでの送信先(親Activity)をターゲットにしたIntent。

Return:
trueである場合、UPナビゲーションは新しいタスクスタックを生成すべき。
falseである場合、Intentは同じタスクに対して送信されるべき。

---

public boolean navigateUpTo (Intent upIntent)
このActivityから、引数upIntentで特定されるActivity(親)にナビゲートする時に呼ばれ
ます。
この過程で自Activityはfinishされます。

もしupIntentで指定されたActivityがヒストリスタックに存在する場合、自Activityと、
それよりも上のActivityが破棄されます(Like FLAG_ACTIVITY_CLEAR_TOP)

もしupIntentで指定されたActivityがヒストリスタックに存在しない場合、自タスクの
ルートActivityに到達するまでにある各Activityを破棄しルートActivityを起動します。
(これはアプリのホームActivityに戻るような動作です)
これは"Activityが正規の親Activityを通過しないで到達可能"といった、複雑なナビゲー
ション階層をもつアプリケーションで有効な方法です。

このメソッドは同じタスクを宛先とするUPナビゲーションの実行で呼ばれます。
もしタスクを跨ぐUPナビゲーションが必要な場合はshouldUpRecreateTask(Intent)を参照。

Param:
upIntent
UPナビゲーションでの送信先(親Activity)をターゲットにしたIntent。

Return:
trueの場合、ナビゲーションは正常にupIntentで指定されたActivityまで到達可能であり、
それが配信されたことを示します。
falseの場合、upIntentで指定されたActivityを見つけることができなかったことを示します。
この時、自Activityは単純にfinishするのみです。

---

●JellyBeanより古い環境で

ICS以前で同等の処理を実装したい場合は互換性ライブラリを使用する必要があります。

TaskStackBuilder(v4版)
NavUtil

下記サンプルです。
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case android.R.id.home:
            Intent upIntent = new Intent(this, ParentActivity.class);
            if (NavUtils.shouldUpRecreateTask(this, upIntent)) {
                // 下記のような状態のバックスタックを生成
                // | ParentActivity            | (peek)
                // |---------------------------|
                // | GrandParentActivity       |
                // |---------------------------|
                // | GreateGrandParentActivity |
                // |---------------------------|
                TaskStackBuilder.from(this)
                        .addNextIntent(new Intent(this, GreatGrandParentActivity.class))
                        .addNextIntent(new Intent(this, GrandParentActivity.class))
                        .addNextIntent(upIntent)
                        .startActivities();
                finish();
            } else {
                // ParentActivityは自タスク上に生成される。
                // シンプルなUPナビゲーション
                NavUtils.navigateUpTo(this, upIntent);
            }
            return true;
    }
    return super.onOptionsItemSelected(item);
}


●UPナビゲーションのデフォルトの挙動

UPナビゲーションのひょう動作を解析。

・同じアプリケーションタスクへのUPナビゲーション
メソッドコール
onNavigateUp
  |-- getParentActivityIntent
  |    親ActivityのComponentNameを持つIntentを生成
  |
  |-- shouldUpRecreateTask
  |    同アプリタスクへのUPナビゲーションのため戻り値はfalse
  |
  |-- navigateUpTo
       ヒストリスタックに親Activityがいる場合true。そうでない場合false


・異なるアプリケーションタスクへのUPナビゲーション
メソッドコール
onNavigateUp
  |-- getParentActivityIntent
  |    親ActivityのComponentNameを持つIntentを生成
  |
  |-- shouldUpRecreateTask
  |    異なるアプリケーションタスクへのUPナビゲーションのため戻り値はtrue
  |
  |-- onCreateNavigateUpTaskStack
  |   | ここでTaskStackBuilderが構築されていく
  |   |
  |   |-  getParentActivityIntent
  |          親ActivityのComponentNameを持つIntentを生成
  |
  |-- onPrepareNavigateUpTaskStack
       構築済みTaskStackBuilderが渡され、この後タスク生成とActivity起動を開始


・存在しない不正な親Activity名を指定した場合
onNavigateUp
  |-- getParentActivityIntent
  |    親ActivityのComponentNameを持つIntentを生成
  |
  |-- shouldUpRecreateTask
  |    存在しないActivityの場合、タスクを生成すべきでないため戻り値はfalse
  |
  |-- navigateUpTo
       メソッドの結果はfalseとなり、ルートActivityまで遡って起動

AndroidManifest.xmlでparentActivityNameに存在しない不正な親Activityを指定しても
ActivityNotFoundExceptionは発生しないことがわかる。

以上です。