自定义View,这一篇还不够!

1. 简介

View是Android中所有控件的基类,当系统内置的View无法满足我们的需求时,我们需要根据需求定制不同的View,完成一个自定义View大概需要如下几个步骤

  • 继承View(或者ViewGroup),重写构造方法
  • 根据需求判断是否需要自定义属性
  • 重写onMeasure( )方法来测量宽高
  • 根据需求是否需要重写onLayout( )来进行摆放
  • 重写onDraw( )方法进行绘制

2. View的构造方法

我们自定义View的第一步肯定是创建一个类继承View(ViewGroup)并重写构造方法,View有4种形式的构造方法,不同参数的构造方法分别对应不同的创建方式,下面我们来依次分析一下.

  • 只有一个参数的构造:通过代码初始化控件时被调用
    public MyView(Context context) {
        super(context);
    }
  • 有两个参数的构造:View被布局文件填充时调用
    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
  • 有三个参数的构造:多了一个自定义属性资源id的参数,通常让前两个构造调用第三个
    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
  • 四个参数的构造:API21才出现,一般不使用

3. 自定义属性

当我们使用系统内置的View时,可以直接使用这些View已定义好的属性,比如layout_width, layout_height,id,background… …,这些系统定义的所有属性保存在 /sdk/platforms/android-xx/data/res/values/attrs.xml 这个文件中.
当我们自定义View时,只用系统提供的属性可能无法实现想要的效果,为了扩展性的考虑,经常需要自己来定义一些属性

3.1 创建属性资源文件

自定义属性的第一步就是要创建属性的资源文件,在res/values目录下创建名为attrs.xml的文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyView">
        <attr name="myText" format="string"/>
        <attr name="myColor" format="color"/>
    </declare-styleable>
</resources>

然后在布局文件中要使用自定义属性,引入命名空间xmlns:David="http://schemas.android.com/apk/res-auto",res-auto表示自动查找也可替换为应用程序的包名,加上引入后就可以使用我们自定义的属性了:

    <com.david.knowledge_planet.view.MyView
        David:myText="Test"
        David:myColor="#ffc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

最后在自定义View的构造方法中获取属性值:

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取自定义属性值
        TypedArray typedArray = context.getTheme().
                obtainStyledAttributes(attrs, R.styleable.MyView, defStyleAttr, 0);
        myText = typedArray.getString(R.styleable.MyView_myText);
        myColor = typedArray.getColor(R.styleable.MyView_myColor, Color.RED);
        mySize = typedArray.getDimension(R.styleable.MyView_mySize,100);
        //回收资源
        typedArray.recycle();
        paint = new Paint();
        paint.setColor(myColor);
        paint.setTextSize(mySize);
    }

3.2 format属性值的类型

format一共支持11种类型

  • dimension:尺寸值
  • color:颜色值
  • string:字符串
  • boolean:布尔值
  • enum:枚举值
  • flag:位或运算
  • float:浮点值
  • fraction:百分数
  • integer:整型值
  • reference:资源id
  • 混合类型:定义时可以指定多种类型

3.3 AttributeSet和TypedArray

AttributeSet是一个属性的集合,内部是一个XML解析器,用来解析控件中的所有属性,并以key-value的键值对形式维护起来.但通过AttributeSet获取的属性值就是将布局文件中的原始值取出来,如果我们要使用的话很麻烦.
TypedArray提供了一系列获取不同类型属性的方法,可以直接得到我们想要的数据类型,不像AttributeSet获取属性值后还要一个一个的处理才能得到具体数据,TypedArray使用完毕后要调用recycle()进行回收.

4. measure流程

View的绘制流程是从ViewRoot的performTraversals方法开始的,经过measure,layout和draw过程将一个View绘制出来,其中measure用来测量View的宽和高,layout用来确定View在父容器中的摆放位置,draw负责将View绘制在屏幕上. performTraversals的大致流程如下图:
performTraversals工作流程
如图所示,performTraversals会依次调用performMeasure,performLayout和performDraw来完成顶级View的measure,layout和draw这三个流程.
在performMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中会对所有的子元素进行measure过程,这时measure流程就从父容器传递到子元素中了,完成了一次measure过程.

4.1 onMeasure方法

创建一个View(执行构造方法)时不需要测量控件的大小,只有将这个view放入一个容器(父容器)时才需要测量,而这个测量方法就是父控件唤起调用的;当父控件要放置该控件时,父控件会调用子控件的onMeasure方法询问子控件需要多大的空间,然后传入两个参数 widthMeasureSpecheightMeasureSpec ,这两个参数就是父控件告诉子控件可获得的空间以及这个空间的约束条件,子控件通过这些条件就能正确测量自身的宽高了.

4.2 MeasureSpec

上面说到MeasureSpec是由父控件传递给子控件的,系统会将View的LayoutParams根据父控件所施加的规则转换成对应的MeasureSpec,然后根据这个MeasureSpec来测量View的宽高;
MeasureSpec代表一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(某种测量模式下的规格大小).

    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /**
         * 父容器不对View有限制,要多大给多大
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * 父容器已检测出View所需要的大小,这时View的最终大小就是SpecSize所指定的值
         */
        public static final int EXACTLY = 1 << MODE_SHIFT;

        /**
         * 父容器会给View尽可能大的尺寸,但不能超过SpecSize
         */
        public static final int AT_MOST = 2 << MODE_SHIFT;

        /**
         * 根据所提供的大小和模式创建一个测量规范
         */
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        /** 
         *   从所提供的测量规范中获取测量模式
         */
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        /**
         * 从所提供的测量规范中获取尺寸
         */
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
    }

通过源码可以看出,MeasureSpec就是通过将SpecModeSpecSize打包成的一个int值,而一个MeasureSpec可以通过解包的形式获取原始的SpecModeSpecSize.

4.3 ViewGroup的measure过程

View的onMeasure是由父控件调用的,而所有的父控件都是ViewGroup的子类,ViewGroup又是一个抽象类,里面只有一个抽象方法onLayout,这个方法的作用是摆放所有的子控件,但是在摆放之前,必须要获取子控件的大小,可是ViewGroup并没有重写View的onMeasure方法,那ViewGroup是如何测量子控件大小的呢?原来ViewGroup单独提供了3个用来测量子控件的方法,下面让我们依次来看一下

    /**
     * 遍历ViewGroup中的所有子控件,调用measureChild测量宽高
     */
    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);
            }
        }
    }

    /**
     * 测量某个子控件的宽高
     */
    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
	//获取子控件的宽高约束规则
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

   /**
     *  测量某个child的宽高,考虑margin值
     */
    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);
    }

子控件的宽高约束规则是由父控件调用getChildMeasureSpec方法生成的.而子控件MeasureSpec的创建与父控件的MeasureSpec和子元素的本身的LayoutParams有关,还受View的margin及padding影响.

4.4 View的measure过程

View的measure过程由measure方法来完成,measure方法是一个final类型的方法,所以子类不可以重写. measure方法中会去调用View的onMeasure方法

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
         getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec));
    }

  /**
     * 未宽度取一个最小值
     */
    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

    /**
     * 获取默认的宽高值
     */
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
  • 如果View的宽高 模式为 EXACTLY (具体的size ),最终宽高就是这个size值;
  • 如果View的宽高模式为EXACTLY (填充父控件 ),最终宽高将为填充父控件;
  • 如果View的宽高模式为AT_MOST (包裹内容),最终宽高也是填充父控件;

所以当我们自定义View时,如果宽高是wrap_content时需要重写onMeasure方法,不然宽高默认就是填充父控件了.
最后要记得调用setMeasuredDimension方法来保存测量的宽高值

5. layout过程

layout过程和measure过程相比就简单了很多,layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置.

    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(changed, l, t, r, b);
	... ...

通过源码可以看下大致流程:首先会通过setFrame方法来设定View的四个顶点的位置,View的四个顶点一旦确定,那么View在父容器中的位置也就确定了.接着会调用onLayout方法,这个方法是父容器用来确定子元素的位置的,是一个抽象方法,具体的实现和布局有关,所以View和ViewGroup均没有真正实现.

6. draw过程

draw过程就比较简单了,就是将View绘制到屏幕上,View的绘制流程遵循如下几步:

  • 绘制背景 background.draw(canvas)
  • 绘制自己 (onDraw)
  • 绘制children(dispatchers)
  • 绘制装饰 (onDrawScrollBars)

7. 自定义View的分类

自定义View大体上可分为两大类,每个大类又可以分为两个小类

  • 继承View或ViewGroup
    • 继承View重写onDraw方法,需要自己处理wrap_content和padding.
    • 继承ViewGroup,要在onMeasure和onLayout中考虑自身的padding和子元素的margin属性.
  • 继承现有的控件TextView或LinearLayout

View中如果有线程或者动画需要停止时,在onDetachedFromWindow中完成;当包含此View的Activity退出或者当前View被remove时,onDetachedFromWindow方法会被回调.

详情请拜读刚哥的《Android开发艺术探索》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值