android共享元素动画在Android10以上异常的终极解决方案,共享元素动画调用finishAfterTransition动画失效的问题

不想看原理只想看结论的可以直接拉到最后,下边有你们想要的答案😄

一、首先来回顾一下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);
        }
    }
}

看名字也挺熟悉readStateonSaveInstanceState好像能关联起来,查看调用关系,在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

欢迎star~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值