自定义View之总结

一、View绘制的流程框架



       View的绘制是从上往下一层层迭代下来的。DecorView-->ViewGroup(--->ViewGroup)-->View ,按照这个流程从上往下,依次measure(测量),layout(布局),draw(绘制)。关于DecorView,可以看这篇文章


二、Measure流程

顾名思义,就是测量每个控件的大小。

调用measure()方法,进行一些逻辑处理,然后调用onMeasure()方法测量自己,在其中调用setMeasuredDimension()设定View的宽高信息,完成View的测量操作。

View#Measure:

   public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        ......
        if (forceLayout || needsLayout) {
             ......
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
            ......
        }

    ......
    }
measure()方法中,传入了两个参数 widthMeasureSpec, heightMeasureSpec 表示View的宽高的一些信息。
 
 
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

      由上述流程来看Measure流程很简单,关键点是在于widthMeasureSpec, heightMeasureSpec这两个参数信息怎么获得?

如果有了widthMeasureSpec, heightMeasureSpec,通过一定的处理(可以重写,自定义处理步骤),从中获取View的宽/高,调用setMeasuredDimension()方法,指定View的宽高,完成测量工作。

      如果是ViewGroup需要调用measureChild方法对子控件进行测量,例如,WaterfallLayout#onMeasure:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        ......

    }
ViewGroup#measureChildren:
   protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }
说明,在这里遍历ViewGroup的子Views,调用measureChild方法测量子控件。
ViewGroup# measureChild:
    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

View的测量流程:


理解view测量,就必须了解MeasureSpec,参考这篇文章《 MeasureSpec之详解 》。

三、Layout流程

      测量完View大小后,就需要将View布局在Window中,View的布局主要通过确定上下左右四个点来确定的。

其中布局也是自上而下,不同的是ViewGroup先在layout()中确定自己的布局,然后在onLayout()方法中再调用子View的layout()方法,让子View布局。在Measure过程中,ViewGroup一般是先测量子View的大小,然后再确定自身的大小。

View#layout:

public void layout(int l, int t, int r, int b) {  

    // 当前视图的四个顶点
    int oldL = mLeft;  
    int oldT = mTop;  
    int oldB = mBottom;  
    int oldR = mRight;  

    // setFrame() / setOpticalFrame():确定View自身的位置
    // 即初始化四个顶点的值,然后判断当前View大小和位置是否发生了变化并返回  
 boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    //如果视图的大小和位置发生变化,会调用onLayout()
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {  

        // onLayout():确定该View所有的子View在父容器的位置     
        onLayout(changed, l, t, r, b);      
  ...

}
上面看出通过 setFrame() / setOpticalFrame():确定View自身的位置,通过onLayout()确定子View的布局。

setOpticalFrame()内部也是调用了setFrame(),所以具体看setFrame()怎么确定自身的位置布局。

 
 
protected boolean setFrame(int left, int top, int right, int bottom) {
    ...
// 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
// 即确定了视图的位置
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;

    mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}

确定了自身的位置后,就要通过onLayout()确定子View的布局。onLayout()是一个可继承的空方法。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

注意:

(1)如果当前View就是一个单一的View,那么没有子View,就不需要实现该方法。

(2)如果当前View是一个ViewGroup,就需要实现onLayout方法,该方法的实现个自定义ViewGroup时其特性有关,必须自己实现。

由此便完成了一层层的的布局工作。

ViewGroup的onLayout中要遍历view调用layout函数:

例如,WaterfallLayout#onLayout:

  @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        clearTop();
        for (int i = 0; i < childCount; i++) {
            View child = this.getChildAt(i);
            //得到当前要摆放图片的高度
            int childHeight = child.getMeasuredHeight() * childWidth / child.getMeasuredWidth();
            //得到最短的列,准备将这个控件摆放在这个列中
            int minColum = getMinHeightColum();
            // 因为通过getMinHeightColum()得到的是当前最短列的索引,因为索引是从0开始的,所以,假设我们当前最短的是第三列,
            // 所以通过getMinHeightColum()得到的值是2;因为每个图片都是由图片本身和中间的间距组成,
            // 所以当前控件的left值就是2*(childWidth + hSpace)
            int tleft = minColum * (childWidth + hSpace);
            //top[minColum]就是当前要摆放图片的top值
            int ttop = top[minColum];
            //right就是left加上图片的宽度
            int tright = tleft + childWidth;
            //加上图片的自身高度就是bottom值
            int tbottom = ttop + childHeight;
            //更新当前列的高度
            top[minColum] += vSpace + childHeight;
            child.layout(tleft, ttop, tright, tbottom);
        }

    }

View的布局流程:


四、Draw过程

View的绘制过程遵循如下几步:
①绘制背景 background.draw(canvas)

②绘制自己(onDraw)

③绘制Children(dispatchDraw)

④绘制装饰(onDrawScrollBars)

从源码中可以清楚地看出绘制的顺序。

View#draw:

public void draw(Canvas canvas) {
// 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
// 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制。
// 如果自定义的视图确实要复写该方法,那么需要先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。
    ...
    int saveCount;
    if (!dirtyOpaque) {
          // 步骤1: 绘制本身View背景
        drawBackground(canvas);
    }

        // 如果有必要,就保存图层(还有一个复原图层)
        // 优化技巧:
        // 当不需要绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过
        // 因此在绘制的时候,节省 layer 可以提高绘制效率
        final int viewFlags = mViewFlags;
        if (!verticalEdges && !horizontalEdges) {

        if (!dirtyOpaque) 
             // 步骤2:绘制本身View内容  默认为空实现,  自定义View时需要进行复写
            onDraw(canvas);

        ......
        // 步骤3:绘制子View   默认为空实现 单一View中不需要实现,ViewGroup中已经实现该方法
        dispatchDraw(canvas);

        ........

        // 步骤4:绘制滑动条和前景色等等
        onDrawScrollBars(canvas);

       ..........
        return;
    }
    ...    
}      

      无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。

View#onDraw:

    protected void onDraw(Canvas canvas) {
    }

View绘制流程:


五、总结

      从View的测量、布局和绘制原理来看,要实现自定义View,根据自定义View的种类不同,可能分别要自定义实现不同的方法。但是这些方法不外乎:onMeasure()方法,onLayout()方法,onDraw()方法。

onMeasure()方法:单一View,一般重写此方法,针对wrap_content情况,规定View默认的大小值,避免于match_parent情况一致。    ViewGroup,若不重写,就会执行和单子View中相同逻辑,不会测量子View。一般会重写onMeasure()方法,循环测量子View。

onLayout()方法:单一View,不需要实现该方法。ViewGroup必须实现,该方法是个抽象方法,实现该方法,来对子View进行布局。

onDraw()方法:无论单一View,或者ViewGroup都需要实现该方法,因其是个空方法


参考:

http://www.jianshu.com/p/3d2c49315d68


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值