View绘制完成的监听

本文详细解释了Android中View#post方法的工作原理,它如何在View的测量、布局和绘制完成后执行Runnable。通过分析ViewRootImpl和HandlerActionQueue,展示了post方法如何将任务推迟到UI线程的MessageQueue中执行。
摘要由CSDN通过智能技术生成

        有一个问题是:什么时候能知道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会比较合适。如果你想在布局完成直接进行某些绘制相关的操作,可以考虑使用postaddOnPreDrawListener

相关参考

深度详解 View.post() 为何能够获取到 View 的宽高值?

  • 22
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值