Activity Shared Element TransitionでActivityが多重起動されるとAnimationがおかしくなる
はじめに
Activity Shared Element TransitionでActivityが多重起動されるとAnimationがおかしくなる現象を見つけて面白かったので調べてみた。
具体的に
まずは下記のgifを見てほしい。リストアイテム押下時にわざと1秒のdelayを入れてstartActivity()
している。
この時、Activity Shared Element Transitionするように bundle
をstartActivity()
の第2引数に渡している。
gifでやっていること手順をまとめると、
- 左上のアイテムをタップ、1秒のdelayが走る(A)
- 1のdelayが終わる前に右上のアイテムをタップ(B)
- Aのdelayが終わり、左上のタップイベントが発火、
startActivity()
が走る - Bのdelayが終わり、右上のタップイベントが発火、
startActivity()
が走る - Aのタップイベント契機のActivityが起動される
- Bのタップイベント契機のActivityが起動される
- Back keyをタップしてBのactivityがfinishされる
- 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の内部実装を読んでいく。
このシーケンス図を見てもらえればだいたいわかるんだけど、
まずActivity起動時にstartActivity()の第2引数として渡すbundle (ActivityOptions
) は色々あって最終的にActivityStarter
がActivityRecord
を作るときに使われる。
別の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系)馴染みのない古のツールかもしれないけど、私は好きです。
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()