Android 自定义View及View绘制流程

前言

一、自定义View介绍

二、View绘制流程

2.1  Measure()

2.2  Layout()

2.2  Draw()

注意


前言

我们经常使用TextView、button等控件,但是有些同学对于它们是如何显示和扩展的却并不那么熟悉。而这一块的知识也进阶高手必备的,写这一篇文章是想把view绘制这块的技术全面总结一下。

Activity作为应用程序的载体负责向用户展现界面并提供了窗口进行视图绘制。看这篇文章 View如果加载出来的 ,当调用 Activity 的setContentView 方法后会调用PhoneWindow 类的setContentView方法,最终会生成一个继承FrameLayout的PhoneWindow的内部类DecorView对象。DecorView容器中包含根布局,通过findViewById()找到一个id为content的FrameLayout的根布局,Activity加载布局的xml最后通过 LayoutInflater.inflate()  将xml文件中的内容解析成View层级体系,最后填加到id为content的FrameLayout根布局中。LayoutInflater.inflate() 会调用 createViewFromTag解析该元素拿到View类型的temp对象实例,再调用rInflate采用递归解析temp中的子View,并将这些子View添加到temp中。

好了,上面大致认识了一下View被加载显示的原理,那么接下来一起看看View类内部是什么样子的?

一、自定义View介绍

1.1 实现方式:

类型 定义
自定义组合控件 多个控件组合成为一个新的控件,方便多处复用
继承系统控件 继承自TextView等系统控件,在系统控件的基础功能上进行扩展
继承View、ViewGroup 不复用系统控件逻辑,继承View、ViewGroup进行功能定义

查看本篇,代码实例教程:Android 自定义组合控件+约束布局ConstraintLayout+自定义控件属性

1.2 构造函数:

继承系统View或直接继承View,都需要对构造函数进行重写,构造函数有多个,区别在于方法参数:

public class TestView extends View {
    /**
     * 在java代码里new的时候会用到
     * @param context
     */
    public TestView(Context context) {
        super(context);
    }

    /**
     * 在xml布局文件中使用时自动调用
     * @param context
     */
    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 不会自动调用,如果有默认style时,在第二个构造函数中调用
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    /**
     * 只有在API版本>21时才会用到
     * 不会自动调用,如果有默认style时,在第二个构造函数中调用
     * @param context
     * @param attrs
     * @param defStyleAttr
     * @param defStyleRes
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}

 1.3 绘制方法:

函数 定义
Measure() 测量View的宽高
Layout() 计算当前View以及子View位置
Draw() 绘制视图的工作

*onLayout()只执行一次,之后先调用getMeasureWidth()才能调用getWidth()去拿到View的宽度。

1.4 View的屏幕坐标

函数 定义
getTop() 获取View到其父布局顶边的距离
getLeft() 获取View到其父布局左边的距离
getBottom() 获取View到其父布局底边的距离

getRight()

获取View到其父布局右边的距离

二、View绘制流程

2.1  Measure()

1、 onMeasure()方法

测量视图大小,整个测量过程的入口位于View的measure方法当中,measure方法又回调OnMeasure。从顶层父View到子View递归调用measure()方法。measure()方法当中做了一些参数的初始化之后调用了onMeasure方法。

源码

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        
//该方法用来设置View的宽高,在我们自定义View时也会经常用到。
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
          
//该方法用来获取View默认的宽高,结合源码来看。
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

先看getDefautSize()方法的参数 getSuggestedMinimumHeight() 的源码:

protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
  • 当View没有设置背景时,默认大小就是mMinWidth,这个值对应Android:minWidth属性,如果没有设置时默认为0.
  • 如果有设置背景,则默认大小为mMinWidth和mBackground.getMinimumWidth()当中的较大值。

再看getDefaultSize(int size , int measureSpec) 源码:

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

        //从这里我们看出,对于AT_MOST和EXACTLY在View当中的处理是完全相同的。所以在我们自定义View时要对这两种模式做出处理。
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
}

有两个参数size和measureSpec

  • size表示View的默认大小,它的值是通过参数getSuggestedMinimumWidth()方法来获取的.
  • measureSpec里面存储了View的测量值以及测量模式.
     

小结

ViewGroup的测量过程与View有一点点区别,其本身是继承自View,它没有对View的measure方法以及onMeasure方法进行重写。为什么没有重写onMeasure呢?ViewGroup除了要测量自身宽高外还需要测量各个子View的大小,而不同的布局测量方式也都不同(可参考LinearLayout以及FrameLayout),所以没有办法统一设置。

因此它提供了测量子View的方法measureChildren()以及measureChild()帮助我们对子View进行测量。measureChildren()以及measureChild()的源码这里不再分析,大致流程就是遍历所有的子View,然后调用View的measure()方法,让子View测量自身大小。具体测量流程上面也以及介绍过了。

measure过程会因为布局的不同或者需求的不同而呈现不同的形式,使用时还是要根据业务场景来具体分析,如果想再深入研究可以看一下LinearLayout的onMeasure方法。

2、 MeasureSpec类

在调用onMeasure()时,会根据MeasureSpec类的封装View尺寸的值来确定View的宽高,MeasureSpec = mode+size ,其中一共存在三种mode:UNSPECIFIED、EXACTLY 和AT_MOST。如下:

父mode 作用 对应子View
EXACTLY 精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Size. 父布局没有做出限制,子View有自己的尺寸,则使用,如果没有则为0.
AT_MOST 最大模式,View尺寸有一个最大值,不可以超过MeasureSpec当中的Size值. 父布局采用精准模式,有确切的大小,如果有大小则直接使用,如果子View没有大小,子View不得超出父view的大小范围.
UNSPECIFIED 无限制,View对尺寸没有任何限制,View设置为多大就应当为多大. 父布局采用最大模式,存在确切的大小,如果有大小则直接使用,如果子View没有大小,子View不得超出父view的大小范围.

 在View当中,MeasureSpece的测量代码:(为子View设置MeasureSpec参数,子View大小需要在View中具体设置)

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        //当父View要求一个精确值时,为子View赋值
        case MeasureSpec.EXACTLY:
            //如果子view有自己的尺寸,则使用自己的尺寸
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
                //当子View是match_parent,将父View的大小赋值给子View
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
                //如果子View是wrap_content,设置子View的最大尺寸为父View
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父布局给子View了一个最大界限
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                //如果子view有自己的尺寸,则使用自己的尺寸
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 父View的尺寸为子View的最大尺寸
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //父View的尺寸为子View的最大尺寸
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父布局对子View没有做任何限制
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
            //如果子view有自己的尺寸,则使用自己的尺寸
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //因父布局没有对子View做出限制,当子View为MATCH_PARENT时则大小为0
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //因父布局没有对子View做出限制,当子View为WRAP_CONTENT时则大小为0
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
    
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

可见:子View的测量模式是由自身LayoutParam和父View的MeasureSpec来决定的!

2.2  Layout()

对于View来说用来计算View的位置参数,进行页面布局。对于ViewGroup来说,除了要测量自身位置,还需要测量子View的位置,即从顶层父View向子View的递归调用view.layout()方法的过程,父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。

源码:


public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        //这里通过setFrame或setOpticalFrame方法确定View在父容器当中的位置。
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        //调用onLayout方法。onLayout方法是一个空实现,不同的布局会有不同的实现。
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

        }

    }

小结

  • 在layout()方法中的四个参数l、t、r、b分别代表View的左、上、右、下四个边界相对于其父View的距离。
  • 在layout()方法中通过setOpticalFrame(l, t, r, b)或 setFrame(l, t, r, b)方法对View自身的位置进行了设置,所以onLayout(changed, l, t, r, b)方法主要是ViewGroup对子View的位置进行计算。

2.2  Draw()

绘制视图。draw过程也就是View绘制到屏幕上的过程。ViewRoot创建一个Canvas对象,然后调用OnDraw()。整个过程可以分为6个步骤。

  1. 绘制背景。
  2. 保存canvas画布的图层。
  3. 绘制View的内容。
  4. 绘制子View。
  5. 绘制边缘、阴影等效果。
  6. 绘制前景,如滚动条。

源码:

    public void draw(Canvas canvas) {
        int saveCount;
        // 1. 如果需要,绘制背景
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
        // 2. 如果有必要,保存当前canvas。
        final int viewFlags = mViewFlags;
        if (!verticalEdges && !horizontalEdges) {
            // 3. 绘制View的内容。
            if (!dirtyOpaque) onDraw(canvas);
            // 4. 绘制子View。
            dispatchDraw(canvas);
            drawAutofilledHighlight(canvas);
            // 覆盖是内容的一部分,在前景下绘制
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }
            // 6. 绘制前景,如滚动条等等。
            onDrawForeground(canvas);
            return;
        }
    }
    private void drawBackground(Canvas canvas) {
        //获取背景
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        setBackgroundBounds();
        //获取便宜值scrollX和scrollY,如果scrollX和scrollY都不等于0,则会在平移后的canvas上面绘制背景。
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

在onDraw(Canvas canvas)方法中,可以绘制图片,通过使用canvas、paint、matrix等。

参考链接https://www.jianshu.com/p/705a6cb6bfee

注意

在onLayout()方法中,需要先调用getMeasureWidth()方法之后才能getWidth()。onMearure()/onLayout()方法,需要调用Requestlayout()才能生效。onDreaw()调用invalidate()方法可以生效(主线程会卡顿),如果在子线程调用PostInvalidate()生效。

发布了153 篇原创文章 · 获赞 755 · 访问量 100万+

猜你喜欢

转载自blog.csdn.net/csdn_aiyang/article/details/88852531