View绘制流程浅析,我所理解的View绘制。

有过自定义控件经历的朋友都知道,自定义View的时候所经历的三个方法 onMeasure()、onLayout()、onDraw(), 分别对应 测量(要在多大的地方绘制)、布局(确定位置)、绘制(具体绘制的内容) ;
这个和现实生活中作画是完全能对应上的。

这里先来张过程图:
这里写图片描述
图片出处
一个前辈的blog,如果觉得这篇文章分析太浅可以去看看。

这里我弄了个自己理解的草图:

这里写图片描述

这里省略了很多步骤,不过一个View 显示在Activity或者Fragment上这几个方法肯定是要走的。

onMeasure()

作画的第一步:选择要画在哪里 如:画在A4纸上、墙壁上····
这些东西都有个统一属性,那就是有个范围,比如说在A4纸上画一颗树,如果你画出去就没意义了;
在View绘制中的 measure 也是如此,而在我们的程序界面也不只可能只有一颗树这么一个元素,树上可能有树叶、果实;
如一个View中包含了TextView,EditText、Button 等,所以,需要对View中的每一个元素进行测量,才能更好的对这些元素进行 排版、控制、使用… (想想如果你画一棵树,而树的果实有整颗树那么大的画面吧~是不是很美 O(∩_∩)O哈哈~)

先来个概念性的东西:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
        ...
}

onMeasure()这个方法中的widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。
MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。specMode一共有三种类型,如下所示:

  1. EXACTLY
    表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。 //(match_parent or 200dp;)
  2. AT_MOST
    表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。 // (wrap_content)
  3. UNSPECIFIED
    表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。

上面的解释是不是看得稀里糊涂的,没看懂没关系,下面这个例子一目了然。

一般在自定义控件的时候:

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        switch (modeWidth){
            case MeasureSpec.EXACTLY: //match_parent or 指定具体值 如:android:layout_width="200dp";
                sizeWidth = 200;//这里是随便写的~
                break;
            case MeasureSpec.AT_MOST: //wrap_content
                sizeWidth = 300;
                break;
            case MeasureSpec.UNSPECIFIED:  //该视图大小按照自己的意愿任意大小,没有任何限制
                sizeWidth = 400;
                break;
        }
}

这个想必用过的都不会陌生了,这段代码的意思是:
如果控件的宽度 设置模式为
android:layout_width="match_parent"
或者
android:layout_width="100dp"
就把 该控件的宽度设置为 200px;
而设置为
android:layout_width="warp_content"
则把控件宽度设置为 300px;

至于第三个 case MeasureSpec.UNSPECIFIED 这个我也不知道怎么进去(主要是没用到过,在ScrollView或者ListView这种控件上应该有用到,我就懒得去查了~~)

为了更直观点,来张图吧:
这里写图片描述

xml:

<?xml version="1.0" encoding="utf-8"?>
<viewdrawprocess.fmr.com.viewdrawprocesstest.MyView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorAccent"
    >

</viewdrawprocess.fmr.com.viewdrawprocesstest.MyView>

Java 代码:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        switch (modeWidth){
            case MeasureSpec.EXACTLY: //match_parent or 指定具体值 如:android:layout_width="200dp";
                sizeWidth = 200;//这里是随便写的~
                break;
            case MeasureSpec.AT_MOST: //wrap_content
                sizeWidth = 300;
                break;
            case MeasureSpec.UNSPECIFIED:  //该视图大小按照自己的意愿任意大小,没有任何限制
                sizeWidth = 400;
                break;
        }

        setMeasuredDimension(sizeWidth, 200);
    }

这里只设置了宽度,并且只设置了match_parent 一种模式,其他的模式各位可以自己试试,是否像上面说的一样~

onLayout()

测量完成之后接着就是确定视图的位置了,现在是这样一种情景:
我拿到了一张 1米长,1米宽的纸,想在离纸最上方20厘米,最左边20厘米的地方开始作画;
而这一行为正是onLayout要做的事。
注意:确定位置是对于 子View 而言的;如同作画,画的元素位置是相对于纸张的。
在ViewRoot的performTraversals()方法会在measure结束后继续执行,并调用View的layout()方法来继续执行:

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);  

layout()方法接收四个参数,分别代表着left、top、right、buttom的坐标(Android只要涉及到四个坐标的,我看过的都是这个顺序~~),而这个坐标是相对于当前视图的父视图而言的。可以看到,这里还把onMeasure测量出的宽度和高度传到了layout()方法中。
继续来看layout();

public void layout(int l, int t, int r, int b) {  
    int oldL = mLeft;  
    int oldT = mTop;  
    int oldB = mBottom;  
    int oldR = mRight;  
    boolean changed = setFrame(l, t, r, b);  
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {  
        if (ViewDebug.TRACE_HIERARCHY) {  
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);  
        }  
        onLayout(changed, l, t, r, b);  
        mPrivateFlags &= ~LAYOUT_REQUIRED;  
        if (mOnLayoutChangeListeners != null) {  
            ArrayList<OnLayoutChangeListener> listenersCopy =  
                    (ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone();  
            int numListeners = listenersCopy.size();  
            for (int i = 0; i < numListeners; ++i) {  
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);  
            }  
        }  
    }  
    mPrivateFlags &= ~FORCE_LAYOUT;  
}  

在layout()中,首先会调用setFrame()来判断视图的大小是否发生过变化,来确定有没有必要对当前的视图进行重新layout,同时还会在这里把传递过来的四个参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量。
接着会调用onLayout回调方法,具体实现由重写了onLayout方法的ViewGroup的子类去实现。
为什么这么说呢? 来看代码
View中的onLayout:

   protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        throw new RuntimeException("Stub!");
    }

啥事都没干!

而ViewGroup:

 protected abstract void onLayout(boolean changed, int l, int t, int r, int b);  

可以看到,ViewGroup中的onLayout()方法是一个抽象方法,所以ViewGroup的子类都必须重写这个方法。而像LinearLayout、RelativeLayout这种继承ViewGroup的,都重写了这个方法,然后在内部按照各自的规则对子视图位置进行计算。

好了原理分析完毕,接着来个例子看看是不是如上分析的一样。

public class MyViewGroup extends ViewGroup {

    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (getChildCount() > 0) {
            View childView = getChildAt(0); //得到第一个添加到 MyViewGroup 的View (这应该很容易理解了~)
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }
    /**
     * @param changed 当前View的大小和位置改变了
     * @param left 左部位置(相对于父视图)
     * @param top 顶部位置(相对于父视图)
     * @param right 右部位置(相对于父视图)
     * @param bottom 底部位置(相对于父视图)
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        if (getChildCount() > 0) {
            View childView = getChildAt(0);
            //childView.getMeasuredWidth() 这个值是是在 onMeasure 中测量出来的值,childView.getMeasuredHeight() 同~。
            childView.layout(100, 100, childView.getMeasuredWidth(), childView.getMeasuredHeight());
        }
    }
}

逻辑就很简单了,onMeasure 就不多说了;来看onLayout 判断是否有子视图,有则调用子视图的layout(),这里为了有效果,我给了离父视图左、上、分别100px的间距(是不是和margin很像~)。
ok 来看看我们的布局:

  <viewdrawprocess.fmr.com.viewdrawprocesstest.MyViewGroup
        android:id="@+id/myViewGroup"
        android:layout_below="@+id/myView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
       >

        <ImageView
            android:id="@+id/img"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/colorPrimaryDark"
            android:src="@mipmap/ic_launcher" />

    </viewdrawprocess.fmr.com.viewdrawprocesstest.MyViewGroup>

效果:
这里写图片描述

有点小~ 不过不影响,可以看见,我在ImageView 这个控件里没有设置margin 值,它依然有个左、上边距。

onDraw()

measure 和 layout 之后 就是 draw 了 话不多说,直接来看看draw 干了些什么:

 public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;


        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
    ...
   // Step 2, save the canvas' layers
        int paddingLeft = mPaddingLeft;

        final boolean offsetRequired = isPaddingOffsetRequired();
        if (offsetRequired) {
            paddingLeft += getLeftPaddingOffset();
        }
    ...
  // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);
    ...
   // Step 5, draw the fade effect and restore layers
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;
    ...
  // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);

        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }
   }

1、如果有设置背景,则绘制背景
2、保存canvas层
3、绘制自身内容
4、如果有子元素则绘制子元素
5、绘制效果
6、绘制装饰品(scrollbars)

这里只给出了结果,并没有去分析了,因为我觉得源码这东西,看人家分析还不如自己看一遍来着深刻

可以看见 draw() 这个方法有一个 canvas 对象,这个就是“纸张”对象,我们要做的就是往这上面画东西;
老规矩:

public class MyView extends View{

    private Paint mPaint;

    public MyView(Context context) {
        super(context);
        init();
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    }

        @Override
    protected void onDraw(Canvas canvas) {
        mPaint.setColor(Color.RED);
        canvas.drawRect(0 ,0 ,getWidth() / 2 ,getHeight() / 2 ,mPaint);

        mPaint.setColor(Color.BLUE);
        mPaint.setTextSize(40);
        canvas.drawText("Hello!",0,getHeight() / 4 ,mPaint);
    }
}

这里的 Paint 是Android提供给我们的画笔,它提供了丰富的API (反正多到我都不想看~) ,这里只是在 画布上画了一个矩形,,然后再写了个“Hello”,代码很简单。
使用:

 <viewdrawprocess.fmr.com.viewdrawprocesstest.MyView
        android:id="@+id/myView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorAccent" />

效果:
这里写图片描述

ok~ 一个简单的自定义控件就这么愉快的完成了,不过看完之后是不是觉得数学不好已经hold不住了~
而且要写好一个自定义控件用到交互需要对View触摸事件进行处理,如果想牛逼点还要加动画…说到这里又是一部,从入门到放弃史~~~!

这篇blog是个人对View绘制的理解,虽然现在各种Blog都对它分析很透彻了,但是那永远是别人的!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值