我们经常在做项目的时候遇到这样的情况,客户提出需求,UI把设计稿拿出来,你发现直接用现成的开源库好像不行哎,多多少少有些不同。这时候你就会想:要是能自己画一个出来就好了。
所以说:自己写出来的View最靠谱,最灵活,你想让它什么样它就是什么样,前提是你要能画出来…
这也是为什么自定义View的知识如此重要的原因了,所以,打算系统地总结下这方面的知识。
首先,我们先由一个最简单的同心圆开始,例子简单,主要是归纳下自定义View的步骤,为以后更复杂的View打下基础。
好了,废话少说,先看一下这次View的效果,见下图:
(图1)
这次我们要完成的就是上图中的带进度条的同心圆部分。
有人就问了,这些属性有什么用呢?举个例子,我们平时用到的控件有许多属性,这样我们可以根据实际需要控制它的长宽,颜色等等,这样可以提高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:
<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类似于值的类型,具体可查看这个链接。
...
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个构造方法我们需要用哪个呢?
第一个是在正常创建类对象时被调用的:
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获取我们定义的属性。
另外,获取属性的方法,第一个参数就是我们自定义的属性,第二个参数是一个默认值。
@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"
...
"@+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