View的post方法执行的时机

Posted lxn_李小牛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了View的post方法执行的时机相关的知识,希望对你有一定的参考价值。

概述

View的post方法我一般用来在Activity的onCreate方法中获取View的尺寸,那么为什么在这里面能够正常获取到,它的执行时机又是什么时候,今天来分析一下。

首先把自定义View添加到布局文件中

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.appclient.MainActivity"
    android:orientation="vertical"
    >

    <com.example.appclient.CustomView
        android:id="@+id/custom"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    </com.example.appclient.CustomView>
</LinearLayout>

用法如下

 customView.post(new Runnable() 
            @Override
            public void run() 
                System.out.println("=====自定义View尺寸: " + customView.getWidth());
            
        );

customView是我们自定义的一个View,我们先通过打印log的方式直观的看一下这里run方法执行的时机,代码如下

public class CustomView extends View
    public CustomView(Context context) 
        super(context);
    

    public CustomView(Context context, @Nullable AttributeSet attrs) 
        super(context, attrs);
        System.out.println("============构造函数");
    

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        System.out.println("==========onMeasure");
    

    @Override
    protected void onFinishInflate() 
        super.onFinishInflate();
        System.out.println("==========onFinishInflate");
    

    @Override
    protected void onAttachedToWindow() 
        super.onAttachedToWindow();
        System.out.println("=================onAttachedToWindow");
    

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) 
        super.onLayout(changed, left, top, right, bottom);
        System.out.println("==========onLayout");
    

    @Override
    protected void onDraw(Canvas canvas) 
        super.onDraw(canvas);
        System.out.println("==========onDraw");
    


从日志中我们就能够明白,为什么post方法中能够正确获取View的尺寸了,因为此时View已经onLayout完毕了。这里还有一个需要注意的地方,不知道大家注意到了没,我们自定义的View在布局文件宽写的是wrap_content,怎么打印的尺寸是768,这里的768是屏幕的宽度。这里就要涉及到自定义View中的知识了,先说下结论,然后我们在从源码的角度来分析一下

结论:当自定义View的尺寸为wrap_content的时候,它的效果和match_parent一样,等于父View的尺寸。

接下来我们从源码的角度分析一下,父View对子View的测量是从measureChildWithMargins方法开始的,这个方法的主要的作用是根据父View的测量规格和尺寸以及子View的布局参数来测量子View


 protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) 
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();//获取子View的布局参数
        //根据父View的MeasureSpec,padding,子View的margin,宽度等获取子View的MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
        //调用子View的measure方法,传入MeasureSpec
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    

我们主要看看getChildMeasureSpec方法

spec:父View的MeasureSpec

padding:父View指定的padding,

childDimension:子View的布局参数

public static int getChildMeasureSpec(int spec, int padding, int childDimension) 
        int specMode = MeasureSpec.getMode(spec);//父View的测量模式
        int specSize = MeasureSpec.getSize(spec);//父View的尺寸

        int size = Math.max(0, specSize - padding);//子View的布局参数是wrap_content时,最终设定给它的尺寸大小

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) 
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) 
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
             else if (childDimension == LayoutParams.MATCH_PARENT) 
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
             else if (childDimension == LayoutParams.WRAP_CONTENT) 
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) 
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
             else if (childDimension == LayoutParams.MATCH_PARENT) 
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
             else if (childDimension == LayoutParams.WRAP_CONTENT) 
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) 
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
             else if (childDimension == LayoutParams.MATCH_PARENT) 
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
             else if (childDimension == LayoutParams.WRAP_CONTENT) 
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            
            break;
        
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    

从上面的代码可以看出,当子View的尺寸设置为wap_content的时候,它最终的尺寸被赋值了size,而size就是父View的尺寸减去padding,如果没有设置padding的话,那么就用父View的尺寸去获取子View的MeasureSpec,获取到子View的MeasureSpec之后,就调用它的measure方法进行测量,measure又会调用onMeasure方法,实际的测量是在onMeasure方法里的.

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    

setMeasureDimension方法用来保存测量之后的宽高,getDefaultSize用来获取默认情况下的尺寸。

 public static int getDefaultSize(int size, int measureSpec) 
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) 
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        
        return result;
    

从这里我们可以看到,当我们给View指定尺寸为wrap_content,也就是测量模式是AT_MOST的时候,效果和EXACTLY一样,View的尺寸是specSize,而这个specSize的值是我们从父View传给子View的MeasureSpec中获取出来的,从上面的代码我们分析出来这个尺寸就是父View的尺寸,所以可以验证我们的结论。

Post方法

回到主题,我们这篇文章的重点是讲View的post方法执行的时机。再次给出日志的打印顺序


如何判断ListView的某个条目是否滑出了屏幕这篇文章我们知道,当attachInfo为空的时候,我们通过post提交的动作会缓存到一个HandlerActionQueue中,执行的时机是在View的performTraversals方法中,关键代码如下

 private void performTraversals() 
        // cache mView since it is used so much below...
        final View host = mView;

        if (DBG) 
            System.out.println("======================================");
            System.out.println("performTraversals");
            host.debug();
        
        //省去一些代码
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        //开始测量
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        //开始布局
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);

performTraversals方法中会依次进行View的measure,layout,draw,我们可以看到,dispatchAttachedToWindow方法是在performMeasure方法之前执行的,那么为什么执行的时机却是onLayout之后呢?

我们的App都是基于消息驱动机制来运行的,主线程的Looper会不断的循环,从MessageQueue中取出消息来执行,只有上一个Message执行完了才会取出下一个Message进行执行,而Handler是用于把Message发送到MessageQueue中去,等轮到Message执行时,会交给Message的target,也就是对应的Handler去执行,执行完了,又取出下一个消息,如此循环。

performTraversals会先执行dispatchAttachedToWindow,

  void dispatchAttachedToWindow(AttachInfo info, int visibility) 
        mAttachInfo = info;
       //省去一些代码
        if (mRunQueue != null) 
            mRunQueue.executeActions(info.mHandler);
executeActions方法会执行我们通过post方法提交的action,它的过程是这样的,通过info的mHandler对象把消息发送到消息队列去排队执行,而主线程的Handler现在执行的是performMeasure,performLayout等这个消息,等这个消息执行完,就执行我们post提交的消息,这个时候就能够获取尺寸了。



以上是关于View的post方法执行的时机的主要内容,如果未能解决你的问题,请参考以下文章

View测量流程与困惑

View测量流程与困惑

Android布局基础

Android必知必会-获取View坐标和长宽的时机

Android自定义View之自定义一个简单的阶梯式布局

Android自定义View之自定义一个简单的阶梯式布局