自己动手,丰衣足食 —— 学习自定义View(一)

自己动手,丰衣足食 —— 学习自定义View(一)

前言

我们经常在做项目的时候遇到这样的情况,客户提出需求,UI把设计稿拿出来,你发现直接用现成的开源库好像不行哎,多多少少有些不同。这时候你就会想:要是能自己画一个出来就好了。
所以说:自己写出来的View最靠谱,最灵活,你想让它什么样它就是什么样,前提是你要能画出来…
这也是为什么自定义View的知识如此重要的原因了,所以,打算系统地总结下这方面的知识。
首先,我们先由一个最简单的同心圆开始,例子简单,主要是归纳下自定义View的步骤,为以后更复杂的View打下基础。
好了,废话少说,先看一下这次View的效果,见下图:
自定义View的效果图
(图1)
这次我们要完成的就是上图中的带进度条的同心圆部分。

自定义View的大致步骤:

  • 自定义属性;
  • 创建自定义View的类,继承自View或其它继承自View的控件;
  • 添加类的构造函数(在构造函数中,我们要获取之前自定义的属性);
  • 重写父类的一些方法,例如onDraw,OnMeasure等.

下面结合代码具体来看一下:

1.在res/values下面新建一个attrs.xml,在里面我们可以定义一些View要用到属性。

有人就问了,这些属性有什么用呢?举个例子,我们平时用到的控件有许多属性,这样我们可以根据实际需要控制它的长宽,颜色等等,这样可以提高View的重用性,避免频繁修改代码。
首先确认一下本次需要要到的属性:

    /**
     * 圆环的颜色
     */
    private int roundColor;

    /**
     * 圆环进度的颜色
     */
    private int roundProgressColor;

    /**
     * 内圆的颜色
     */
    private int roundInsideColor;

    /**
     * 进度百分比的字符串的颜色
     */
    private int textProgressColor;

    /**
     * 标题文字的字符串的颜色
     */
    private int textTitleColor;

   /**
     * 标题文字的字符串
     */
    private String textTitle;

    /**
     * 进度百分比的字符串的字体
     */
    private float textProgressSize;

    /**
     * 标题文字的字符串的字体
     */

    private float textTitleSize;

    /**
     * 圆环的宽度
     */
    private float roundWidth;

    /**
     * 最大进度
     */
    private int max;

    /**
     * 当前进度
     */
    private int progress;
    /**
     * 是否显示中间的进度
     */
    private boolean textIsDisplayable;

    /**
     * 进度的风格,实心或者空心
     */
    private int style;

    public static final int STROKE = 0;
    public static final int FILL = 1;

然后根据上面的内容写出attrs.xml:

<?xml version="1.0" encoding="UTF-8"?>
<resources>
  <declare-styleable name="RoundProgressBar">
        <attr name="roundColor" format="color"/>
        <attr name="roundProgressColor" format="color"/>
        <attr name="roundInsideColor" format="color"/>
        <attr name="roundWidth" format="dimension"></attr>
        <attr name="textProgressSize" format="dimension" />
        <attr name="textProgressColor" format="color" />
        <attr name="textTitle" format="string" />
        <attr name="textTitleSize" format="dimension" />
        <attr name="textTitleColor" format="color" />
        <attr name="max" format="integer"></attr>
        <attr name="style">
            <enum name="STROKE" value="0"></enum>
            <enum name="FILL" value="1"></enum>
        </attr>
  </declare-styleable>
  ...
</resources>  

declare-styleable 标签后的name是此styleable的名字,attr 标签后的name是属性的名字,format类似于值的类型,具体可查看这个链接

2.创建一个自定义View的类,它必须继承自View或者其它继承自View的控件。

...

public class RoundProgressBar extends View {
    
    

public RoundProgressBar(Context context) {
        this(context, null);
    }

    public RoundProgressBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RoundProgressBar(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

...
        }

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

...
}
...

}

可以看到,我们创建好以后,重写了它的三个构造方法,并且重写了onDraw这个方法,至于干什么我们下一步再说。

3.在构造方法中获取自定义的属性。

我们知道,构造方法是在这个类对象刚被创建的时候被调用的,那么这3个构造方法我们需要用哪个呢?
第一个是在正常创建类对象时被调用的:

RoundProgressBar roundProgressBar = new RoundProgressBar(this);

第二个是在xml里添加一个View:

<com.customview.RoundProgressBar
     android:layout_width="120dp"
     android:layout_height="120dp"  />

里面添加的属性会被存放在AttributeSet参数里。
前两个方法都会在某些情况下被系统自动调用,而第三个方法则需要我们实现调用。而且由代码中可以看出,第三个方法是被第二个方法调用,第二个方法又是被第一个方法调用。所以,我们只要在第三个方法中获取自定义属性,就能确保自定义属性的获取。关于第三个构造方法的其它高级技巧,这里就不再深入了。

public RoundProgressBar(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
...
        TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.RoundProgressBar);

        //获取自定义属性和默认值
        roundColor = mTypedArray.getColor(R.styleable.RoundProgressBar_roundColor, Color.RED);
        roundProgressColor = mTypedArray.getColor(R.styleable.RoundProgressBar_roundProgressColor, Color.GREEN);
        roundInsideColor = mTypedArray.getColor(R.styleable.RoundProgressBar_roundInsideColor, Color.TRANSPARENT);
        textTitle = mTypedArray.getString(R.styleable.RoundProgressBar_textTitle);
        textProgressColor = mTypedArray.getColor(R.styleable.RoundProgressBar_textProgressColor, Color.GREEN);
        textTitleColor = mTypedArray.getColor(R.styleable.RoundProgressBar_textTitleColor, Color.GREEN);
        textProgressSize = mTypedArray.getDimension(R.styleable.RoundProgressBar_textProgressSize, 15);
        textTitleSize = mTypedArray.getDimension(R.styleable.RoundProgressBar_textTitleSize, 15);
        roundWidth = mTypedArray.getDimension(R.styleable.RoundProgressBar_roundWidth, 5);
        max = mTypedArray.getInteger(R.styleable.RoundProgressBar_max, 100);
        textIsDisplayable = mTypedArray.getBoolean(R.styleable.RoundProgressBar_textIsDisplayable, true);
        style = mTypedArray.getInt(R.styleable.RoundProgressBar_style, 0);

        mTypedArray.recycle();
    }

可以看到,我们先是调用context.obtainStyledAttributes(attrs,R.styleable.RoundProgressBar)来获取TypedArray,然后从TypedArray获取我们定义的属性。
另外,获取属性的方法,第一个参数就是我们自定义的属性,第二个参数是一个默认值。

4.接下来,得到了属性值,我们就可以在onDraw里把自定义View描画出来了。

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        /**
         * 画外层的圆环
         */
        int centre = getWidth() / 2; //获取圆心的x坐标
        int radius = (int) (centre - roundWidth / 2); //圆环的半径
        int radiusInside = (int) (centre - roundWidth); //内圆的半径
        paint.setColor(roundColor); //设置圆环的颜色
        paint.setStyle(Paint.Style.STROKE); //设置空心
        paint.setStrokeWidth(roundWidth); //设置圆环的宽度
        paint.setAntiAlias(true);  //消除锯齿
        canvas.drawCircle(centre, centre, radius, paint); //画出圆环

        /**
         * 画内层的圆
         */
        paint.setColor(roundInsideColor);
        paint.setStyle(Paint.Style.FILL);
        paint.setStrokeWidth(radiusInside);
        paint.setAntiAlias(true);
        canvas.drawCircle(centre, centre, radiusInside, paint);

        /**
         * 画进度百分比
         */
        paint.setStrokeWidth(0);
        paint.setColor(textProgressColor);
        paint.setTextSize(textProgressSize);
        paint.setTypeface(Typeface.DEFAULT_BOLD); //设置字体
        int percent = (int) (((float) progress / (float) max) * 100);  //中间的进度百分比,先转换成float在进行除法运算,不然都为0
        float textWidth = paint.measureText(percent + "%");   //测量字体宽度,我们需要根据字体的宽度设置在圆环中间

        if (textIsDisplayable && percent != 0 && style == STROKE) {
            canvas.drawText(percent + "%", centre - textWidth / 2, centre - radiusInside/3 + textProgressSize / 2, paint); //画出进度百分比
        }

        /**
         * 画标题
         */
        paint.setStrokeWidth(0);
        paint.setColor(textTitleColor);
        paint.setTextSize(textTitleSize);
        paint.setTypeface(Typeface.DEFAULT_BOLD); //设置字体
        textWidth = paint.measureText(textTitle);   //测量字体宽度,我们需要根据字体的宽度设置在圆环中间

        if (textIsDisplayable  && style == STROKE) {
            canvas.drawText(textTitle, centre - textWidth / 2, centre + radiusInside/3 + textProgressSize / 2, paint); //画出进度百分比
        }

        /**
         * 画圆弧 ,画圆环的进度
         */

        //设置进度是实心还是空心
        paint.setStrokeWidth(roundWidth); //设置圆环的宽度
        paint.setColor(roundProgressColor);  //设置进度的颜色
        RectF oval = new RectF(centre - radius, centre - radius, centre
                + radius, centre + radius);  //矩形用于定义的圆弧的形状和大小的界限

        switch (style) {
            case STROKE: {
                paint.setStyle(Paint.Style.STROKE);
                canvas.drawArc(oval, 0, 360 * progress / max, false, paint);  //根据进度画圆弧
                break;
            }
            case FILL: {
                paint.setStyle(Paint.Style.FILL_AND_STROKE);
                if (progress != 0)
                    canvas.drawArc(oval, 0, 360 * progress / max, true, paint);  //根据进度画扇形
                break;
            }
        }

    }

首先要说明一下关于View的坐标系知识,View相对于整个屏幕左上角的位置坐标叫做绝对坐标,而相对于父控件的位置坐标叫做相对坐标,以下如不说明,使用的都是相对坐标。
关于绘图的具体细节就不深入了,感兴趣的同学可以自己画图算下坐标和长度以及相关的API。

这样,这个自定义的RoundProgressBar 就算是写好了,那么我们怎么在自己写的代码里使用它呢?
跟平常一样的xml布局文件,添加一个RoundProgressBar :

...
xmlns:custom="http://schemas.android.com/apk/res-auto"
...
<com.customview.RoundProgressBar
            android:id="@+id/rpb"
            android:layout_width="120dp"
            android:layout_height="120dp"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="31dp"
            custom:roundColor="@color/light_green"
            custom:roundProgressColor="@color/yellow"
            custom:roundInsideColor="@color/blue"
            custom:roundWidth="17dp"
            custom:textProgressSize="20sp"
            custom:textProgressColor="@color/white"
            custom:textTitle="当前全市拥堵率"
            custom:textTitleSize="8sp"
            custom:textTitleColor="@color/white"
            custom:textIsDisplayable="true"
            />

可以看到,我们在布局文件顶部添加了一个命名空间,这样我们就可以使用自定义属性的前缀“custom”了,后面跟那些属性,就像系统默认的“android”一样。
好了,关于自定义View的基础知识就介绍到这里,下一次我们深入研究一下它的其它特性。

参考文章:
http://blog.csdn.net/xiaanming/article/details/10298163
http://shaohui.xyz/2016/07/08/Android%E8%87%AA%E5%AE%9A%E4%B9%89view%E8%AF%A6%E8%A7%A3/
http://www.jianshu.com/p/e76374706c3e?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
http://blog.csdn.net/lmj623565791/article/details/24252901

猜你喜欢

转载自blog.csdn.net/Yaoobs/article/details/51244243