新的任务又来了,这次需要实现一个仪表盘的自定义控件,自定义控件一不常写就手生,这次又巩固下,并且学了一些新知识。
https://developer.android.com/training/custom-views/index.html
Android官方文档中关于自定义控件的教程,在大致了解自定义控件相关内容后,看官方的文档,收获更多,能发现许多其他人文章里并没有写的地方。所以有能力的都看看。
美工设计如下:
主要需要实现中间表盘和下方小表盘两个控件(背景色赞)
在开始前,先整理下自定义控件步骤,
首先需要想好要自定义的控件应该需要哪几个属性,比如对于大表盘,背景什么的美工都帮我设计好了,所以我只需要一个属性 speed,表示当前速度。对于小表盘的话,需要较多,我需要圆的半径,当前数值,数值最大值,数值名称,圆环颜色这几个属性。
想好了所需要的属性后,我们需要去定义这些属性,在res/values/attrs.xml里定义,定义好类名和属性名。
创建对应之前定义的类名,生成构造函数,声明所需要的几个属性与paint,并从xml里提取对应的属性值。
重写onMeasure方法
重写onDraw方法,完成控件所需的各种绘制
提供接口,使得可以在代码中更改属性,改变自定义控件状态。
对于自定义view来说,按以上步骤基本就完成了(viewgroup将onDraw换为OnLayout)。
接下来开始第一个控件:
MyWatch
1.首先自定义属性,如上所说我只需要一个speed属性,于是声明
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyWatch">
<attr name="speed" format="integer" />
</declare-styleable>
</resources>
2.创建对应类,构造函数如下
public MyWatch(Context context) {
this(context, null, 0);
}
public MyWatch(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyWatch(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
/**
* 获得我们所定义的自定义样式属性
*/
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyWatch, defStyleAttr, 0);
speed = a.getInt(R.styleable.MyWatch_speed, 0);
paint = new Paint();
back = BitmapFactory.decodeResource(getResources(), R.drawable.back_watch);
center = BitmapFactory.decodeResource(getResources(), R.drawable.icon_watch_center);
arrow = BitmapFactory.decodeResource(getResources(), R.drawable.icon_watch_arrow);
a.recycle();
}
把paint的创建,和几个使用到的bitmap的构造都放到构造函数里,而不放在onDraw里,提高每次界面重绘的效率。
3.重写OnMeasure方法
我这没有做太多的计算,由于表盘使用的是一张图片(源码中已经改成了自己绘制),不是手动画的,所以控件大小基本就固定了。
这里具体做法可以看下一个控件。
在这里不做操作的后果是,如果使用wrap_content属性设置长宽,会和使用match_parent一样的效果。参见http://blog.csdn.net/lmj623565791/article/details/24252901/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
float needWidth = back.getWidth();
int desired = (int) (getPaddingLeft() + needWidth + getPaddingRight());
width = desired;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
float needWidth = back.getHeight();
int desired = (int) (getPaddingTop() + needWidth + getPaddingBottom());
height = desired;
}
setMeasuredDimension(width, height);
}
4.重写OnDraw方法
这里是最主要的了。
@Override
protected void onDraw(Canvas canvas) {
int backWidth = back.getWidth();
int backHeight = back.getHeight();
//绘制表盘
canvas.drawBitmap(back, 0, 0, paint);
//绘制指针
float arc = (speed%240 - 120) * 180 / 240;
Matrix matrix = new Matrix();
matrix.postRotate(arc);
Bitmap dstbmp = Bitmap.createBitmap(arrow, 0, 0, arrow.getWidth(),
arrow.getHeight(), matrix, true);
if (arc>=0)
canvas.drawBitmap(dstbmp, backWidth / 2 - arrow.getWidth() / 2 + (int) (arrow.getWidth() * Math.sin(arc * Math.PI / 180)), (int) (backHeight - center.getWidth() / 2 -arrow.getWidth()/4- arrow.getHeight() * Math.cos(arc * Math.PI / 180)), null);
else {
canvas.drawBitmap(dstbmp, backWidth / 2 - arrow.getWidth() / 2 + (int) (arrow.getHeight() * Math.sin(arc * Math.PI / 180)), (int) (backHeight - center.getWidth() / 2 -arrow.getWidth()/4- arrow.getHeight() * Math.cos(arc * Math.PI / 180)), null);
}
//绘制圆心
// 计算左边位置
left = backWidth / 2 - center.getWidth() / 2;
// 计算上边位置
top = backHeight - center.getHeight() ;
canvas.drawBitmap(center, left, top, paint);
//中心数字
paint.setStrokeWidth(4);
paint.setColor(Color.WHITE);
paint.setTextSize(100);
//抗锯齿
paint.setAntiAlias(true);
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(speed%240+"",backWidth / 2,backHeight*2 / 3,paint);
}
首先绘制表盘,我这提供了图片(源码中已经改成了自己绘制),所以我是用drawBitmap方法,参数表示从控件最左上角开始绘制,这个参见API。
表盘绘制成功后,就是最麻烦的指针了,这里采用的方法是将bitmap通过Matrix旋转变换生成新bitmap后,再计算需要绘制到的坐标。这里方法不唯一,应该有更好的方法。
至于具体的坐标计算,就是数学知识了,慢慢算就好了。。
(不过下次还是自己绘制比较好,因为图片毕竟有宽度,在旋转的时候,非常不好调。。会慢慢偏)
然后绘制圆心,同理,计算出坐标后drawBitmap
最后绘制中心的数字显示,这里主要介绍两个方法。
paint.setTextAlign(Paint.Align.CENTER);
paint.setAntiAlias(true);
绘制文字的时候使用的是,drawText,两个参数分别表示文字的起点坐标和baseline文字底线。
但这样的话,我想绘制到中间居中很不容易,因为我的字长可能变动。所以就用到了
paint.setTextAlign(Paint.Align.CENTER);
方法,使用该方法后,会以坐标为中心,向两边生成文字。之前的话,是一坐标为起点,向右不断生成文字。(大概这么意思)
现在居中完成了,我们可以看下效果
基本效果是好了,但是仔细看,文字有毛边,不平滑。
后来查了半天查到了这个方法
paint.setAntiAlias(true);
其提供的功能是抗锯齿,可以使文字或线条变平滑,看了下,是个native方法,就不管了。
启用这个方法后效果
效果不错。
5.现在要提供方法来改变speed这个数值,写个简单的函数
public void setSpeed(int speed) {
this.speed = speed;
invalidate();
}
然后调用即可。
这个控件基本是完成了,再来做下个控件。
MySmallWatch
1.也是先来确定属性,这个没有提供过多素材,所以需要我自己画。
<declare-styleable name="MySmallWatch">
<attr name="val" format="integer" />
<attr name="total" format="integer" />
<attr name="name" format="string" />
<attr name="unit" format="string" />
<attr name="circle_color" format="color" />
<attr name="radius" format="dimension" />
</declare-styleable>
name表示数值名称,val表示当前数值,unit表示数值单位,total表示最大数值,因为每个表盘的上限值不一致,所以我们在属性里设定,然后好计算所占的比例大小。可以看到,半径使用的类型为dimension,也就是dp,在代码中,会根据分辨率大小,自动计算为相应的px。
2.构造函数
public MySmallWatch(Context context) {
this(context, null, 0);
}
public MySmallWatch(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MySmallWatch(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
/**
* 获得我们所定义的自定义样式属性
*/
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MySmallWatch, defStyleAttr, 0);
val = a.getInt(R.styleable.MySmallWatch_val, 0);
total = a.getInt(R.styleable.MySmallWatch_total, 100);
name = a.getString(R.styleable.MySmallWatch_name);
unit = a.getString(R.styleable.MySmallWatch_unit);
color = a.getColor(R.styleable.MySmallWatch_circle_color,getResources().getColor(R.color.colorAccent));
radius = a.getDimension(R.styleable.MySmallWatch_radius,200);
paint = new Paint();
a.recycle();
}
3.重写OnMeasure,计算长宽
首先获取长宽设定值与长宽的Mode,即在xml文件中,是给长宽指定了具体值还是使用了wrap_content 或 match_parent
MeasureSpec.getMode结果有以下几种:
MeasureSpec.EXACTLY,MeasureSpec.AT_MOST,MeasureSpec.UNSPECIFIED
当指定了长宽具体值时,Mode对应,MeasureSpec.EXACTLY,长宽为设定值
当使用match_parent时,Mode对应,MeasureSpec.EXACTLY,长宽为父容器大小
当使用wrap_content 时,Mode对应MeasureSpec.AT_MOST,长宽尽可能大,这需要我们自己计算。
MeasureSpec.UNSPECIFIED很少遇到
所以我们只需要处理MeasureSpec.AT_MOST情况,计算自己的控件需要多大的长宽。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY)
{
mWidth = widthSize;
} else
{
float needWidth =radius*2+24;
int desired = (int) (getPaddingLeft() + needWidth + getPaddingRight());
mWidth = desired;
}
if (heightMode == MeasureSpec.EXACTLY)
{
mHeight = heightSize;
} else
{
paint.setStrokeWidth(6);
float needWidth =radius*2+24;
int desired = (int) (getPaddingTop() + needWidth + getPaddingBottom());
mHeight = desired;
}
setMeasuredDimension(mWidth, mHeight);
}
4.重写onDraw
接下来开始绘制
@Override
protected void onDraw(Canvas canvas) {
paint.setStrokeWidth(6);
paint.setColor(Color.WHITE);
//抗锯齿
paint.setAntiAlias(true);
//绘制圆圈
canvas.drawCircle(mWidth/2,mHeight/2,radius,paint);
//绘制进度
paint.setStrokeWidth(6);
paint.setColor(color);
paint.setStyle(Paint.Style.STROKE);
int arc=val%total*360/total;
canvas.drawArc(new RectF(mWidth/2-radius-6,mHeight/2-radius-6, mWidth/2+radius+6, mHeight/2+radius+6), 180, arc, false, paint);
//中心数字
paint.setTextAlign(Paint.Align.CENTER);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.FILL);
paint.setTextSize(48);
canvas.drawText(val+unit,mWidth / 2,mHeight/2,paint);
canvas.drawText(name,mWidth / 2,mHeight/2+48,paint);
}
首先创建好paint,
然后drawCircle绘制圆环,需要参数为圆心坐标和半径。
之后绘制外圈的进度表示,使用drawArc方法来绘制圆弧。
这里先需要设置画笔 paint.setStyle(Paint.Style.STROKE);,表示画一个空心的圆弧,即一条弧线。
这个方法需要的参数稍微复杂一点,第一个参数为一个RectF,可以理解为一个与圆弧相切的外接矩形,后两个参数为圆弧的起始度数和扫过度数,起始度数0度即为时钟的3点钟位置。false表示不显示两条弦,这个试验下就明白了。
最后绘制中间的数字,单位,与数值名称即可。
5.最后一样提供一个接口
public void setVal(int val) {
this.val = val;
invalidate();
}
至此第二个控件也完成了,还算比较容易。
加了个按钮,看下效果