有一个问题是:什么时候能知道View绘制完成?
解决方法
解决方法非常简单,就是使用View#post方法,传入一个Runnable对象,以下是示例代码:
private fun ActivityTouchBinding.drawComplete() {
myView.post {
Log.e(TAG, "post")
}
}
运行结果:
可以看到post中的Runable确实是在onDraw之后运行的。
post源码分析
我们来看一下为什么View#post方法可以在View绘制完成后执行。先猜一手,由于用了post,估计是一个Handler。
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
首先判断mAttachInfo是否存在。由于我们是在Activity#onCreate中调用的View#post方法,而mAttachInfo是在ViewRootImpl的构造函数中赋值,而ViewRootImpl则是在Activity#onResume中实例化的。因此,当前会走下面的getRunQueue().post(action)
。
getRunQueue()
是获取一个HandlerActionQueue实例,内部有一个长度为4的Array<HandlerAction>,而HandlerAction是对post方法传进去的Runnable的封装。post(action)
就是封装Runnable成HandlerAction并加入Array<HandlerAction>
在HandlerActionQueue中还有一个executeActions(Handler handler)
方法用于将HandlerAction传递给参数handler执行,也就是将我们的post中的代码传给参数handler执行:
public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
// 将HandlerAction传递给参数handler执行
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
mActions = null;
mCount = 0;
}
}
而executeActions是在ViewRootImpl#performTraversals中被调用的:
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView; // DecorView
mIsInTraversal = true;
...
if (mFirst) {
...
// 给AttachInfo赋值
mAttachInfo.mWindowVisibility = viewVisibility;
// 此时控件树即将第一次被显示在 Window 上
// 调用 DecorView 的 dispatchAttachedToWindow() 方法传入 mAttachInfo 实例
// 为每个位于 DecorView 中的 View 传递 mAttachInfo 关联信息
// 同时调用 View # onAttachedToWindow() 来绑定到 Window
host.dispatchAttachedToWindow(mAttachInfo, 0);
...
}
...
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(mAttachInfo.mHandler);
...
// 执行View的测量
performMeasure(childWidthMeasureSpec, childHeightMeasure);
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
if (didLayout) {
// 执行View的布局
performLayout(lp, mWidth, mHeight);
...
}
...
// 执行完测量布局后,mFirst = false
mFirst = false;
boolean cancelAndRedraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw();
// 这里简化了一下代码,相当于伪代码
if (isViewVisible && !cancelAndRedraw) {
performDraw();
}
mIsInTraversal = false;
...
}
我们可以看到,getRunQueue().post(mAttachInfo.mHandler)
在绘制流程中被调用,而传进来的Handler就是mAttachInfo中的mHandler,也就是ViewRootHandler
实例:
// AttachInfo构造函数
AttachInfo(IWindowSession session, IWindow window, Display display,
ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
Context context) {
mSession = session;
mWindow = window;
mWindowToken = window.asBinder();
mDisplay = display;
mViewRootImpl = viewRootImpl;
mHandler = handler;
mRootCallbacks = effectPlayer;
mTreeObserver = new ViewTreeObserver(context);
}
// ViewRootImpl构造函数
public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
boolean useSfChoreographer) {
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);
}
// ViewRootImpl
final ViewRootHandler mHandler = new ViewRootHandler();
综上所述,View#post方法传进去的Runnable会在ViewRootImpl的绘制流程中被传递给ViewRootHandler,也就是把Runnable传递进UI线程的MessageQueue等待执行。此时,View的测量、布局和绘制在执行,也就是说我们的Runnable会在绘制之后被执行。
延申
通常使用以下3种方法,用于判断View是否完成绘制:
1. View#post方法
view.post {
// 此处的代码会在View完成绘制之后调用
}
2. View#getViewTreeObserver.addOnGlobalLayoutLinstener监听
view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
// 被执行代码
// 不需要时,移除监听器
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
myView.viewTreeObserver.removeOnGlobalLayoutListener(this)
} else {
myView.viewTreeObserver.removeGlobalOnLayoutListener(this)
}
}
})
3. View#getViewTreeObserver.addOnPreDrawLinstener监听
view.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
// 被执行代码
// 移除监听器,避免重复调用
myView.viewTreeObserver.removeOnPreDrawListener(this)
// 返回true,继续后续绘制流程;返回false,取消后续绘制流程
return true
}
})
每种方法都有它的适用场景,一般来说,如果你想要在View大小确定后做一些操作(比如获取宽高),使用addGlobalLayoutLinstener
会比较合适。如果你想在布局完成直接进行某些绘制相关的操作,可以考虑使用post
或addOnPreDrawListener
。