ちょっと複雑なUPボタンの実装

  • 13
    Like
  • 0
    Comment

BACKとUPの違い

バックキーが他のアプリも含めて一つ前の画面に戻るのに対して、UPボタンは同じアプリ内の上の階層に戻る

バックキーとUPボタンの違いはわかったが、いざ作ってみると結構めんどくさいという話。

例:ECアプリ

例として以下の様な遷移のECアプリを考えてみる。

Untitled Diagram.png

製品詳細画面に至るには4つの導線がある。

  • 1. タイムラインから直接来る
  • 2. タイムラインから検索を経由して来る
  • 3. 製品詳細画面から、別の製品詳細画面に来る
  • 4. 他のアプリ(例えばGoogle検索結果)から、ディープリンクで来る

この製品詳細画面のUPボタンはどう振る舞うべきだろうか。それぞれの導線の場合について、以下のように振る舞うべきだと考える。

  • 1の場合
    • タイムラインに戻る
    • 以前表示していたタイムラインの状態(スクロール位置など)を維持する
  • 2の場合
    • 検索画面に戻る
    • 以前表示していた検索画面の状態(スクロール位置など)を維持する
  • 3の場合
    • 最初の詳細画面がタイムラインから来てたらタイムラインに、検索から来てたら検索に戻る
    • 以前表示していたタイムライン or 検索画面の状態(スクロール位置など)を維持する
  • 4の場合
    • タイムラインに戻る
    • バックスタックにはないので、画面は新規生成される

実装してみよう

NG: finishだけ呼ぶ

要はUPがバックボタンと同じ挙動をする実装。1や2の導線しかない場合はこれで上手くいく。大概のアプリや画面はこれで上手くいくような気もする。しかし、3や4があるとそうはいかない。

3の導線の場合、スタックに製品詳細画面が複数積まれるので、この実装だと一つ前の製品詳細画面に戻るだけで、上の階層には戻れない。

4の導線の場合、前のアプリに戻ってしまい、同じアプリの上の階層には行けない。

経路 結果 コメント
1
2
3 詳細画面に戻ってしまう
4 前のアプリに戻ってしまう

NG: 親ActivityへのIntent作成

なるほど、では親のActivityを直接指定してIntentを発行したらどうだろうか。

AndroidManifest.xmlで親Activityを指定する。

AndroidManifest.xml
<activity
    android:name=".ProductActivity"
    android:label="Product"
    android:parentActivityName=".TimelineActivity">
    <meta-data
        android:name="android.support.PARENT_ACTIVITY"
        android:value=".TimelineActivity" />
</activity>

NavUtils.getParentActivityIntentで親ActivityへのIntentを作成する。

ProductActivity.java
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case android.R.id.home:
            Intent upIntent = NavUtils.getParentActivityIntent(this);
            NavUtils.navigateUpTo(this, upIntent);
            return true;
    }
    return false;
}

しかし、これだと戻り先が常にタイムラインに固定されてしまい、2の場合に期待しない挙動になる。さらに、遷移先のタイムラインが再生成されてしまうので、ユーザーからすると違和感が強い。

経路 結果 コメント
1 画面が再生成されてしまう
2 タイムラインに戻ってしまう
3 画面が再生成されてしまう
4

NG: 親ActivityへのIntent作成 + SINGLE_TOP

戻り先を再生成しないためには、IntentにSINGLE_TOPのフラグを追加すればよい。こうすればスタックに戻り先の画面がある場合は再生成されない。スタックに画面がない場合は、新規に生成される。

ProductActivity.java
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case android.R.id.home:
            Intent upIntent = NavUtils.getParentActivityIntent(this);
            // CLEAR_TOPはnavigateUpToが勝手につけるが念のため
            upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
            NavUtils.navigateUpTo(this, upIntent);
            return true;
    }
    return false;
}

もしくは、ActivityのlaunchModeをsingleTopにしてもよい。

AndroidManifest.xml
<activity
    android:name=".TimelineActivity"
    android:launchMode="singleTop">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
経路 結果 コメント
1
2 タイムラインに戻ってしまう
3
4

OK: 戻り先のActivityへのIntent作成 + SINGLE_TOP

最後に、2のパターンをなんとかしよう。AndroidManifest.xmlに書いた親Activityを使うと必ずタイムラインに戻ってしまうので、NavUtils.getParentActivityIntentを使わずにIntentを作成する。

詳細画面に来るときに、戻り先を指定させる。

ProductActivity.java
public static void startActivity(Activity activity, int parent) {
    Intent intent = new Intent(activity, ProductActivity.class);
    intent.putExtra("parent", parent);
    activity.startActivity(intent);
}

// 呼び出し方
// startActivity(SearchActivity.this, ProductActivity.PARENT_SEARCH);
ProductActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_product);

    // 親アクティビティの取得。デフォルトはタイムライン画面。
    this.parent = getIntent().getIntExtra("parent", PARENT_TIMELINE);

}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case android.R.id.home:
            Intent upIntent;
            if (this.parent = PARENT_SEARCH) {
                upIntent = new Intent(this, SearchActivity.class);
            } else {
                upIntent = new Intent(this, TimelineActivity.class);
            }
            upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
            NavUtils.navigateUpTo(this, upIntent);
            return true;
    }
    return false;
}

こうすれば、元来たActivityに戻れる。

経路 結果 コメント
1
2
3
4

結論

  • UPの戻り先が複数ある場合は、NavUtils.getParentActivityIntentを使わない。自分でIntentを作る。
  • 戻り先を再生成したくないなら、IntentにSINGLE_TOPフラグをつける

おまけ

developer.android.comに、

If your activity provides any intent filters that allow other apps to start the activity, you should implement the onOptionsItemSelected() callback such that if the user presses the Up button after entering your activity from another app's task, your app starts a new task with the appropriate back stack before navigating up.
You can do so by first calling shouldUpRecreateTask() to check whether the current activity instance exists in a different app's task. If it returns true, then build a new task with TaskStackBuilder. Otherwise, you can use the navigateUpFromSameTask() method as shown above.

とあったが、google検索結果からのディープリンクや暗黙的インテントではshouldUpRecreateTask()がtrueにならなかった。タスクを再生成しないといけないケースというのがどういうときなのかよく分からない。明示的インテントで起動されたときなのだろうか。。。