Android View之绘制流程

android view是大家实现各种漂亮ui的基础,因此对于它的重要性,就可想而知了;网上关于android view分析的文章也是非常的多,之所以还写这篇文章主要还是,通过看大家的分析和自己的理解做一个整理和记录,这样会有个更加深刻的印象。

android view 有几万行的代码,

本文主要针对view绘制流程的主要三个方法进行分析:测量(Measure)、布局(Layout)、绘制(draw)

 

想要搞懂一个知识,最好的办法是通过实例来分析,所以这里我写了个简单的例子来看下Activity 中setContentView()时view三个方法的流程:

(1)自定义的一个view,重写onMeasure、onLayout、onDraw方法:

public class CustomView extends View {
    private static final String TAG "CustomView";
    public CustomView(Context context) {
        super(context);
        Log.e(TAG,"构造方法");
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpecheightMeasureSpec);
        Log.e(TAG,"onMeasure");

    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changedlefttoprightbottom);
        Log.e(TAG,"onLayout");
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.e(TAG,"onDraw");
    }
}

 

(2)activity 中使用:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    final CustomView view = new CustomView(this);
    view.setBackgroundResource(R.drawable.ic_launcher_background);
    setContentView(view);
}

 

跑下程序看下log 跑的结果:

可以看到三个方法的执行顺序是:onMeasure->onLayout->onDraw,但是这里执行了三次onMeasure两次onLayout,

这是为什么呢?

  我们知道把视图展示到屏幕上,是有一个树形结构的视图,而这个视图树的根节点是DecorView,而DecorView是FrameLayout的子类,我们看FrameLayout的源码,会调用两次onMeasure,而在onLayout后又执行onMeasure->onLayout这个过程,视图大小发生变化然后调用requestLayout()方法,而requestLayout()会导致调用measure()过程 和 layout()过程 。

 

知道了这三个方法的执行流程,我们来分别解释下这三个方法的作用:

 

1.onMeasure方法:

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

}

我们看到onMeasure源码中调用了setMeasuredDimension方法,setMeasuredDimension源码:

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    setMeasuredDimensionRaw(measuredWidthmeasuredHeight);
}

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

 

可以看到setMeasuredDimension()就是真正的测量视图的大小,测量一个view实际上是给字段mMeasuredWidth,mMeasuredHeight设置值,最后执行mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET,将字段mPrivateFlags的EASURED_DIMENSION_SET位设置为1。

这时候我们实例中调用下setMeasuredDimension:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpecheightMeasureSpec);
    setMeasuredDimension(1000,1000);
    Log.e(TAG,"onMeasure");

}

 

如果没重写setMeasuredDimension方法,默认是满屏的,调用后就设置了视图的大小.

接下来我们通过另一个方式来了解下onMeasure方法设置视图大小与父视图的关系:

<android.support.constraint.ConstraintLayout 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=".MainActivity">

    <com.example.linwenbing.demo.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#000000"
        />

</android.support.constraint.ConstraintLayout>

我这边自定义的view父视图设置的宽高都是match_parent 而view本事宽高是wrap_content,运行发现,子视图需然设置的wrap_content但是确布满整个屏幕,但是当自定义view设置具体的宽高时,就是根据具体的宽高显示所以得出结论

当子视图没有设置具体宽高时,子视图采用的是父视图的宽高。这是什么原因呢?看源码:

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

}

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;
}

 

我们发现AT_MOST (相当于wrap_content )和EXACTLY (相当于match_parent )两种情况返回的测量宽高都是specSize,而这个specSize正是我们上面说的父控件剩余的宽高,所以默认onMeasure方法中wrap_content 和match_parent 的效果是一样的,都是填充剩余的空间。

 

要真正理解onMeasure我们还需要知道onMeasure两个参数int widthMeasureSpec, int heightMeasureSpec的意思:

这两个参数并不是真正的宽高,这两个参数中包含了两个意思:MeasureSpec类中的specMode和specSize

MeasureSpec类中specMode的三种模式:

public static class MeasureSpec {
    private static final int MODE_SHIFT 30;
    private static final int MODE_MASK 0x3 << MODE_SHIFT;

    /** @hide */
    @IntDef({UNSPECIFIEDEXACTLYAT_MOST})
    @Retention(RetentionPolicy.SOURCE)
    public @interface MeasureSpecMode {}

    /**
     * Measure specification mode: The parent has not imposed any constraint
     * on the child. It can be whatever size it wants.
     * 父控件不强加任何约束给子控件,它可以是它想要任何大小
     */
    public static final int UNSPECIFIED << MODE_SHIFT;

    /**
     * Measure specification mode: The parent has determined an exact size
     * for the child. The child is going to be given those bounds regardless
     * of how big it wants to be.
     * 父控件已为子控件确定了一个确切的大小,孩子将被给予这些界限,不管子控件自己希望的是多大
     */
    public static final int EXACTLY     << MODE_SHIFT;

    /**
     * Measure specification mode: The child can be as large as it wants up
     * to the specified size.
     * 父控件会给子控件尽可能大的尺寸
     */
   
    public static final int AT_MOST     << MODE_SHIFT;

 

specSize:父控件传过来的大小。

onMeasure方法总结:

(1onMeasure (int widthMeasureSpec, int heightMeasureSpec)是view自己的方法

(2onMeasure 方法简单的理解就是是用于测量视图的大小,主要是用来测量自己和内容的来确定宽度和高度

(3onMeasure有两个参数 int widthMeasureSpec, int heightMeasureSpec),该参数表示控件可获得的空间以及关于这个空间描述的元数据.

(4widthMeasureSpecheightMeasureSpec这两个值通常情况下都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小。

2.onLayout方法:

(1)首页我们看onLayout的源码:

/**
 * Called from layout when this view should
 * assign a size and position to each of its children.
 *
 * Derived classes with children should override
 * this method and call layout on each of
 * their children.
 * @param changed This is a new size or position for this view
 * @param left Left position, relative to parent
 * @param top Top position, relative to parent
 * @param right Right position, relative to parent
 * @param bottom Bottom position, relative to parent
 */
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

 

会发现什么都没有实现,这是因为:

这个其实是android留给我们自己去实现的一个方法,也就是大家都知道的,去布局子View的位置,只有含有子View的容器,才需要重写这个方法,也就是ViewGroup。

而view是通过layout方法来确认自己在父容器中的位置。

viewgroup自定义的onLayout:

 

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int hadUsedHorizontal = 0;//水平已经使用的距离
    int hadUsedVertical = 0;//垂直已经使用的距离
    int width = getMeasuredWidth();
    for (int i = 0i < getChildCount()i++) {
        View view = getChildAt(i);
        if (view.getMeasuredWidth() + hadUsedHorizontal > width) {
            hadUsedVertical = hadUsedVertical + view.getMeasuredHeight() + verticalSpace;
            hadUsedHorizontal = 0;
        }
        view.layout(hadUsedHorizontalhadUsedVerticalhadUsedHorizontal + view.getMeasuredWidth()hadUsedVertical + view.getMeasuredHeight());
        hadUsedHorizontal = hadUsedHorizontal + horizontalSpace + view.getMeasuredWidth();
    }

}

 

 

onLayout总结:

onLayout主要是viewgroup用来确定子view在父容器中的位置,所以在自定义viewgroup时需要重写onLayout.

 

3.onDraw方法:

 onDraw方法比较简单,主要就是把view绘制到屏幕上,看源码:

/**
 * Implement this to do your drawing.
 *
 * @param canvas the canvas on which the background will be drawn
 */
protected void onDraw(Canvas canvas) {

}

也是一个没有任何实现的方法,主要是提供给view自己绘制,onDraw怎么绘制,这里不做详细说,就写个简单绘制一个位于中心的圆:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Log.e(TAG,"onDraw");
    canvas.drawCircle(getMeasuredWidth()/2,getMeasuredHeight()/2,150,mPaint);

}

android view是大家实现各种漂亮ui的基础,因此对于它的重要性,就可想而知了;网上关于android view分析的文章也是非常的多,之所以还写这篇文章主要还是,通过看大家的分析和自己的理解做一个整理和记录,这样会有个更加深刻的印象。

android view 有几万行的代码,

本文主要针对view绘制流程的主要三个方法进行分析:测量(Measure)、布局(Layout)、绘制(draw)

 

想要搞懂一个知识,最好的办法是通过实例来分析,所以这里我写了个简单的例子来看下Activity 中setContentView()时view三个方法的流程:

(1)自定义的一个view,重写onMeasure、onLayout、onDraw方法:

public class CustomView extends View {
    private static final String TAG "CustomView";
    public CustomView(Context context) {
        super(context);
        Log.e(TAG,"构造方法");
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpecheightMeasureSpec);
        Log.e(TAG,"onMeasure");

    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changedlefttoprightbottom);
        Log.e(TAG,"onLayout");
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.e(TAG,"onDraw");
    }
}

 

(2)activity 中使用:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    final CustomView view = new CustomView(this);
    view.setBackgroundResource(R.drawable.ic_launcher_background);
    setContentView(view);
}

 

跑下程序看下log 跑的结果:

 

可以看到三个方法的执行顺序是:onMeasure->onLayout->onDraw,但是这里执行了三次onMeasure两次onLayout,

这是为什么呢?

  我们知道把视图展示到屏幕上,是有一个树形结构的视图,而这个视图树的根节点是DecorView,而DecorView是FrameLayout的子类,我们看FrameLayout的源码,会调用两次onMeasure,而在onLayout后又执行onMeasure->onLayout这个过程,视图大小发生变化然后调用requestLayout()方法,而requestLayout()会导致调用measure()过程 和 layout()过程 。

 

知道了这三个方法的执行流程,我们来分别解释下这三个方法的作用:

 

1.onMeasure方法:

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

}

我们看到onMeasure源码中调用了setMeasuredDimension方法,setMeasuredDimension源码:

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    setMeasuredDimensionRaw(measuredWidthmeasuredHeight);
}

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

 

可以看到setMeasuredDimension()就是真正的测量视图的大小,测量一个view实际上是给字段mMeasuredWidth,mMeasuredHeight设置值,最后执行mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET,将字段mPrivateFlags的EASURED_DIMENSION_SET位设置为1。

这时候我们实例中调用下setMeasuredDimension:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpecheightMeasureSpec);
    setMeasuredDimension(1000,1000);
    Log.e(TAG,"onMeasure");

}

如果没重写setMeasuredDimension方法,默认是满屏的,调用后就设置了视图的大小.

接下来我们通过另一个方式来了解下onMeasure方法设置视图大小与父视图的关系:

<android.support.constraint.ConstraintLayout 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=".MainActivity">

    <com.example.linwenbing.demo.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#000000"
        />

 

</android.support.constraint.ConstraintLayout>

我这边自定义的view父视图设置的宽高都是match_parent 而view本事宽高是wrap_content,运行发现,子视图需然设置的wrap_content但是确布满整个屏幕,但是当自定义view设置具体的宽高时,就是根据具体的宽高显示所以得出结论

当子视图没有设置具体宽高时,子视图采用的是父视图的宽高。这是什么原因呢?看源码:

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

}

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;
}

 

我们发现AT_MOST (相当于wrap_content )和EXACTLY (相当于match_parent )两种情况返回的测量宽高都是specSize,而这个specSize正是我们上面说的父控件剩余的宽高,所以默认onMeasure方法中wrap_content 和match_parent 的效果是一样的,都是填充剩余的空间。

 

要真正理解onMeasure我们还需要知道onMeasure两个参数int widthMeasureSpec, int heightMeasureSpec的意思:

这两个参数并不是真正的宽高,这两个参数中包含了两个意思:MeasureSpec类中的specMode和specSize

MeasureSpec类中specMode的三种模式:

public static class MeasureSpec {
    private static final int MODE_SHIFT 30;
    private static final int MODE_MASK 0x3 << MODE_SHIFT;

    /** @hide */
    @IntDef({UNSPECIFIEDEXACTLYAT_MOST})
    @Retention(RetentionPolicy.SOURCE)
    public @interface MeasureSpecMode {}

    /**
     * Measure specification mode: The parent has not imposed any constraint
     * on the child. It can be whatever size it wants.
     * 父控件不强加任何约束给子控件,它可以是它想要任何大小
     */
    public static final int UNSPECIFIED << MODE_SHIFT;

    /**
     * Measure specification mode: The parent has determined an exact size
     * for the child. The child is going to be given those bounds regardless
     * of how big it wants to be.
     * 父控件已为子控件确定了一个确切的大小,孩子将被给予这些界限,不管子控件自己希望的是多大
     */
    public static final int EXACTLY     << MODE_SHIFT;

    /**
     * Measure specification mode: The child can be as large as it wants up
     * to the specified size.
     * 父控件会给子控件尽可能大的尺寸
     */
   
    public static final int AT_MOST     << MODE_SHIFT;

 

specSize:父控件传过来的大小。

onMeasure方法总结:

(1onMeasure (int widthMeasureSpec, int heightMeasureSpec)是view自己的方法

(2onMeasure 方法简单的理解就是是用于测量视图的大小,主要是用来测量自己和内容的来确定宽度和高度

(3onMeasure有两个参数 int widthMeasureSpec, int heightMeasureSpec),该参数表示控件可获得的空间以及关于这个空间描述的元数据.

(4widthMeasureSpecheightMeasureSpec这两个值通常情况下都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小。

2.onLayout方法:

(1)首页我们看onLayout的源码:

/**
 * Called from layout when this view should
 * assign a size and position to each of its children.
 *
 * Derived classes with children should override
 * this method and call layout on each of
 * their children.
 * @param changed This is a new size or position for this view
 * @param left Left position, relative to parent
 * @param top Top position, relative to parent
 * @param right Right position, relative to parent
 * @param bottom Bottom position, relative to parent
 */
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

 

会发现什么都没有实现,这是因为:

这个其实是android留给我们自己去实现的一个方法,也就是大家都知道的,去布局子View的位置,只有含有子View的容器,才需要重写这个方法,也就是ViewGroup。

而view是通过layout方法来确认自己在父容器中的位置。

viewgroup自定义的onLayout:

 

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int hadUsedHorizontal = 0;//水平已经使用的距离
    int hadUsedVertical = 0;//垂直已经使用的距离
    int width = getMeasuredWidth();
    for (int i = 0i < getChildCount()i++) {
        View view = getChildAt(i);
        if (view.getMeasuredWidth() + hadUsedHorizontal > width) {
            hadUsedVertical = hadUsedVertical + view.getMeasuredHeight() + verticalSpace;
            hadUsedHorizontal = 0;
        }
        view.layout(hadUsedHorizontalhadUsedVerticalhadUsedHorizontal + view.getMeasuredWidth()hadUsedVertical + view.getMeasuredHeight());
        hadUsedHorizontal = hadUsedHorizontal + horizontalSpace + view.getMeasuredWidth();
    }

}

 

 

onLayout总结:

onLayout主要是viewgroup用来确定子view在父容器中的位置,所以在自定义viewgroup时需要重写onLayout.

 

3.onDraw方法:

 onDraw方法比较简单,主要就是把view绘制到屏幕上,看源码:

/**
 * Implement this to do your drawing.
 *
 * @param canvas the canvas on which the background will be drawn
 */
protected void onDraw(Canvas canvas) {

}

也是一个没有任何实现的方法,主要是提供给view自己绘制,onDraw怎么绘制,这里不做详细说,就写个简单绘制一个位于中心的圆:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Log.e(TAG,"onDraw");
    canvas.drawCircle(getMeasuredWidth()/2,getMeasuredHeight()/2,150,mPaint);

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值