言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

Activity Shared Element TransitionでActivityが多重起動されるとAnimationがおかしくなる

はじめに

Activity Shared Element TransitionでActivityが多重起動されるとAnimationがおかしくなる現象を見つけて面白かったので調べてみた。

具体的に

まずは下記のgifを見てほしい。リストアイテム押下時にわざと1秒のdelayを入れてstartActivity()している。 この時、Activity Shared Element Transitionするように bundlestartActivity()の第2引数に渡している。

f:id:muumuumuumuu:20190602161732g:plain

gifでやっていること手順をまとめると、

  1. 左上のアイテムをタップ、1秒のdelayが走る(A)
  2. 1のdelayが終わる前に右上のアイテムをタップ(B)
  3. Aのdelayが終わり、左上のタップイベントが発火、startActivity()が走る
  4. Bのdelayが終わり、右上のタップイベントが発火、startActivity()が走る
  5. Aのタップイベント契機のActivityが起動される
  6. Bのタップイベント契機のActivityが起動される
  7. Back keyをタップしてBのactivityがfinishされる
  8. Back keyをタップしてAのactivityがfinishされる

step 8のあと、A, B両方のshared itemはなぜかBのpositionに戻っていき、Aのitemがいなくなってしまうのである。

結論だけ言うと

一つのActivityに登録できるActivity Shared Element Transition用のbundleは一つだけで、しかも上書き。このようなケースを防ぎたかったらActivityの多重起動はできないようにするしかない。

何が起こったのか

何が起こったかを理解するためには、Activity Shared Element Transitionの仕組みを知らないといけない。そのためにFrameworkの内部実装を読んでいく。

このシーケンス図を見てもらえればだいたいわかるんだけど、 f:id:muumuumuumuu:20190602175004p:plain

まずActivity起動時にstartActivity()の第2引数として渡すbundle (ActivityOptions) は色々あって最終的にActivityStarterActivityRecordを作るときに使われる。
別のactivityから戻って来たときのアニメーション用にこのoptionを取得する必要があるが、このときActivityRecordのstatic methodであるisInStackLocked()が使われる。

    static ActivityRecord isInStackLocked(IBinder token) {
        final ActivityRecord r = ActivityRecord.forTokenLocked(token);
        return (r != null) ? r.getStack().isInStackLocked(r) : null;
    }

このmethodはactivityのtokenからActivityRecordを返してくれる。そしてActivityRecord内に格納されているActivityOptionsを使ってActivityTransitionState#setEnterActivityOptions()がコールされる。

と言うわけでActivityOptionsとactivityのtokenは一対一で対応される。Activityが多重で起動されると起動された順番とfinishされる順番がおかしくなるとアニメーションもおかしくなる。この仕組みはアプリからは避けられないので、アプリとしてはActivityの多重起動を避けると言うアプローチがいいと思う。

おまけ

今回貼ったシーケンス図は下記のサイトで作りました。めっちゃ便利。 シーケンス図って最近エンジニアになった人には(特にweb系)馴染みのない古のツールかもしれないけど、私は好きです。

sequencediagram.org

title How to handle enter transition
note over User, Activity: Search Result Screen
User->User:click item
User->Activity:startActivityForResult()
Activity->Instrumentation:execStartActivity()
Instrumentation->ActivityManagerService:startActivity()
ActivityManagerService->ActivityManagerService:startActivityMayWait()
ActivityManagerService->ActivityStarter:setActivityOptions()

note over ActivityStarter:mRequest.activityOptions = options;
ActivityManagerService->ActivityStarter:execute()
ActivityStarter->ActivityStarter:startActivityMayWait()
ActivityStarter->ActivityStarter:startActivity()

note over ActivityStarter: create ActivityRecord using activity option as a one of parameter


note over User,ActivityStarter: New Activity is displayed

User->User:click back key
User->Activity:performStart()
Activity->Activity:getActivityOptions()
Activity->ActivityManagerService:getActivityOptions(mToken)
note over Activity,ActivityManagerService: get acvitivy options from ActivityRecord from static method
Activity<--ActivityManagerService:options
Activity->ActivityTransitionState:setEnterActivityOptions()