Android 虚拟按键隐藏或显示之后共享元素动画异常解决方案

96
bauerbao
0.4 2019.06.05 15:26* 字数 1638

背景

本篇算是共享元素的第三篇文章。主要还是因为第一篇才会衍生出来了第二篇和第三篇文章,后两篇均属于bug的分析和解决。
1.Android 仿微信朋友圈图片拖拽返回
2.Android 共享元素动画分析及背景空白的解决方案
3.Android 虚拟按键隐藏或显示之后共享元素动画异常解决方案

在部分可以隐藏或者显示虚拟按键的手机上,只要显示或者隐藏虚拟按键,再执行共享元素,就会异常。如图:


SVID_20190605_144033_123.gif

按照惯例,发现了问题,看看别人是怎么解决的。既然是仿微信朋友圈的功能,因此看了下朋友圈的效果。如图


SVID_20190605_143621_123.gif

然后之前郭大佬开源了giffun,也看了下对应的效果。如图


SVID_20190605_143908_123.gif

备注:测试手机是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有没有合适的属性。

屏幕快照 2019-06-05 上午11.53.34.png

所以,最终的解决方案就是,给页面A设置如下属性即可。而且只要虚拟按键显示或者隐藏之后再进行页面跳转,页面A都会重建,和共享元素没有太大关联,除非以后虚拟按键彻底退出历史舞台。所以为了防止重建,建议每个activity都添加此属性。

android:configChanges="screenLayout"

效果图:


SVID_20190605_144212_123.gif

总结

虚拟按键的隐藏或者显示,导致在页面跳转的时候,该页面进行了重建的过程,导致共享元素的关键变量mSharedElementBundle被重置为null,从而导致页面A没法发送MSG_TAKE_SHARED_ELEMENTS给页面B,从而也就导致了共享元素动画被中断的问题。

现在再回顾微信和giffun,黑屏的时候,页面A都重建了(微信的,仔细看左上角的loading。giffun,仔细看标题和右下角FAB按钮),所以和分析的相吻合。

在开发中,大家会遇到各种各样的问题,合理利用网络搜索可以解决大部分的问题。当搜不到的时候,不妨试试自己去找解决办法,从源码角度分析,一方面可以提高自己,另一方面说不定还真把问题解决了呢。

日记本