不想看原理只想看结论的可以直接拉到最后,下边有你们想要的答案😄
一、首先来回顾一下Android共享元素动画使用方法
第1步:在Activity1传入要共享的元素View和其Name
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, shareElement, shareElementName).toBundle();
context.startActivity(intent, bundle);
第2步:在Activity2设置对应的共享元素
ViewCompat.setTransitionName(shareElement, shareElementName);
第3步:返回上一页面要想也有动画返回效果,只需调用:
finishAfterTransition();
经过以上两步已经可以看到共享元素动画已经完美地显示了,但是测试小伙伴讲他的手机上返回时有时候没有动画😱😱😱,不可能!!!
经过测试,出现这种这种情况一般是屏幕息屏后解锁,或者按下Home键返回来,或者打开了其他页面,又或者其他app显示在了我的应用上边,调用finishAfterTransition();它~它~它~就没有动画了,吓得我磕巴了,不好意思😅
二、问题追溯
测试反馈这块出问题的只是Android 10的手机,经过测试Android 11也有问题,估计是从Android 10开始就有这种问题了,那么看一下源码
既然是从Android 10开始出问题的,那么我们就来看这Android 9和Android 10的代码对比,首先来看下调用finishAfterTransition为什么会没有动画
public void finishAfterTransition() {
if (!mActivityTransitionState.startExitBackTransition(this)) {
finish();
}
}
这块代码Android 9和10 都是一样的,很简单,这里有一个属于ActivityTransitionState类的判断startExitBackTransition返回false就直接调用了finish,我们前边已经说过了返回时要想有动画效果需要调用finishAfterTransition,这里经过一个判断返回false就直接调用finish,基本可以确定是因为返回了false,debug看一下也确实如此,到此基本上知道了原因了,问题是为啥返回了false呢
来吧,继续看下startExitBackTransition方法的源码,这里就出现区别了,贴一下两者对比的源码,相同的代码我直接用…省略了(下边也一样)
Android 9
public boolean startExitBackTransition(final Activity activity) {
if (mEnteringNames == null || mCalledExitCoordinator != null) {
return false;
} else {
...
return true;
}
}
Android 10
public boolean startExitBackTransition(final Activity activity) {
ArrayList<String> pendingExitNames = getPendingExitNames();
if (pendingExitNames == null || mCalledExitCoordinator != null) {
return false;
} else {
...
return true;
}
}
这里返回false是因为判断条件跟以前不一样了,而且Android 10还多调用了getPendingExitNames()这一个方法,判断条件对比一下很明显就是因为这个方法获取到了null。来看一下getPendingExitNames方法是怎么写的
private ArrayList<String> getPendingExitNames() {
if (mPendingExitNames == null && mEnterTransitionCoordinator != null) {
mPendingExitNames = mEnterTransitionCoordinator.getPendingExitSharedElementNames();
}
return mPendingExitNames;
}
通过debug发现返回时mPendingExitNames和mEnterTransitionCoordinator都已经是null了,所以startExitBackTransition自然而然地就返回了false,那么这两个值为啥成null了呢,重点是不是在于mEnterTransitionCoordinator它为null了呢,因为他不是null的话还可以给mPendingExitNames赋值。
接下来看一下mEnterTransitionCoordinator什么时候变成null的。在ActivityTransitionState类中只有这3个地方置null了
public void onStop() {
restoreExitedViews();
if (mEnterTransitionCoordinator != null) {
mEnterTransitionCoordinator.stop();
mEnterTransitionCoordinator = null;
}
if (mReturnExitCoordinator != null) {
mReturnExitCoordinator.stop();
mReturnExitCoordinator = null;
}
}
public void onResume(Activity activity) {
// After orientation change, the onResume can come in before the top Activity has
// left, so if the Activity is not top, wait a second for the top Activity to exit.
if (mEnterTransitionCoordinator == null || activity.isTopOfTask()) {
restoreExitedViews();
restoreReenteringViews();
} else {
activity.mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (mEnterTransitionCoordinator == null ||
mEnterTransitionCoordinator.isWaitingForRemoteExit()) {
restoreExitedViews();
restoreReenteringViews();
} else if (mEnterTransitionCoordinator.isReturning()) {
mEnterTransitionCoordinator.runAfterTransitionsComplete(() -> {
mEnterTransitionCoordinator = null;
});
}
}
}, 1000);
}
}
public void clear() {
mPendingExitNames = null;
mExitingFrom = null;
mExitingTo = null;
mExitingToView = null;
mCalledExitCoordinator = null;
mEnterTransitionCoordinator = null;
mEnterActivityOptions = null;
mExitTransitionCoordinators = null;
}
通过debug大法,看到息屏时走了onStop,并且走到了置null的那一句,到此貌似就找到源头了,那按理说Google开发时应该会有重新恢复赋值的操作才对,不然也太傻X了,通过查找源码,找到了赋值的地方,仅此一处
public void enterReady(Activity activity) {
if (mEnterActivityOptions == null || mIsEnterTriggered) {
return;
}
mIsEnterTriggered = true;
mHasExited = false;
ArrayList<String> sharedElementNames = mEnterActivityOptions.getSharedElementNames();
ResultReceiver resultReceiver = mEnterActivityOptions.getResultReceiver();
final boolean isReturning = mEnterActivityOptions.isReturning();
if (isReturning) {
restoreExitedViews();
activity.getWindow().getDecorView().setVisibility(View.VISIBLE);
}
mEnterTransitionCoordinator = new EnterTransitionCoordinator(activity,
resultReceiver, sharedElementNames, mEnterActivityOptions.isReturning(),
mEnterActivityOptions.isCrossTask());
if (mEnterActivityOptions.isCrossTask()) {
mExitingFrom = new ArrayList<>(mEnterActivityOptions.getSharedElementNames());
mExitingTo = new ArrayList<>(mEnterActivityOptions.getSharedElementNames());
}
if (!mIsEnterPostponed) {
startEnter();//这里被调用了
}
}
通过debug发现,这个方法只是在页面走到performStart这个方法时有调用,但是息屏再开屏时,Activity重新走onStart时mEnterActivityOptions也为null了所以走不到下边了,而mEnterActivityOptions也在页面第一次进来时也被置null了,注意一下mPendingExitNames在这也被置null了,如下:
private void startEnter() {
if (mEnterTransitionCoordinator.isReturning()) {
...
} else {
...
mPendingExitNames = null;
}
mEnterActivityOptions = null;
}
到此似乎陷入了僵局…😭,回过头来再看一下前边的
private ArrayList<String> getPendingExitNames() {
if (mPendingExitNames == null && mEnterTransitionCoordinator != null) {
mPendingExitNames = mEnterTransitionCoordinator.getPendingExitSharedElementNames();
}
return mPendingExitNames;
}
这里mPendingExitNames的值如果不是null不也可以吗,不非得通过mEnterTransitionCoordinator来重新赋值,那从哪里可以给mPendingExitNames重新赋值呢?在ActivityTransitionState中搜了一下除了上边那个方法还有这么一处重新赋值的
public void readState(Bundle bundle) {
if (bundle != null) {
if (mEnterTransitionCoordinator == null || mEnterTransitionCoordinator.isReturning()) {
mPendingExitNames = bundle.getStringArrayList(PENDING_EXIT_SHARED_ELEMENTS);
}
if (mEnterTransitionCoordinator == null) {
mExitingFrom = bundle.getStringArrayList(EXITING_MAPPED_FROM);
mExitingTo = bundle.getStringArrayList(EXITING_MAPPED_TO);
}
}
}
看名字也挺熟悉readState和onSaveInstanceState好像能关联起来,查看调用关系,在Activity中只有这里
final void performCreate(Bundle icicle, PersistableBundle persistentState) {
...
mActivityTransitionState.readState(icicle);
...
}
有读readState就应该有存,代码如下
public void saveState(Bundle bundle) {
ArrayList<String> pendingExitNames = getPendingExitNames();
if (pendingExitNames != null) {
bundle.putStringArrayList(PENDING_EXIT_SHARED_ELEMENTS, pendingExitNames);
}
if (mExitingFrom != null) {
bundle.putStringArrayList(EXITING_MAPPED_FROM, mExitingFrom);
bundle.putStringArrayList(EXITING_MAPPED_TO, mExitingTo);
}
}
通过调用关系看到在Activity中这里进行了保存
/**
* The hook for ActivityThread to save the state of this activity. Calls onSaveInstanceState(Bundle) and saveManagedDialogs(Bundle).
* Params:
* outState – The bundle to save the state to.
*/
final void performSaveInstanceState(@NonNull Bundle outState) {
...
mActivityTransitionState.saveState(outState);
...
}
真理、真相近在咫尺了,真是激动啊😄,上边的方法我把注释也一并贴上了,意思就是
ActivityThread 用于保存此活动状态的钩子调用的它,通过查找ActivityThread源码我发现有这么一句(我是找的SaveInstanceState作为关键字)
private void callActivityOnSaveInstanceState(ActivityClientRecord r) {
r.state = new Bundle();
r.state.setAllowFds(false);
if (r.isPersistable()) {
r.persistentState = new PersistableBundle();
mInstrumentation.callActivityOnSaveInstanceState(r.activity, r.state,
r.persistentState);
} else {
mInstrumentation.callActivityOnSaveInstanceState(r.activity, r.state);
}
}
点(callActivityOnSaveInstanceState)进去看一下
public void callActivityOnSaveInstanceState(@NonNull Activity activity,
@NonNull Bundle outState, @NonNull PersistableBundle outPersistentState) {
activity.performSaveInstanceState(outState, outPersistentState);
}
至此找到了调用的地方了,这个方法位于Instrumentation中,经过了解学习Instrumentation这个类就是Android里边的钩子也就是常说的hook,在performSaveInstanceState也提到了这一点,我们一般基本上用不到它,这里不展开说了。
至此其实就找到了原因和解决方法了,应该在onStop中利用Instrumentation来保存一下
@Override
protected void onStop() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !isFinishing()) {
new Instrumentation().callActivityOnSaveInstanceState(this, new Bundle());
}
super.onStop();
}
这里需要判断一下版本号 Android 10以上进行一次保存,并且不是在finishing中。
这里讲一下原理,其实Activity也会走performSaveInstanceState,但是顺序是在onStop之后,前边提到了onSaveInstanceState,可以通过debug 断点onSaveInstanceState发现它是performSaveInstanceState被调用的,但是performSaveInstanceState是在onStop之后才会执行,前边提到了
class ActivityTransitionState{
public void onStop() {
restoreExitedViews();
if (mEnterTransitionCoordinator != null) {
mEnterTransitionCoordinator.stop();
mEnterTransitionCoordinator = null;
}
if (mReturnExitCoordinator != null) {
mReturnExitCoordinator.stop();
mReturnExitCoordinator = null;
}
}
}
class Activity{
protected void onStop() {
...
mActivityTransitionState.onStop();
...
}
}
因为这个调用顺序,先走了Activity的onStop也就调用了ActivityTransitionState的onStop方法,从而导致mEnterTransitionCoordinator为null
final void performSaveInstanceState(@NonNull Bundle outState) {
...
mActivityTransitionState.saveState(outState);
...
}
public void saveState(Bundle bundle) {
ArrayList<String> pendingExitNames = getPendingExitNames();
if (pendingExitNames != null) {
bundle.putStringArrayList(PENDING_EXIT_SHARED_ELEMENTS, pendingExitNames);
}
if (mExitingFrom != null) {
bundle.putStringArrayList(EXITING_MAPPED_FROM, mExitingFrom);
bundle.putStringArrayList(EXITING_MAPPED_TO, mExitingTo);
}
}
private ArrayList<String> getPendingExitNames() {
if (mPendingExitNames == null && mEnterTransitionCoordinator != null) {
mPendingExitNames = mEnterTransitionCoordinator.getPendingExitSharedElementNames();
}
return mPendingExitNames;
}
当Activity执行到performSaveInstanceState中的 mActivityTransitionState.saveState(outState)时,这里又一次调用了getPendingExitNames();因为mEnterTransitionCoordinator已经是null,所以获取到的mPendingExitNames是null,这里也就没有保存,所以要在Activity的onStop之前先调用一下Activity的performSaveInstanceState,进而调用ActivityTransitionState的saveState,saveState中再一次调用了getPendingExitNames,这个时候mEnterTransitionCoordinator还不是null,mPendingExitNames这个成员变量就重新有值了,调用finishAfterTransition时就不会直接finish了
总结一下
Android 10以上,Activity先走了onStop方法导致mEnterTransitionCoordinator为null,然后才走了performSaveInstanceState,在这里虽然有调用ActivityTransitionState的saveState,但是因为mEnterTransitionCoordinator已经为null,数据拿不到了,所以没有保存成功。严重怀疑这是Google的Bug
我的解决方案就是在onStop 之前先保存数据,不吹牛逼,只需加上这一句即可(网上那些反射解决的方案简直弱爆了有没有~😄):
@Override
protected void onStop() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !isFinishing()) {
new Instrumentation().callActivityOnSaveInstanceState(this, new Bundle());
}
super.onStop();
}
另外需要注意一点,调用这句之后onSaveInstanceState会走两遍,第一遍是这句调用的,第二遍是ActivityThread调用的,如果你在这块有代码,需要注意一下,最简单的方法就是在这块传入的Bundle传入一个标记位,来进行识别,例如:
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (outState.containsKey("InstrumentationFixBug") && outState.getBoolean("InstrumentationFixBug")){
//new Instrumentation().callActivityOnSaveInstanceState(this, bundle);
}else {
//ActivityThread调用,用于保存逻辑
}
}
@Override
protected void onStop() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !isFinishing()) {
Bundle bundle = new Bundle();
bundle.putBoolean("InstrumentationFixBug",true);
new Instrumentation().callActivityOnSaveInstanceState(this, bundle);
}
super.onStop();
}
至此打完收工!!!如果帮到你点个赞吧👍~
最后推荐一个我写的查看大图浏览器(支持视频),如果你的项目中需要点击小图查看大图的功能,亦或聊天页面的查看视频和图片功能都可以使用它,地址如下:
https://github.com/FlyJingFish/OpenImage