Android 自定义简单View及获取xml自定义属性

作者: cnbzlj 发布时间: 2019-09-26 浏览: 187 次 编辑

一、前言

对于自定义View相信这是每一个初学者心里的痛,但开发久了,慢慢的你就会发现,其实自定义View并不难,看几篇基础文章,懂得了原理和流程套路,用的多了,实战多了,自然而然就掌握了。近期在时间上还算空余,所以打算在学习新的东西的同时,把旧的基础的东西也好好梳理一下,总不能捡一路丢一路吧。

二、自定义View

首先先总结一下一般自定义View的步骤,让我们在写的时候能有一个大致的方向:

1.继承一个View或者一个我们需要的View的子类,并添加构造方法

2.重写onMeasure方法

3.重写onDraw方法

4.在xml中自定义View属性

看了很多文章都是先设置View属性,这主要是看个人习惯吧,因为我在写自定义View的时候,有时候属性想的不够全面或者写的多余最后用不到,修改起来比较频繁,所以我就习惯先将需要的属性通过成员变量赋予固定的测试值,当自定义的view成型了,再把测试值替换成xml中设置的View属性值。

闲话不多说了,我们直接来拿一个小例子来从头到尾的实现一下吧,虽然没有实际用处吧,这种效果完全可以通过xml的shape实现,在这里就当是为了熟悉一下自定义View的流程吧,效果图如下:

这是我们自定义的View类,我们添加了不同参数的构造方法,并进行了相关初始化

public class MyCustomSimpleView extends View{


    //显示的文本
    String title;
    //字体的大小
    int wordSize;
    //字体的颜色
    int wordCorlor;
    //背景颜色
    int bgCorlor;


    //搜索栏出去左右两个半圆的长度
    int searchLength;
    //搜索栏的高度
    int searchHight;


    //搜索栏文本所占的区域
    Rect rect;
    //写字的笔
    Paint worldPaint;
    //画背景的笔
    Paint bgPaint;


    //在代码中直接new的时候,会调用一个参数构造方法
    public MyCustomSimpleView(Context context) {
        this(context, null);
    }


    //在xml中使用自定义控件的时候,不管有没有使用我们自己的自定义属性都会调用两个参数构造方法
    public MyCustomSimpleView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }


    //这个构造方法系统不会默认调用,需要我们自己主动调用
    public MyCustomSimpleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }


    private void init(Context context) {
        wordSize = 16;
        title = "搜索更多内容";
        wordCorlor = 0xFF6a6aff;//浅蓝
        bgCorlor = 0xFFbebebe;//浅灰


        worldPaint = new Paint();
        worldPaint.setTextSize(CommentUtils.sp2px(context, wordSize));
        worldPaint.setColor(wordCorlor);


        bgPaint = new Paint();
        bgPaint.setColor(bgCorlor);


        rect = new Rect();//通过rect我们要获取title内容的宽度和高度
        worldPaint.getTextBounds(title, 0, title.length(), rect);
    }
}

通常我们习惯写上三个构造方法,这三个构造方法的使用时机请参考代码的注释

我们要重写onMeasure方法,我们知道,在xml中我们的layout_width和layout_height可以是wrap_content、match_parent或者固定大小,可其中wrap_content并没有指定真正大小,可是我们要绘制的view是要有具体的宽度的,所以这就要求我们自己去测量尺寸提供给父View让其给我们提供期望大小的区域,为此系统给我们提供了MeasureSpec类,通过MeasureSpec类我们可以获取父view提供给我们的宽高尺寸和其所用的测量模式。

测量模式分为3种:

1.EXACTLY:提供的尺寸就是当前view所应该取的尺寸,一般设置了明确的固定大小或者match_parent

2.AT_MOST:提供的尺寸就是当前view所能取的最大尺寸,一般设置了wrap_content

3.UNSPECIFIED:父view对当前view的尺寸没有限制,当前view想要多大都可以,一般很少用到

这是我们的onMeasure方法,对于layout_width和layout_height的不同情况做了相应的处理

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthModel = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightModel = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        switch (heightModel) {
            case MeasureSpec.UNSPECIFIED:
            case MeasureSpec.AT_MOST:
                //文字高度+上下边距
                heightSize = rect.height() + getPaddingTop() + getPaddingBottom();
                //当高度为wrap_content时
                searchHight = rect.height();
                break;
            case MeasureSpec.EXACTLY:
                //当高度全屏或者固定尺寸时候
                searchHight = heightSize - getPaddingTop() - getPaddingBottom();
                break;;
        }
        switch (widthModel) {
            case MeasureSpec.UNSPECIFIED:
            case MeasureSpec.AT_MOST:
                //文字宽度+两侧半圆半径
                widthSize = rect.width() + getPaddingLeft() + getPaddingRight()
                        + searchHight/2 + getPaddingRight() + searchHight/2 + getPaddingLeft();
                //当宽度为wrap_content时,搜索栏宽度(不包括两边半圆)为文字的宽度
                searchLength = rect.width();
                break;
            case MeasureSpec.EXACTLY:
                //当宽度全屏或者固定尺寸时候,宽度-左右半圆宽度-左右边距
                searchLength = widthSize - (searchHight/2 + getPaddingRight() + searchHight/2 + getPaddingLeft()) - getPaddingLeft() - getPaddingRight();
                break;
        }
        setMeasuredDimension(widthSize, heightSize);
    }

重写onDraw方法,通过我们上面定义的画笔在canvas上绘制就行啦,其实不管多么复杂的画面,都是由一个个最基础的元素构成的,Android为我们提供了画线、画圆、画弧形、画矩形等等的方法,我们用到的时候直接去查看api开发文档就行了,不需要死记硬背那么多,同样本例中的绘制方法也不需要完全缕清,无非是找到需要的点的坐标罢了,跟对于自定义view的理解没有关系

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(worldPaint != null){
            //左侧加个半圆
            RectF rectF_left = new RectF(0, 0
                    , 2 * getPaddingLeft() + searchHight
                    , searchHight + getPaddingTop() + getPaddingBottom());
            canvas.drawArc(rectF_left, 90, 180, false, bgPaint);
            //画一个矩形
            canvas.drawRect(searchHight/2 + getPaddingLeft(), 0
                    , searchLength + 2*getPaddingLeft() + getPaddingRight() + searchHight/2
                    , searchHight + getPaddingTop() +getPaddingBottom(), bgPaint);
            //在画的矩形区域上写内容,在这里默认都是padding 上下内边距一致 左右内边距一致
            canvas.drawText(title
                    , getPaddingLeft() + getPaddingLeft() + searchHight/2
                    , (searchHight + getPaddingTop() + getPaddingBottom())/2 + rect.height()/2, worldPaint);
            //右侧加个半圆
            RectF rectF_right = new RectF(searchLength + 2 * getPaddingLeft()
                    , 0, searchLength + 2 * getPaddingLeft() + 2 * getPaddingRight() + searchHight
                    , searchHight + getPaddingTop() + getPaddingBottom());
            canvas.drawArc(rectF_right, -90, 180, false, bgPaint);
        }
    }

好了,我们的自定义View基本已经成型了,但是你会发现我们的属性都是写死的测试值,不用担心,用我们自定义的属性替换下就好啦。我们先在res/values目录下新建一个attrs.xml:

<resources>


    <!--最好是自定义View的类名,查看起来一目了然-->
    <declare-styleable name="MyCustomSimpleView">
        <attr name="wordSize" format="integer"/>
        <attr name="test" format="string"/>
        <attr name="wordCorlor" format="integer"/>
        <attr name="bgCorlor" format="integer"/>
    </declare-styleable>
</resources>

我们的xml布局文件

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:custom="http://schemas.android.com/apk/res-auto">


    <custom.com.mycustomviewdemo.widget.MyCustomSimpleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:layout_margin="20dp"
        custom:test="搜索更多内容"
        custom:wordSize="16"
        custom:wordCorlor="0xFF6a6aff"
        custom:bgCorlor="0xFFbebebe"
        />


</RelativeLayout>
注意需要在根目录引入我们的命名空间xmlns:custom=”http://schemas.android.com/apk/res-auto”,其中custom我们可以更改,没有具体名字要求

将我们自定义View里的测试值改成从xml属性中获取

private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        TypedArray array = context.getTheme().obtainStyledAttributes(attrs
                , R.styleable.MyCustomSimpleView
                , defStyleAttr, 0);
        title = array.getString(R.styleable.MyCustomSimpleView_test);
        wordSize = array.getInt(R.styleable.MyCustomSimpleView_wordSize, 16);
        wordCorlor = array.getInt(R.styleable.MyCustomSimpleView_wordCorlor, 0xFF6a6aff);//默认浅蓝
        bgCorlor = array.getInt(R.styleable.MyCustomSimpleView_bgCorlor, 0xFFbebebe);//默认浅灰
        //记得使用完销毁
        array.recycle();


        worldPaint = new Paint();
        worldPaint.setTextSize(CommentUtils.sp2px(context, wordSize));
        worldPaint.setColor(wordCorlor);


        bgPaint = new Paint();
        bgPaint.setColor(bgCorlor);


        rect = new Rect();
        worldPaint.getTextBounds(title, 0, title.length(), rect);
    }

三、总结

到此我已经基本介绍完自定义简单view的流程了,但其中涉及到的很多知识点,都没有具体的去深入介绍,充其量只是有个使用方法罢了,如果你感兴趣可以通过切入一个知识点,单独的再去查资料深入剖析一下。

本例源码点击下载