页面拉起动画卡顿问题优化
背景
发现页面拉起动画不连贯,拉起过程中会发生跳变,在体验其他产品类似页面的时候并没有发现类似的现象,由此开始研究是什么导致的页面拉起动画的卡顿以及如何提升页面拉起动画的流畅程度。
排查过程
-
当前页面动画的拉起实现是BottomSheetBehavior 内部通过ViewDragHelper 做的过渡动画
-
当页面拉起动画执行的过程中,同时在渲染页面内容,主线程任务在一帧增多,到导致一帧时间过长,从而导致拉起动画出现跳动
动画跳动代码分析
动画循环执行
private class SettleRunnable implements Runnable { private final View view; private final int targetState; SettleRunnable(View view, int targetState) { this.view = view; this.targetState = targetState; } @Override public void run() { if (viewDragHelper != null && viewDragHelper.continueSettling(true)) { // 动画尚未完成,继续下一帧,如果某一帧因为任务太多,那么下次执行到这里时间已经差很多了 ViewCompat.postOnAnimation(view, this); } else { // 动画完成,设置最终状态 setStateInternal(targetState); } } }ViewDragHelper中的view高度处理逻辑
public boolean continueSettling(boolean deferCallbacks) { if (dragState == STATE_SETTLING) { // ⚠️ 关键问题在这里:scroller基于时间计算位置 boolean keepGoing = scroller.computeScrollOffset(); int x = scroller.getCurrX(); int y = scroller.getCurrY(); // 这个Y值会突然跳跃! // 更新View位置 - 导致视觉跳变 int dy = y - capturedView.getTop(); if (dy != 0) { ViewCompat.offsetTopAndBottom(capturedView, dy); // 触发onSlide回调,slideOffset也会跳跃 dispatchOnSlide(y); } return keepGoing; } return false; }移动距离计算源码
public boolean computeScrollOffset() { if (isFinished()) { return false; } switch (mMode) { case SCROLL_MODE: //基于时间做的计算处理 long time = AnimationUtils.currentAnimationTimeMillis(); // Any scroller can be used for time, since they were started // together in scroll mode. We use X here. final long elapsedTime = time - mScrollerX.mStartTime; final int duration = mScrollerX.mDuration; if (elapsedTime < duration) { final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration); mScrollerX.updateScroll(q); mScrollerY.updateScroll(q); } else { abortAnimation(); } break; }
问题分析
- 如果要打散启动阶段,主线程的任务,分散到不同runnable中,可以环节这个动画卡顿的问题,但是会导致用户看到页面的内容变的很慢,显然是不符合当下让用户尽早看到页面内容的目标的。
- 通过实测不同的页面发现,activity 拉起的时候做的页面进入动画,即使当时主线程有任务在执行,也不会有卡顿的现象
下面解释一下为什么activity的进场动画不会有卡顿的问题
startActivity 整体的调用链
// frameworks/base/services/core/java/com/android/server/wm/AppTransition.java
boolean prepareAppTransition(@TransitionType int transit, @TransitionFlags int flags) {
if (mDisplayContent.mTransitionController.isShellTransitionsEnabled()) {
return false;
}
//将动画请求放在请求队列中,防止多页面拉起动画冲图
mNextAppTransitionRequests.add(transit);
mNextAppTransitionFlags |= flags;
updateBooster();
removeAppTransitionTimeoutCallbacks();
mHandler.postDelayed(mHandleAppTransitionTimeoutRunnable,
APP_TRANSITION_TIMEOUT_MS);
return prepare();
}
同时 在这个对象中会进行加载动画文件的处理
@Nullable
Animation loadAnimationAttr(LayoutParams lp, int animAttr, int transit) {
return mTransitionAnimation.loadAnimationAttr(lp, animAttr, transit);
}
开启activity进场动画的代码
// frameworks/base/core/java/android/app/EnterTransitionCoordinator.java
private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition,
boolean startSharedElementTransition) {
......
Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
if (transition != null) {
transition.addListener(new ContinueTransitionListener());
if (startEnterTransition) {
setTransitioningViewsVisiblity(View.INVISIBLE, false);
}
//启动activity的进场动画
TransitionManager.beginDelayedTransition(decorView, transition);
if (startEnterTransition) {
setTransitioningViewsVisiblity(View.VISIBLE, false);
}
decorView.invalidate();
} else {
transitionStarted();
}
return transition;
}
public static void beginDelayedTransition(final ViewGroup sceneRoot, Transition transition) {
if (!sPendingTransitions.contains(sceneRoot) && sceneRoot.isLaidOut()) {
//开启动画真正逻辑
sceneChangeRunTransition(sceneRoot, transitionClone);
}
}
private static void sceneChangeRunTransition(final ViewGroup sceneRoot,
final Transition transition) {
if (transition != null && sceneRoot != null) {
MultiListener listener = new MultiListener(transition, sceneRoot);
//添加到ViewTreeObserver的绘制监听
sceneRoot.addOnAttachStateChangeListener(listener);
sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener);
}
}
private static class MultiListener implements ViewTreeObserver.OnPreDrawListener,
View.OnAttachStateChangeListener {
@Override
public boolean onPreDraw() {
removeListeners();
//...省略无关部分
//开启动画
mTransition.playTransition(mSceneRoot);
return true;
}
}
动画事物具体提交部分
// frameworks/base/core/java/android/view/SurfaceControl.java
public static class Transaction implements Closeable, Parcelable {
public void apply(boolean sync) {
applyResizedSurfaces();
notifyReparentedSurfaces();
//将事务发送到SurfaceFlinger进程
nativeApplyTransaction(mNativeObject, sync);
}
}
surfaceflinger 进程是独立的渲染进程
总结
由整个动画执行的代码中我们可以看出,每个Activity有独立的Surface,Surface由系统进程管理,在系统进程中设置和渲染的,Activity拉起动画动画是脱离于app的进程的,所以应用主线程的阻塞不会影响到系统级别的动画渲染。
解决方案:我们将原有的由bottomsheet做的弹起动画干掉,让bottomsheet直接定位到起始高度,当用户拉起页面的时候,直接使用activity 设置类似的进场动画,解决动画卡顿的问题。
那么你可能会问了,我的页面不是activity 我的页面是fragment 怎么办?
可曾听闻DialogFragment,你可以给他的window设置动画效果,原理也类似于activity的进场动画,独立于App进程的
@Override
public void onActivityCreated(Bundle savedInstanceState) {
Dialog dialog = getDialog();
super.onActivityCreated(savedInstanceState);
Window window = dialog == null ? null : dialog.getWindow();
if (window != null) {
window.setWindowAnimations(R.style.XXXX);
}
}
[!NOTE]
如果有内容错误,大佬们多多指正,感谢!