背景
本篇算是共享元素的第三篇文章。主要还是因为第一篇才会衍生出来了第二篇和第三篇文章,后两篇均属于bug的分析和解决。
1.Android 仿微信朋友圈图片拖拽返回
2.Android 共享元素动画分析及背景空白的解决方案
3.Android 虚拟按键隐藏或显示之后共享元素动画异常解决方案
在部分可以隐藏或者显示虚拟按键的手机上,只要显示或者隐藏虚拟按键,再执行共享元素,就会异常。如图:
按照惯例,发现了问题,看看别人是怎么解决的。既然是仿微信朋友圈的功能,因此看了下朋友圈的效果。如图
然后之前郭大佬开源了giffun,也看了下对应的效果。如图
备注:测试手机是8.0华为。而这个问题,只发生在页面A进入页面B的时候,页面B返回页面A则正常。同时,在隐藏或者显示虚拟键盘之后,将手机休眠再唤醒,再进入页面B,则不会发生这个问题。
对比了微信和giffun的效果,基本都会黑屏一下。但是微信的可以成功进入下一页面。而giffun,在测试的时候发现,偶尔也可以进入下一页面。为什么不同app之间会有差异,很正常,大家的用的库不一样,环境不一样等等。但是,都黑屏了,这就是共同点。
在这里,我可以口出狂言,只要不做任何处理,大家都有这个问题,而且这是属于官方的bug。很开心,大家都解决不了,那我也肯定解决不了了。所以 ---- 完!!!感谢大家点进来阅读。这就是篇骗访问量的文章。
分析
本着专研的精神,决定看看到底是什么问题导致的。因为上一篇已经对共享元素进行了源码分析,所以此处跳过大部分的源码分析。
还是和上次一样,看下页面A进入页面B共享元素的回调监听吧。
A:
onMapSharedElements
onCaptureSharedElementSnapshot
B:
onMapSharedElements
再看下应该出现的回调监听
A:
onMapSharedElements
onCaptureSharedElementSnapshot
onSharedElementsArrived
B:
onMapSharedElements
onSharedElementsArrived
onRejectSharedElements
onCreateSnapshotView
onSharedElementStart
onSharedElementEnd
从出现的结果来看,可以证明我在上一篇最后提出的问题。即页面A和B的onSharedElementsArrived以及之后的监听,都是在页面A收到页面B发送的MSG_SET_REMOTE_RECEIVER消息之后,再发送MSG_TAKE_SHARED_ELEMENTS消息给页面B之后产生的。
而现在后面的监听都没有回调,这就说明,页面B给页面A消息之后,就出现了问题,导致页面A没有给页面B发送消息。所以,根据上一篇分析,直接将代码定位到A页面收到B页面消息的地方
//ExitTransitionCoordinator.class
case MSG_SET_REMOTE_RECEIVER:
stopCancel();
mResultReceiver = resultData.getParcelable(KEY_REMOTE_RECEIVER);
if (mIsCanceled) {
mResultReceiver.send(MSG_CANCEL, null);
mResultReceiver = null;
} else {
notifyComplete();
}
break;
调试发现mIsCanceled为true,所以notifyComplete出现了问题。继续跟踪。
//ExitTransitionCoordinator.class
protected void notifyComplete() {
if (isReadyToNotify()) {
if (!mSharedElementNotified) {
mSharedElementNotified = true;
delayCancel();
if (mListener == null) {
mResultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, mSharedElementBundle);
notifyExitComplete();
} else {
final ResultReceiver resultReceiver = mResultReceiver;
final Bundle sharedElementBundle = mSharedElementBundle;
mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements,
new OnSharedElementsReadyListener() {
@Override
public void onSharedElementsReady() {
resultReceiver.send(MSG_TAKE_SHARED_ELEMENTS,
sharedElementBundle);
notifyExitComplete();
}
});
}
} else {
notifyExitComplete();
}
}
}
所以关键代码就是isReadyToNotify()的判断了。
//ExitTransitionCoordinator.class
protected boolean isReadyToNotify() {
return mSharedElementBundle != null && mResultReceiver != null && mIsBackgroundReady;
}
调试发现mSharedElementBundle为null。所以根源已经找到,肯定有个地方将这个值重新设置为null了,导致共享元素的动画出现了异常。
//ExitTransitionCoordinator.class
@Override
protected void sharedElementTransitionComplete() {
mSharedElementBundle = mExitSharedElementBundle == null
? captureSharedElementState() : captureExitSharedElementsState();
super.sharedElementTransitionComplete();
}
@Override
protected void clearState() {
mHandler = null;
mSharedElementBundle = null;
****省略部分代码****
mExitSharedElementBundle = null;
super.clearState();
}
总共就两处赋值,第一处是正常的值,第二处为null,所以怀疑对象就是clearState这个方法了。经过长时间的调试,发现异常情况的时候,执行了activity.onStop()-->mActivityTransitionState.onStop()-->mActivityTransitionState.restoreExitedViews()-->mCalledExitCoordinator.resetViews()-->clearState()一系列操作。
发现了onStop(),没错是activity生命周期onStop(),怎么回事?页面跳转,执行一个onStop生命周期,不是再正常不过了么?怎么就出现问题了?一个虚拟按键的隐藏或者显示,一个在正常不过的onStop生命周期,怎么就导致了共享元素动画异常?这三者之间到底有何勾当!砸电脑ing
静静:你想我了吧[坏笑]
凡是出现问题,既要研究异常情况,也要对比正常情况。日志对比:
//正常情况
A:
onMapSharedElements
onCaptureSharedElementSnapshot
onSharedElementsArrived
B:
onMapSharedElements
onSharedElementsArrived
onRejectSharedElements
onCreateSnapshotView
onSharedElementStart
onSharedElementEnd
A:
onStop(因为项目中activity的背景是透明的,所以肯定不会出现onStop的情况,为了测试,改为了非透明)
//异常情况
A:
onMapSharedElements
onCaptureSharedElementSnapshot
onStop
B:
onMapSharedElements
也就是说,两者的差别是,onStop生命周期的调用时间不一致。
静静:你又想我了吧[坏笑]
说实话到了这里,我只能开启脑洞,往各个有可能的方向去尝试。结果发现,共享元素异常情况的时候,不仅仅onStop提前了,而且页面A的整个生命周期都重新调了一遍。那就好办了,等于就是页面A重新创建了。马上联想到手机旋转会导致页面重新创建,赶紧去manifest中找找android:configChanges有没有合适的属性。
所以,最终的解决方案就是,给页面A设置如下属性即可。而且只要虚拟按键显示或者隐藏之后再进行页面跳转,页面A都会重建,和共享元素没有太大关联,除非以后虚拟按键彻底退出历史舞台。所以为了防止重建,建议每个activity都添加此属性。
android:configChanges="screenLayout"
效果图:
总结
虚拟按键的隐藏或者显示,导致在页面跳转的时候,该页面进行了重建的过程,导致共享元素的关键变量mSharedElementBundle被重置为null,从而导致页面A没法发送MSG_TAKE_SHARED_ELEMENTS给页面B,从而也就导致了共享元素动画被中断的问题。
现在再回顾微信和giffun,黑屏的时候,页面A都重建了(微信的,仔细看左上角的loading。giffun,仔细看标题和右下角FAB按钮),所以和分析的相吻合。
在开发中,大家会遇到各种各样的问题,合理利用网络搜索可以解决大部分的问题。当搜不到的时候,不妨试试自己去找解决办法,从源码角度分析,一方面可以提高自己,另一方面说不定还真把问题解决了呢。