Android面试Android进阶(十五)-自定义View相关1

问:自定义View有几个构造函数,及自定义View的主要流程

答:自定义View中共有四个构造函数,一般只需要实现一个参数及两个参数的构造函数即可。自定义View过程中,主要流程有:measure、layout、draw即 测量、布局、绘制,这里面涉及到MeasureSpec、Paint、Canvas、Path等很多重要类。
自定义View的实现方式有很多:自定义组合控件继承系统View 如继承TextView、继承系统ViewGroup 如继承LinearLayout、继承View继承ViewGroup等。

自定义View的四个构造方法:

class MyView : View {
    /**
     * 在java代码里new的时候会用到
     */
    constructor(context: Context?) : super(context) {}

    /**
     * 在xml布局文件中使用时自动调用
     */
    constructor(context: Context?, @Nullable attrs: AttributeSet?) : super(context, attrs) {}

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

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

自定义View的三个主要流程:measure、layout、draw
1、Measure测量流程,从View的measure方法为入口,该方法只是做了一些初始化,之后调用onMeasure方法。来看onMeasure()方法:

    //View的onMeasure源码
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

首先看到的是onMeasure()方法要求传入两个int类型的参数,分别是宽高。这里需要了解一下 MeasureSpec 是什么东西。
MeasureSpec是View的内部类,值保存在一个int值当中,一个int有32位,前两位是 mode(模式),后30位是 size(大小) 即:MeasureSpec = mode + size
其中mode的值有三种,UNSPECIFIED,EXACTLY、AT_MOST,

模式 意义 对应
EXACTLY 精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Size match_parent
AT_MOST 最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值 wrap_content
UNSPECIFIED 无限制,View对尺寸没有任何限制,View设置为多大就应当为多大 不怎么用

在ViewGroup中的 MeasureSpec测量源码如下:

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:
            if (childDimension >= 0) {
                //如果子view有自己的尺寸,则使用自己的尺寸
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //当子View是match_parent,将父View的大小赋值给子View
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 如果子View是wrap_content,设置子View的最大尺寸为父View
                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是match_parent,父View的尺寸为子View的最大尺寸
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // 如果子View是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;
        }
        //通过Mode 和 Size 生成新的SpecMode 返回
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

注:这里只是给子View设置了MeasureSpec参数,真正的大小是在View中具体设置的,只是给子View做了一个限制。子View的测量模式由自身的LayoutParams和父View的MeasureSpec来决定。

回过头来看View.onMeasure()方法:就一行代码,但是有三个函数:
setMeasuredDimension(int measuredWidth, int measuredHeight) :该方法用来设置View的宽高
getDefaultSize(int size, int measureSpec): 该方法用来获取View默认的宽高
getSuggestedMinimumWidth(): 该方法用于获取Android:minWidth属性的值,如果没有则为0(如果有背景还需要判断背景与mMinWidth的大小,取大值)
这里面其实最重要的是getDefaultSize方法,对于AT_MOST和EXACTLY在View当中的处理是完全相同的,在我们自定义View时要对这两种模式做出处理。

  /**
  *   有两个参数size和measureSpec
  *   1、size表示View的默认大小,它的值是通过`getSuggestedMinimumWidth()方法来获取的,之后我们再分析。
  *   2、measureSpec则是我们之前分析的MeasureSpec,里面存储了View的测量值以及测量模式
  */
  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;
    }

ViewGroup除了测量自身,还需要测量子View的大小,ViewGroup中提供了对子View的测量方法:measureChildren(),在measureChildren中遍历所有子View,调用measureChild(),在measureChild中调用了View的measure()方法,让子View测量自身大小。

2、layout布局流程,layout()过程,对于View来说用来计算View的位置参数,对于ViewGroup来说,除了要测量自身位置,还需要测量子View的位置。其实layout最重要的在自定义ViewGroup时的重写,对其子类进行布局。

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;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //onLayout方法是一个空实现,我们在自定义View的时候关注重新这个onLayout方法即可,可以看看LinearLayout的onLayout方法
            onLayout(changed, l, t, r, b);
            //省略很多代码....
        }
    }

3、draw绘制流程:draw绘制流程就是绘制View的过程,整个过程可以分为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);

            // we're done...
            return;
        }
    }
    
    /**
    *  1.绘制View背景
    */
    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);
        }
    }
    
    /**
    * 3.绘制View的内容,该方法是一个空的实现,在各个业务当中自行处理。
    */
    protected void onDraw(Canvas canvas) {
    }
    
    /**
    * 4. 绘制子View。该方法在View当中是一个空的实现,在各个业务当中自行处理。
    *  在ViewGroup当中对dispatchDraw方法做了实现,主要是遍历子View,并调用子类的draw方法,一般我们不需要自己重写该方法。
    */
    protected void dispatchDraw(Canvas canvas) {

    }

4、Paint、Canvas、Path等相关内容可以看看: 扔物线的自定义View

你可能感兴趣的:(Android面试Android进阶(十五)-自定义View相关1)