View的工作原理

view的三大绘制流程从源码中看都是通过根视图viewRoot的requestLayout,然后实现类ViewRootImpl调用scheduleTraversals()方法,里面分别调用了 performMeasure() 、performLayout() 、performDraw() 方法,分别对应了View的measure()、layout()、draw()三个绘制过程。

measure:用来确定视图宽高的规格和大小,MeasureSpec代表32位 int值,包含高2位测量规格SpecMode和低30位的测量大小SpecSize,打包成一个int值主要可以避免过多对象的内存分配,测量模式有exactly父view指定子大小具体大小,对应布局文件的match_parent和具体值,at_most父view为子view指定最大范围对应布局文件wrap_content,unSpecified不限制子view大小三种规格。之后又调用setMeasuredDimension()方法来测量出子大小,最终onMeasure结束,所以子view的宽高由父view的MeasureSpec和自身的LayoutParams属性决定。

layout:确定view在父容器的位置,它先调用setFrame确定左上右下四个位置坐标并判断视图是否有变化,如果有就更新视图再调用onLayout()方法。我们可以通过getTop,getLeft,getRight,getBotton获取坐标,所以也说明getWidth和getHeight跟getMeasureWidth和getMeasureHeight有区别的,getMeasureWidth和getMeasureHeight在测量结束就可以获取到,而getWidth和getHeight必须在onLayout结束后才能获取到。

draw:负责view绘制在屏幕上,首先ViewRoot创建一个Canvas对象,然后调用onDraw()方法进行绘制。onDraw()方法的绘制流程为:
① 绘制视图背景。
② 绘制画布的图层。
③ 绘制View内容。
④绘制滚动条。

requestLayout,invalidate,postInvalidate区别与联系

相同点:三个方法都有刷新界面的效果。
不同点:invalidate和postInvalidate只会调用onDraw()方法;requestLayout则会重新调用onMeasure、onLayout、onDraw。

详细解析

调用了invalidate方法后,会为该View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到ViewRootImpl中,最终触发performTraversals方法,进行开始View树重绘流程(只绘制需要重绘的视图)。
调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量onMeasure、布局onLayout、绘制onDraw。

measure过程

一般view的measure是由自身view和子view一起决定的,例如linearLayout。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

源码会先判断水平还是竖直,然后遍历子元素,调用measureChildBeforeLayout(),这个方法内部会调用
measureChindWithMargin()获取子元素的layoutparames 然后调用measure(childWidthMeasureSpec, childHeightMeasureSpec),其中会调用方法getDefaultSize(), 这个会确定measureSpecMode()和measureSpecSize(),计算子view的大小,最后在加上margin得出最终大小。
MeasureSpec:MeasureSpec代表一个32为int值,其中高两位代表SpecMode,低30位代表SpecSize。
AT_MOST:父容器指定一个可用大小,view的大小小于或等于这个值,它对应layoutParams的wrap_content
EXACTLY:父容器指定一个精确大小,view的大小就是specSize指定的值,对应LayoutParams的match_parent。
unspecified:不限制大小,一般用于系统内部,表示测量状态,一般实际开发用不到。


QQ图片20190715162643.png
public abstract class ViewGroup ...{
...
//遍历所有的子元素
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);
            }
        }
    }
//调用子元素的 measure 方法
protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
          //获取子元素的 layoutParams
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        //getChildMeasureSpec 此处获取 getChildMeasureSpec
        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);
        //调用子元素的 measure 方法(子元素宽MeasureSpec,子元素高MeasureSpec)
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
...
}

一般一个view可能会被调用多次或者view的measure过程和activity生命周期不同步导致获取的是0,可以通过avtivity/view的onWindowFocusChanged()或者view.post()或viewTree监听获取。

layout过程

首先通过setFrame()方法设置四个坐标然后调用onLayout

public class LinearLayout extends ViewGroup {
...
 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }
...
void layoutVertical(int left, int top, int right, int bottom) {
       ...//获取竖向子元素的数量
        final int count = getVirtualChildCount();
        ...
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
                
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }
                //childTop 变大,往下排序
                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }
...//调用子元素的layout方法,它会遍历所有子元素,确定子元素位置,进行从上到下排序。
private void setChildFrame(View child, int left, int top, int width, int height) {        
        child.layout(left, top, left + width, top + height);
    }

draw过程

将view绘制到屏幕上
(1)绘制背景 background(canvas)
(2)绘制自己(onDraw)
(3)绘制children(dispatchDraw)
(4)绘制装饰(onDrawScrollBars)

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;

        /*
         * 绘制遍历执行几个绘图步骤,必须按照适当的顺序执行:(渣百度翻译。。。)
         *
         *      1. Draw the background 画背景
         *      2. If necessary, save the canvas' layers to prepare for fading 如果有必要,保存画布图层以备褪色
         *      3. Draw view's content 绘制视图内容
         *      4. Draw children 绘制子元素
         *      5. If necessary, draw the fading edges and restore layers 如有必要,绘制衰减边并恢复图层
         *      6. Draw decorations (scrollbars for instance) 画装饰(滚动条为例)
         */

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

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children 绘制子元素
            dispatchDraw(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // we're done...
            return;
        }

View 绘制过程的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历所有的子元素的 draw 方法,View 没有具体的实现 dispatchDraw ,子类需要重写 dispatchDraw 。

自定义view分类

1.继承 View 重写 onDraw 方法

这种方法主要用于实现一些不规则的效果,既要重写 onDraw 方法,这种方式需要自己支持 wrap_content,并且 padding 也需要自己处理。

2.继承 ViewGroup 派生特殊的 Layout

这种方法主要用于实现自定义布局,这种方式稍微复杂一些,需要合适的处理 ViewGroup 的测量、布局这两个过程,并同事处理子元素的测量和布局过程。

3.继承特定的 View(比如 TextView)

这种方法比较常见,一般用于扩展某种已有的 View 的功能,这种方法不需要自己支持 wrap_content 和 padding 等。

4.继承特定的 ViewGroup(比如 LiearLayout)

这种方法也比较常见,这种方法不需要自己处理 ViewGroup 的测量和布局。

1.自定义view单纯的用画笔绘制view(死view)
2.自定义view增加手势
3.自定义view增加动画
4.自定义view手势动画交互 ,这4步让我们一步一步的来探索学习

要自定义view首先要创建一个 类继承View类

重写他的3个构造方法,然后去定义我的view的属性,属性在res下面创建一个attrs.xml页面。
然后重写onmeasure()
int widthsize = MeasureSpec.getSize(widthMeasureSpec); //取出宽度数值
int widthmode = MeasureSpec.getMode(widthMeasureSpec); //取出宽度的测量模式

int heightsize = MeasureSpec.getSize(heightMeasureSpec); //取出高度数值
int heightmode = MeasureSpec.getMode(heightMeasureSpec); //取出高度的测量模式

onSizeChange()
该方法在我们的自定义View的视图大小发生改变时会调用此方法。

一般使用方法是在onLayout()中去确定四个位置坐标,如果是viewgroup,还要循环的去给子child.layout(l,t,r,b)
l:左侧距离父View的距离 getLeft()
t:顶部距离父View的距离 getTop()
r:右边距离父View的距离 getRight()
b:下边距离父View的距离 getBottom()
然后去indraw,在这一步中,我们创建画布canvas和画笔paint并设置属性,比如设置颜色,样式,抗锯齿属性。然后调用canvas.draw rect画巨型,弧线(arcs)、圆(circle和oval)、点(point)、线(line)、矩形(Rect)、图片( bitmap)
mPaint.setColor(0xff00ff00);
canvas.drawRect(0, 0,getMeasuredWidth(), getMeasuredHeight(), mPaint);

    mPaint.setColor(mTextColor);
    canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight()
            / 2 + mBound.height() / 2, mPaint);

new path调用path.cubicTo()后面分别x1, y1, x2, y2,x3,y3
canvas.drawPath(path2, p);//画出贝塞尔曲线

下拉刷新。继承自google的下拉库swiperefreshLayout,当下拉这个view的60以内,显示的是螃蟹上面一半加下面一半的水滴弧形,用的是二阶贝塞尔曲线,有三个坐标点,两个X轴和一个下拉的Y轴,调用path.cubicTo()后面分别x1, y1, x2, y2,x3,y3六个参数是方法实现了水滴效果,超过60%后下面一个完整的螃蟹就从隐藏变为显示,上面画的贝塞尔螃蟹就隐藏。其实类似QQ消息滑动消除和直播点赞漂浮动画都是用了贝塞尔曲线。
最后更新页面调用postinvalidate.在viewgroup中add这个自定义view

你可能感兴趣的:(View的工作原理)