Android自定义View基础及绘制流程

Android自定义View工作原理关于measure、layout、draw详解

自定义view在日常开发和面试中出现频率很高,那么什么是自定义View,为何需要,其怎么来实现呢?在拿到一个UI效果时,对于如何实现,时常会不知如何下手。自定义View系列文章将从源码角度解析其原理,并列举实际使用。
由于View的工作流程较多,这里笔者先进行总结方便记忆。

在这里插入图片描述

一 什么是自定义View,为何需要

笔者认为除了系统给的View以外,开发者自己写的都叫自定义View。比如写了个MyTextView继承TextView什么都不做,这也是个自定义View。

为何需要?由于系统给的View不能满足我们的实际需求,或者不方便我们的实际应用。

二 自定义View分类

  • 半自定义:继承一个系统的View,加载一个xml布局文件。一般没有重写onMeasure,onLayout,onDraw三个方法
  • 全自定义:重写onMeasure,onLayout,onDraw三个方法,开发者自己去绘制了view

首先强调分类只是笔者根据实际开发的使用习惯进行以及方便讲解进行的分类,有点也按照自定义View和自定义ViewGroup进行分类。笔者认为自定义View本身并没有什么分类,如上述所讲只要是我们自己写的view,无论你是加载xml布局文件还是重写onMeasure等方法或者两者均有都可以叫做自定义View。

半自定义
这种情况比较简单就是继承系统的View后,重写构造方法,然后加载一个xml布局文件。相信大家经常使用,比如一些共通的组件,我们会进行封装,供全局使用。对于这种实现方式不做重点讨论,只介绍其中几个重要方法。

构造方法的调用:

// Java代码里面new调用
 public MyView(Context context) {
        super(context);
    }

// .xml里声明调用
// 自定义属性是从AttributeSet参数传进来的
    public  MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

// 不会自动调用,一般是在第二个构造函数里主动调用
// View有style自定义属性时才需要使用
    public  MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //API21之后才有,不会自动调用,一般是在第二个构造函数里主动调用,可使用自定属性
    public  MyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

自定义View属性的应用:

  1. 通过<declare-styleable>为自定义View添加属性
  2. 在xml中为相应的属性声明属性值
  3. 在运行时(一般为构造函数)获取属性值
  4. 将获取到的属性值应用到View

三 View的显示流程

我们通常说自定义View要重写onMeasure,onLayout,onDraw三个方法,为何要重写这三个方法呢?我们得从View的加载流程谈起。

首先需要明白DecorView。DecorView是整个界面的最顶层View,本质是FrameLayout。一般情况下里面是一个LinearLayout,这个LinearLayout又包含两个FrameLayout,分别显示标题和内容。其中显示内容的FrameLayout,其ID为 android.R.id.content。我们的setContentView就是设置的就是显示内容的View。
在这里插入图片描述

通过以上介绍我们知道了我们的布局文件最终交给了DecorView,那么它是怎么显示到界面上来的呢?
这里就又要引入一个类ViewRoot的实现类ViewRootImpl。
在ActivityThread中,Activity对象被创建的时候,会将DecorView添加到window中,同时会创建ViewRootImpl对象。通过ViewRootImpl将DecorView和window建立了关联,可以理解为ViewRootImpl是帮助window管理View的。

root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams,panelParentView);

通过源码可以看到创建ViewRootImpl对象的时候,将DecorView传了进去,
那么ViewRootImpl拿到后去做了什么?通过查看源码,发现其正是去对传入的DecorView做了一系列的测量,布局,绘制的过程。
其View的绘制流程是从ViewRoot的performTraversals方法开始。performTraversals大致流程:
在这里插入图片描述

performTraversals依次调用performMeasure、performLayout、performDraw方法。这三个方法完成顶级View的measure、layout和draw三大流程。其中performMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中会对所有的子元素进行measure过程,这时measure流程就从父容器传递到了子元素中,这样就完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复就完成了整个View树的遍历。其它两个方法同理。
通过以上我们知道为何要重写这三个方法了吧。
那么这三个方法都干了什么?

  • measure:测量确定宽高
  • layout:布局确定位置
  • draw:绘制

这里思考一个问题为何要有测量的过程呢?
在常规的h5等其它布局当中,我们一般都指定了控件的宽高,但是安卓提供了一种优雅的布局,有warp_content自适应,填充满父控件等。那么就需要反复的计算才能知道其具体大小了。

四 理解MeasureSpec

从名字上看像“测量规格”,通过源码发现MeasureSpec参与了View的measure过程。
MeasureSpec代表一个32位int值,高2位代表SpecMode->测量模式,低30位代表SpecSize–>规格大小

4.1 SpecMode

父容器测量模式和子View的大小关系:(后续会重源码角度讲解为何是这样的关系)
在这里插入图片描述

4.2 MeasureSpec和LayoutParams的对应关系

通过MeasureSpec的概念知道了,其包含了测量模式和大小。那么View的MeasureSpec是怎么生成的呢?

  • 顶级View(DecorView):MeasureSpec由窗口尺寸和自身的LayoutParams决定
  • 普通View:MeasureSpec由父容器的MeasureSpec和自身LayoutParams决定

4.3 普通View的MeasureSpec确定过程

我们知道子View的measure由ViewGroup传递而来,先查看ViewGroup当中其中一个测量子元素方法源码

class ViewGroup:

 protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        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);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

可以看到父容器的测量规格和子元素的layoutparams被当参数传入getChildMeasureSpec,该方法的源码:

class ViewGroup:
 /**
     *
     *  
     * @param spec 父控件测量规格
     * @param padding 父控件里已经占用的大小(可以看measureChildWithMargins调用的时候传入的值即可理解)
     * @param childDimension 子view的LayoutParams里的尺寸
     * @return 子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) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
         // 当父控件的测量模式是精确模式--有精确尺寸
            if (childDimension >= 0) {
             //如果child的布局参数有固定值比如"layout_height" = "10dp",那么子控件的测量大小就是10dp测量模式EXACTLY
           
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                //如果child的布局参数是"match_parent",占满父控件,其测量大小父控件大小,测量模式EXACTLY
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            
                // Child wants to determine its own size. It can't be
                // bigger than us.
                //如果是WRAP_CONTENT,即想要自己确定,那么测量模式就是AT_MOST,大小父控件(最大值)
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
        //父控件最大模式,父控件自己不知道大小,但不能超过size
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                //child能确定自己大小,那么child的测量模式肯定就是EXACTLY精准,测量大小也是child自己的大小
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                //child想占满父控件,child的测量大小就是父的size,而且最大不能大于这个,其测量模式就是AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                //child自己也不知道大小,其测量大小最大值就是父的size,测量模式就是AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
        //父控件不确定
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                //子控件有自己的大小,既然父控件不确定,那么其大小就将就子控件了。
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //子控件希望充满父控件,但是父控件自己的大小也不确定。这个时候就测量模式就不能确定,测量大小就先赋值0
            //sUseZeroUnspecifiedMeasureSpec是一个常量其值为0
            
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                        //子控件也不能确定,测量模式UNSPECIFIED,测量大小就先赋值0
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

总结起来其对应关系如下:横着的是parentSpecMode 父控件的测量模式,竖直的是child的LayoutParams

parentSpecMode 和childLayoutParamsEXACTLYAT_MOSTUNSPECIFED
dp/pxEXACTLY/childSizeEXACTLY/childSizeEXACTLY/childSize
match_parentEXACTLY/parentSizeAT_MOST/parentSizeUNSPECIFED/0
wrap_contentAT_MOST/parentSizeAT_MOST/parentSizeUNSPECIFED/0

五 坐标系

要理解自定义View,我们还需要掌握android的坐标系

  • View通过view.getxxx()函数获取的位置是相对父控件
  getTop();      //获取子View左上角距父View顶部的距离
  getLeft();      //获取子View左上角距父View左侧的距离
  getBottom();    //获取子View右下角距父View顶部的距离
  getRight();     //获取子View右下角距父View左侧的距离
  • MotionEvent中 get()和getRaw()
//get() :触摸点相对于其所在组件坐标系的坐标
 event.getX();       
 event.getY();
 //getRaw() :触摸点相对于屏幕坐标系的坐标
 event.getRawX();    
 event.getRawY();

图书如下:(其中getBottom和getRight都是以子View的右下角为起始点)
在这里插入图片描述
上面就是自定义View的基础知识了,在后面的章节当中会解析measure,layout,draw的过程Android自定义View工作原理关于measure、layout、draw详解

参考:<<Android开发艺术探索>>

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值