美文网首页
Android面试Android进阶(十五)-自定义View相关

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

作者: 肖义熙 | 来源:发表于2021-04-15 21:10 被阅读0次

    问:自定义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相关

          本文链接:https://www.haomeiwen.com/subject/xynclltx.html