概述 本篇为自定义View第三种实现方式——自绘控件。本篇将用一个自定义摩托车仪表盘的栗子来展示自绘控件的实现方式。
一 自绘控件的定义。
自绘控件,顾名思义就是控件所展示的内容都是我们自己绘制上去的。所有的绘制操作就是在onDraw()方法里面进行的,当然我们的这个自定义控件都是View的直接子类。比如最常使用的TextView、ImageView就是View的直接子类,也可视作自绘控件,所有的绘图操作也都是在自己的onDraw()中。
二 自绘控件的步骤。
1 编写自定义属性的xml文件
2 继承View类 重写构造方法 获取自定义属性。
3 重写 onMeasure方法 测量控件的宽高 并设置。
4 重写 onDraw方法 绘制控件。
5 设置事件。
三 一个栗子。
1 栗子说明。
本篇栗子实现的是一个仿摩托车仪表盘,为了展现自绘控件的事件效果,将添加一个点击事件。啥也不说了 先来一个效果图吧。
2 具体实现步骤
① 定义属性。基本你看的元素都可以是自定义属性。在本例中定义了一下属性。
<declare-styleable name="CustomRoundView">
<!-- 最大数值-->
<attr name="MaxNum" format="integer" />
<!--起始角度-->
<attr name="startAngle" format="integer" />
<!-- 圆盘扫过的角度-->
<attr name="sweepAngle" format="integer" />
<!-- 起始颜色-->
<attr name="startColor" format="color|reference" />
<!-- 结束颜色-->
<attr name="endColor" format="color|reference" />
</declare-styleable>
② 继承View 重写构造方法 获取自定义属性。
public CustomRoundView(Context context) {
this(context, null);
}
public CustomRoundView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomRoundView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
//设置背景颜色
setBackgroundColor(context.getResources().getColor(R.color.colorBg));
//获取自定义属性
initAttrs(attrs);
//初始化画笔
initPaint();
}
/**
* 获取自定义属性
* @param attrs
*/
public void initAttrs(AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomRoundView);
maxNum = array.getInt(R.styleable.CustomRoundView_MaxNum, 60);
startAngle = array.getInt(R.styleable.CustomRoundView_startAngle, 120);
sweepAngle = array.getInt(R.styleable.CustomRoundView_sweepAngle, 300);
startColor = array.getColor(R.styleable.CustomRoundView_startColor, context.getResources().getColor(R.color.start_color));
endColor = array.getColor(R.styleable.CustomRoundView_endColor, context.getResources().getColor(R.color.end_color));
//内外圆的宽度
sweepInWidth = dp2px(8);
sweepOutWidth = dp2px(3);
array.recycle();
}
③ 重写onMeasure方法
在这个方法中进行控件宽高的重新测量和设置。关于测量模式的三个值得介绍:
1 当控件的宽高设置为具体值或者是match_parent 模式为 MeasureSpec.EXACTLY 意味精确的
2 当控件的宽高设置为 wrap_content 模式为 MeasureSpec.AT_MOS
3 第三模式为 MeasureSpec.UNSPECIFIED 表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。
/**
* 测量控件宽高 设置控件宽高
* 当宽不确定时 即宽设置为 wrap_content 时 固定宽为 300dp
* 当高不确定时 即高设置为 wrap_content 时 固定高为 400dp
* 需要注意的是 当宽高设置为 match_parent 时 宽高是确定的 应为 屏幕的宽高是知道的
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int wMode = MeasureSpec.getMode(widthMeasureSpec);
int hSize = MeasureSpec.getSize(heightMeasureSpec);
int hMode = MeasureSpec.getMode(heightMeasureSpec);
if (wMode == MeasureSpec.EXACTLY) {
mWidth = wSize;
} else {
mWidth = dp2px(300);
}
if (hMode == MeasureSpec.EXACTLY) {
mHeight = hSize;
} else {
mHeight = dp2px(400);
}
setMeasuredDimension(mWidth, mHeight);
}
④ 重写onDraw方法 绘制控件
因为不同的自绘控件的显示UI是不一样的所以在这里就不把 具体的绘画步骤给出来 。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 设置圆的半径 不要在构造方法里初始化,那时还没测量宽高
radius = getMeasuredWidth() / 4;
canvas.save();
// 相对于当前位置(即为 0,0) 移动画布 向右移动了半径的距离 向下移动了半径的距离
// 相当于移动画笔的位置
canvas.translate(mWidth / 2, (mWidth) / 2);
// 画 内外圆
drawRound(canvas);
// 画刻度
drawScale(canvas);
// 画当前进度值
drawCurrentProgress(canvas);
//画中间文字
drawCentertext(canvas);
//复位画布
canvas.restore();
}
/**
* 画内外圆
*
* @param canvas
*/
public void drawRound(Canvas canvas) {
// 保存画布
canvas.save();
// // 画内圆
// paint_round.setAlpha(0x40); //设置透明度
// paint_round.setStrokeWidth(sweepInWidth); // 设置画笔宽度 即为 内圆宽度
// // 这个构造方法的参数 是 圆 的 最左 最上 最右 最下的点 到的位置
// RectF rectfN = new RectF(-radius, -radius, radius, radius);
// canvas.drawArc(rectfN, startAngle, sweepAngle, false, paint_round);
// 画外圆
paint_round.setStrokeWidth(sweepOutWidth);
int w = dp2px(18); //内圆和外圆 半径的差值 即为中间的空隙
RectF rectfW = new RectF(-radius - w, -radius - w, radius + w, radius + w);
canvas.drawArc(rectfW, startAngle, sweepAngle, false, paint_round);
// 对画布执行 旋转变换
canvas.restore();
}
/**
* 画刻度
*
* @param canvas
*/
private void drawScale(Canvas canvas) {
canvas.save();
float angle = (float) sweepAngle / count;//刻度间隔
canvas.rotate(-270 + startAngle); //将起始刻度点旋转到正上方(270)
for (int i = 0; i <= count; i++) {
//画大刻度
paint_scale.setColor(calculateColor(i));
paint_scale.setStrokeWidth(dp2px(6));
paint_scale.setAlpha(0x70);
canvas.drawLine(0, -radius - sweepInWidth / 2, 0, -radius + sweepInWidth / 2 + dp2px(10), paint_scale);
drawText(canvas, i * maxNum / 30 + "", paint_round);
// //画刻度区间数字
// if (i != 3 && i != 9 && i != 15 && i != 21 && i != 27) {
// drawText(canvas, i * maxNum / 30 + "", paint_round);
// }
//
//
// //画刻度区间文字
// if (i == 3 || i == 9 || i == 15 || i == 21 || i == 27) {
// paint_round.setStrokeWidth(dp2px(2));
// paint_round.setAlpha(0x50);
// drawText(canvas, text[(i - 3) / 6], paint_round);
// }
canvas.rotate(angle); //逆时针
}
canvas.restore();
}
/**
* 绘制当前进度
*
* @param canvas
*/
private void drawCurrentProgress(Canvas canvas) {
canvas.save();
//进度 圆线
paint_current_progress_arc.setStyle(Paint.Style.STROKE);
int sweep;
if (currentNum <= maxNum) {
sweep = (int) ((float) currentNum / (float) maxNum * sweepAngle);
} else {
sweep = sweepAngle;
}
paint_current_progress_arc.setStrokeWidth(sweepOutWidth);
Shader shader = new SweepGradient(0, 0, indicatorColor, null);
paint_current_progress_arc.setShader(shader);
int w = dp2px(10);
RectF rectf = new RectF(-radius - w, -radius - w, radius + w, radius + w);
canvas.drawArc(rectf, startAngle, sweep, false, paint_current_progress_arc);
//进度圆球
float x = (float) ((radius + dp2px(10)) * Math.cos(Math.toRadians(startAngle + sweep)));
float y = (float) ((radius + dp2px(10)) * Math.sin(Math.toRadians(startAngle + sweep)));
paint_current_progress_circle.setStyle(Paint.Style.FILL);
paint_current_progress_circle.setColor(0xffffffff);
paint_current_progress_circle.setMaskFilter(new BlurMaskFilter(dp2px(3), BlurMaskFilter.Blur.SOLID)); //需关闭硬件加速
canvas.drawCircle(x, y, dp2px(3), paint_current_progress_circle);
canvas.restore();
}
/**
* 画中间的文字
*
* @param canvas
*/
private void drawCentertext(Canvas canvas) {
canvas.save();
//绘制中间大文字
paint_center_big_text.setStyle(Paint.Style.FILL);
paint_center_big_text.setTextSize(radius / 2);
paint_center_big_text.setColor(context.getResources().getColor(R.color.center_big_text_color));
canvas.drawText(currentNum + "", -paint_center_big_text.measureText(currentNum + "") / 2, 0, paint_center_big_text);
//绘制中间小文字
paint_center_samll_text.setStyle(Paint.Style.FILL);
paint_center_samll_text.setTextSize(radius / 10);
paint_center_samll_text.setColor(context.getResources().getColor(R.color.center_small_text_color));
canvas.drawText(centerSmallText, paint_center_big_text.measureText(currentNum + "") / 2 + 5, -5, paint_center_samll_text);
//绘制底部文字
paint_bottom_text.setStyle(Paint.Style.FILL);
paint_bottom_text.setTextSize(radius / 5);
paint_bottom_text.setColor(context.getResources().getColor(R.color.center_small_text_color));
canvas.drawText(bottomText, -paint_bottom_text.measureText(bottomText) / 2, radius - 10, paint_bottom_text);
canvas.restore();
}
/**
* 画文字
*
* @param canvas
* @param text
* @param paint_round
*/
private void drawText(Canvas canvas, String text, Paint paint_round) {
paint_round.setStyle(Paint.Style.FILL);
paint_round.setTextSize(sp2px(10));
float width = paint_round.measureText(text); //相比getTextBounds来说,这个方法获得的类型是float,更精确些
canvas.drawText(text, -width / 2, -radius + dp2px(25), paint_round);
paint_round.setStyle(Paint.Style.STROKE);
}
⑤ 设置事件,为本例设置一个简单的点击事件。
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(getContext(),"让我转起来吧!",Toast.LENGTH_SHORT).show();
}
});