自定义View从实现到原理(一)

自定义View在我自学Android开发中一直感觉是高手才能掌握的知识,因为情况太多而且界面看起来有很复杂炫酷。但是自定义View同样遵循着某些规则,这篇博客我就从这个规则入手,先实现View,在涉及原理。


自定义View实现的步骤

  • 自定义View从实现到原理(一)
  • 自定义View的属性
  • 在View构造方法中获得我们自定义的属性
  • 重写onMeasure()方法,对View的尺寸大小进行修改
  • 重写onDraw()方法,进行想要的样式绘制


自定义View的属性

Android系统的空间是以android:*样式开头的(android:layout_width等),这些都是系统自带的属性,那么我们自定义的View,也同样应该拥有自己的属性。那么首先,我们在res/value目录下新建attrs.xml文件,用于定义我们的属性:

<?xml version="1.0" encoding="utf-8"?>

<!--自定义TestView的attrs属性文件-->
<resources>
    <!--设置styleable的name,在构造函数中会调用-->
    <declare-styleable name="TestView">
        <!--自定义View的属性-->
        <attr name="test_color" format="color"/>
        <!--format是定义的属性的类型,类似的还有boolean等-->

    </declare-styleable>
</resources>

为了方便起见我们在这里只是定义了颜色。

在View构造方法中获得我们自定义的属性

新建TestView继承于View,我们可以看到TestView有四个构造函数,各个构造函数的适用情形我会在代码块中进行注释解释:

/**
     * View和ViewGroup可以利用代码直接new对象时,这时候会调用第一个构造函数,
     * 生成对象后利用内部提供的属性设置方法进行属性设置
     *
     * @param context 传入的当前位置
     */
	public TestView(Context context) {
        super(context);
    }

	 /**
     * 用于加载xml布局文件时调用,通常自定义控件的自定义属性的读取需要用到这个构造函数
     *
     * @param context 传入当前的位置
     * @param attrs   styleable传入位置
     */
    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

 	/**
     * 一般系统不会主动调用,需要手动调用,
     * 基本上是用不到的,以后我用到了再写吧
     * 
     * @param context      芜湖
     * @param attrs        芜湖
     * @param defStyleAttr 这个位置是需要手动传入的
     */
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

	/**
     * 如果第三个参数为0或者没有定义defStyleAttr时,第四个参数才起作用,
     * 我更用不到了,就不浪费时间了
     *
     * @param context      继续芜湖
     * @param attrs        芜湖
     * @param defStyleAttr 芜湖
     * @param defStyleRes  一个style的资源引用
     */
    public TestView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
        super(context, attrs, defStyleAttr, defStyleRes);

    }

介绍完了构造函数这一部分,根据上面介绍的各个构造函数的用法,我们选择第二种,那么就要在xml中声明这一个自定义的View:

<com.example.day1.View.TestView
        xmlns:testView="http://schemas.android.com/apk/res-auto"
        android:id="@+id/testView"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_below="@id/testImage"
        android:layout_marginTop="20dp"
        android:layout_centerHorizontal="true"
        testView:test_color="@android:color/holo_blue_bright" />

这个xmlns:testView="http://schemas.android.com/apk/res-auto"是必备的,只有这样才能调用我们之前自定义的View属性attrs.xml中的test_color。

在自定义的View-TestView中获取attrs中的属性:

/**
     * 定义颜色
     */
    private int testColor = Color.GREEN;

    /**
     * 定义画笔
     */
    private Paint testPaint = new Paint();
    
    /**
     * View和ViewGroup可以利用代码直接new对象时,这时候会调用第一个构造函数,
     * 生成对象后利用内部提供的属性设置方法进行属性设置
     *
     * @param context 传入的当前位置
     */
    public TestView(Context context) {
        this(context, null);
    }

    /**
     * 用于加载xml布局文件时调用,通常自定义控件的自定义属性的读取需要用到这个构造函数
     *
     * @param context 传入当前的位置
     * @param attrs   styleable传入位置
     */
    public TestView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
        //获取我们自定义的样式属性
        TypedArray testTypeArray = context.obtainStyledAttributes(attrs, R.styleable.TestView);
        //提取样式属性中的testcolor属性,如果没有,则设置卫Color.RED红色
        testColor = testTypeArray.getColor(R.styleable.TestView_test_color, Color.RED);
        //获取资源后及时收回
        testTypeArray.recycle();

		testPaint.setColor(testColor);
    }

如上代码所示获取相应的属性,在获取之后一定要记得回收TypeArray变量。

重写onMeasure()方法,对View的尺寸大小进行修改

在重写onMeasure()方法之前,我们首先要了解一下MeasureSpec的specmode属性,一共有三种类型:

specmode属性

含义

AT_MOST

最大模式,表示子布局限制在一个最大值内,一般是wrap_content类型

EXACTLY

一般是设置了明确的值或者是match_parent类型

UNSPECIFIED

表示子布局想多大就多大,没有限制,一般用于系统内部的测量

现在看这三个属性是不是有点迷茫,我一开始看的时候根本不知道这是个什么东西,我们回头来看之前写过的xml布局文件:

<com.example.day1.View.TestView
        ****
        android:layout_width="200dp"
        android:layout_height="200dp"
        ****/>

之前我们在布局文件中写的是定死大小的TestView,当我们把他设置为wrap_content时,就会铺满整个屏幕,和设置为match_parent的效果是一样的了,这就不符合我们的预期了,而在实际开发过程中,如果布局文件中设置为wrap_content时,我们一般都会在自定义View的代码中对宽高进行处理赋值

/**
     * 自定义View测量以及设置宽高
     *
     * @param widthMeasureSpec  传入的View宽度信息
     * @param heightMeasureSpec 传入的View高度信息
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取布局文件的宽度模式(wrap_content等)
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        //获取布局文件的高度模式
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        //获取布局文件的宽度尺寸--返回的是像素大小
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        //获取布局文件的高度尺寸
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        //判断布局文件宽高的模式是否为wrap_content
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            //如果是的话则将宽高设置为150px
            setMeasuredDimension(150, 150);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            //如果只有宽度为wrap_content的话,高度设置为xml设好的,宽度为150px
            setMeasuredDimension(150, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            //同上
            setMeasuredDimension(widthSpecSize, 150);
        }
    }

这个时候我们将xml改为:

<com.example.day1.View.TestView
        ****
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        ****/>

效果并不是铺满全屏了,而是150px的大小。

重写onDraw()方法,进行想要的样式绘制

自定义View的onDraw()方法,负责的就是画出自己想要的View的具体模样,刚开始是这样:

/**
     * 绘制View部份
     *
     * @param canvas 画板
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

这个canvas,就像上面注释写的那样,本质上来说是个画板,我们的View绘制就是在canvas的基础上来进行的,我们这次知识介绍最简单的部份:

canvas的方法有很多,可以直接调用它来进行绘制正方形(drawRect方法),绘制bitmap(drawBitmap方法)等等,这一部分我们后面再看,接下来就简单的看看绘制正方形:

/**
     * Draw the specified Rect using the specified paint. The rectangle will be filled or framed
     * based on the Style in the paint.
     *
     * @param left The left side of the rectangle to be drawn
     * @param top The top side of the rectangle to be drawn
     * @param right The right side of the rectangle to be drawn
     * @param bottom The bottom side of the rectangle to be drawn
     * @param paint The paint used to draw the rect
     */
    public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint) {
        super.drawRect(left, top, right, bottom, paint);
    }

这就是绘制正方形drawRect的源码,从这里面我们可以看到,一共需要五个参数,左上右下以及画笔,看完了源码我们直接应用:

canvas.drawRect(0, 0, getWidth(), getHeight(), testPaint);

很简单的一行代码,就可以绘制出一个正方形,接下来贴出全部的代码以及最终的样式:

/**
 * 自定义的TestView
 */
public class TestView extends View {

    /**
     * 定义颜色
     */
    private int testColor = Color.GREEN;

    /**
     * 定义画笔
     */
    private Paint testPaint = new Paint();

    /**
     * View和ViewGroup可以利用代码直接new对象时,这时候会调用第一个构造函数,
     * 生成对象后利用内部提供的属性设置方法进行属性设置
     *
     * @param context 传入的当前位置
     */
    public TestView(Context context) {
        this(context, null);
    }

    /**
     * 用于加载xml布局文件时调用,通常自定义控件的自定义属性的读取需要用到这个构造函数
     *
     * @param context 传入当前的位置
     * @param attrs   styleable传入位置
     */
    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        //获取我们自定义的样式属性
        TypedArray testTypeArray = context.obtainStyledAttributes(attrs, R.styleable.TestView);
        //提取样式属性中的testcolor属性,如果没有,则设置卫Color.RED红色
        testColor = testTypeArray.getColor(R.styleable.TestView_test_color, Color.RED);
        //获取资源后及时收回
        testTypeArray.recycle();

        testPaint.setColor(testColor);
    }

    /**
     * 一般系统不会主动调用,需要手动调用,
     * 我现在基本上是用不到的,以后我用到了再写吧
     *
     * @param context      芜湖
     * @param attrs        芜湖
     * @param defStyleAttr 这个位置是需要手动传入的
     */
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * 如果第三个参数为0或者没有定义defStyleAttr时,第四个参数才起作用,
     * 我更用不到了,就不浪费时间了
     *
     * @param context      继续芜湖
     * @param attrs        芜湖
     * @param defStyleAttr 芜湖
     * @param defStyleRes  一个style的资源引用
     */
    public TestView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    /**
     * 自定义View测量以及设置宽高
     *
     * @param widthMeasureSpec  传入的View宽度信息
     * @param heightMeasureSpec 传入的View高度信息
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取布局文件的宽度模式(wrap_content等)
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        //获取布局文件的高度模式
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        //获取布局文件的宽度尺寸--返回的是像素大小
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        //获取布局文件的高度尺寸
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        //判断布局文件宽高的模式是否为wrap_content
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            //如果是的话则将宽高设置为150px
            setMeasuredDimension(150, 150);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            //如果只有宽度为wrap_content的话,高度设置为xml设好的,宽度为150px
            setMeasuredDimension(150, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            //同上
            setMeasuredDimension(widthSpecSize, 150);
        }
    }

    /**
     * 绘制View部份
     *
     * @param canvas 画板
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(0, 0, getWidth(), getHeight(), testPaint);
    }
}

只是个很简单的例子,记录我学习的进展,下一篇会介绍View的分发机制以及View的工作流程等,从View的实现入手,真正掌握所有关于自定义View的知识。