关于本文:本文原先在我的 CSDN 博客发布(由图片水印能发现),整理以往博客过程中,发现当时总结的很仔细,所以将其迁移到这里,希望对大家在自定义 View 方面,能有所帮助 💗
引言
Android 自定义 View 应用非常广泛,最近逛 github 是偶然发现一个 Demo 感觉写的很好,我结合着这个项目的内容,给大家讲讲如何绘制时钟表盘,也算是加深下自己对自定义 View 的理解,涉及内容比较多,大家慢慢吸收。
最后效果:
开始之前,先让大家看看最后的效果
在这里插入图片描述现在开始
让我们先搭建这个 View
- 首先,我们定义一个叫做 ClockView 的自定义 View ,让它继承自 View 类。
- 然后在 /res/values 目录下,建立 attrs 文件,在里面定义一些属性 大致如下
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ClockView">
<attr name="clock_backgroundColor" format="color" />
<attr name="clock_lightColor" format="color" />
<attr name="clock_darkColor" format="color" />
<attr name="clock_textSize" format="dimension" />
</declare-styleable>
</resources>
绘制外围小时圆环的准备工作
小时圆环组成分为外围的圆弧和四个小时数字,所以我们需要的东西很明确了。
- 我们首先需要一个 Paint 对象,用于绘制文字,
- 还需要另一个 Paint 对象,用于绘制圆环。
重写构造方法:
/* 暗色,圆弧、刻度线、时针、渐变起始色 */
private int mDarkColor;
/* 小时文本字体大小 */
private float mTextSize;
private Paint mTextPaint;
private Paint mCirclePaint;
public ClockView(Context context) {
super(context);
}
public ClockView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ClockView, 0, 0);
mDarkColor = ta.getColor(R.styleable.ClockView_clock_darkColor, Color.parseColor("#80ffffff"));
mTextSize = ta.getDimension(R.styleable.ClockView_clock_textSize, DensityUtils.sp2px(context, 14));
ta.recycle();
// ANTI_ALIAS_FLAG 平滑绘制 不带磕磕绊绊
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setColor(mDarkColor);
// 居中绘制文字
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextPaint.setTextSize(mTextSize);
mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCirclePaint.setColor(mDarkColor);
// 官方:使用此样式绘制的几何和文本将被描边,尊重绘画上与笔划相关的字段。
// 说白了就是,不要吧这块扇形都上色,只是把最外层的边描下
mCirclePaint.setStyle(Paint.Style.STROKE);
mCirclePaint.setStrokeWidth(mCircleStrokeWidth);// 描边宽度
}
别忘了重写 onMeasure 方法,测量控件大小
关于具体的测量方法,请参考自定义 View 的文章,无非就是对 MeasureSpec 的三种 mode 类型进行分类处理罢了。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getMeasureResult(widthMeasureSpec), getMeasureResult(heightMeasureSpec));
}
private int getMeasureResult(int measureSpec){
int defaultSize = 800;
int size = MeasureSpec.getSize(measureSpec);
int mode = MeasureSpec.getMode(measureSpec);
switch (mode){
case MeasureSpec.UNSPECIFIED:
return defaultSize;
case MeasureSpec.AT_MOST:
return Math.max(defaultSize, size);
case MeasureSpec.EXACTLY:
return size;
default:
return defaultSize;
}
}
开始绘制外围圆环
我们知道,对于绘制圆与椭圆这类图形,经常需要先用 RectF 设置一个边界矩形再进行绘制。如果是绘制文本则是 Rect 。
所以绘制外围圆环,首先要定义一个 RectF 变量用于绘制圆环,在定义一个 Rect 变量,用于绘制文字。
注 mCanvas 绘图类是 onDraw 中的参数,我们在 onDraw 中将它保存起来
// 测量文字大小
private Rect mTextRect = new Rect();
private RectF mCircleRectF = new RectF();
/* 小时圆圈线条宽度 */
private float mCircleStrokeWidth = 4;
/**
* 画最外圈的时间 12、3、6、9 文本和4段弧线
*/
private void drawOutSideArc() {
String[] timeList = new String[]{"12", "3", "6", "9"};
//计算数字的高度
mTextPaint.getTextBounds(timeList[0], 0, timeList[0].length(), mTextRect);// 计算后放回一个矩形存在 mTextRect (涉及c++原生方法,会用就行不要深究)
mCircleRectF.set(mTextRect.width() / 2 + mCircleStrokeWidth / 2,// 画一个外界小矩形,在矩形里画圆
mTextRect.height() / 2 + mCircleStrokeWidth / 2,
getWidth() - mTextRect.width() / 2 - mCircleStrokeWidth / 2,
getHeight() - mTextRect.height() / 2 - mCircleStrokeWidth / 2);
mCanvas.drawText(timeList[0], getWidth() / 2, mCircleRectF.top + mTextRect.height() / 2, mTextPaint);// 定点写字,通过 RectF 取得边界值,由于是顶点在右上方写字,所以要向下平移
mCanvas.drawText(timeList[1], mCircleRectF.right, getHeight() / 2 + mTextRect.height() / 2, mTextPaint);
mCanvas.drawText(timeList[2], getWidth() / 2, mCircleRectF.bottom + mTextRect.height() / 2, mTextPaint);
mCanvas.drawText(timeList[3], mCircleRectF.left, getHeight() / 2 + mTextRect.height() / 2, mTextPaint);
//画连接数字的4段弧线
for (int i = 0; i < 4; i++) {
// 画四个弧线 sweepAngle 弧线角度(扇形角度)
mCanvas.drawArc(mCircleRectF, 5 + 90 * i, 80, false, mCirclePaint);
}
}
接着,我们重写 onDraw() 方法,并在 onDraw() 方法中,调用上面这个方法绘制圆环
private Canvas mCanvas;
@Override
protected void onDraw(Canvas canvas) {
mCanvas = canvas;
drawOutSideArc();
}
运行一下看看效果
我们看到 圆环和时间是出来了,但是这么是个椭圆呢,在仔细检查下我们的代码,在绘制过程中,控制我们圆环的 mCircleRectF 对象,是以整个控件大小为边界的,所以原因就很明了了,那么我们只要将 mCircleRectF 对象设置成一个正方形就行。
在这里插入图片描述
重写 onSizeChanged() 方法,保证绘制的是圆
包正绘图是圆形的前提是:
- 保证 RectF 切割的是正方形
- 那么保证 RextF 围成的是正方形,就要需要知道正方形四边距离控件边界的距离
- 也就是我们需要计算四个整型变量 :1.mPaddingLeft | 2.mPaddingTop | 3.mPaddingRight |
4.mPaddingBottom
private float mRadius;
/* 加一个默认的padding值,为了防止用camera旋转时钟时造成四周超出view大小 */
private float mDefaultPadding;
private float mPaddingLeft;
private float mPaddingTop;
private float mPaddingRight;
private float mPaddingBottom;// 以上4值 均在 onSizechanged()中测量
@Override
protected void onSizeChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
mRadius = Math.min(l - getPaddingLeft() - getPaddingRight(),
t - getPaddingTop() - getPaddingBottom()) / 2;// 各个指针长度
mDefaultPadding = 0.12f * mRadius;
mPaddingLeft = mDefaultPadding + l / 2 - mRadius + getPaddingLeft();// 钟离左边界距离
mPaddingRight = mDefaultPadding + l / 2 - mRadius + getPaddingRight();// 钟离右边界距离
mPaddingTop = mDefaultPadding + t / 2 - mRadius + getPaddingTop();// 钟离上边界距离
mPaddingBottom = mDefaultPadding + t / 2 - mRadius + getPaddingBottom();// 钟离下边界距离
}
对于圆的半径 mRadius ,我们就取控件长和宽中,短的那个的一半为它的值,除此之外还有一种情况,如果控件设置了 padding 那么,如果知识取长宽中短的,那么无论 padding 的值怎么设置,控件的半径始终都是保持长宽中短的那边的一半不变,这样取值使得 padding 失去了作用,也就显得不那么人性化了,所以真正的半径应该是长宽中短的那边,再减去两个 padding 的值,如下:
mRadius = Math.min(w - getPaddingLeft() - getPaddingRight(), h - getPaddingTop() - getPaddingBottom()) / 2;
那么这个 mDefaultPadding 又是什么作用呢?不如我们将其山区看看效果:
在这里插入图片描述试想一下如果我们,没有这个默认值,那么用户在没有设置 padding 时,画出的圆弧必然和 View 的边界相切,圆弧相切到嗨没啥,关键是圆弧上显示时间的文字也得给截去了一半,但有了这个 mDefaultPadding 就不要害怕这个问题。
在这里插入图片描述绘制刻度线的准备
开始绘制先前,我们先要准备下一些工具,
- 首先一个 Paint 对象是必不可少的,
- 然后为了方便用户使用,我们再定义一个颜色,暴露给予设置,
- 最后我们还需要一个 int 型的值,用来设定刻度线的长度
/* 刻度线长度 */
private float mScaleLength;
/* 刻度线画笔 */
private Paint mScaleLinePaint;
/* 背景色 */
private int mBackgroundColor;
public ClockView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ClockView, 0, 0);
mBackgroundColor = ta.getColor(R.styleable.ClockView_clock_backgroundColor, Color.parseColor("#237EAD"));
mDarkColor = ta.getColor(R.styleable.ClockView_clock_darkColor, Color.parseColor("#80ffffff"));
mTextSize = ta.getDimension(R.styleable.ClockView_clock_textSize, DensityUtils.sp2px(context, 14));
ta.recycle();
.
.
.
mScaleLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mScaleLinePaint.setStyle(Paint.Style.STROKE);
mScaleLinePaint.setColor(mBackgroundColor);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
.
.
.
mScaleLength = 0.12f * mRadius;// 根据比例确定刻度线长度
mScaleLinePaint.setStrokeWidth(0.012f * mRadius);// 刻度圈的宽度
}
开始绘制刻度线
绘制国晨反而很简单,对于我们来说 一小时 60min 一分钟 60s,最好的情况莫过于分为 360 份,但是这样一来,由于手机屏幕比较小会直接导致先太密集,密集到了变成圆地步:
在这里插入图片描述所以这里,我们将 360 度,划分为 200份 ,
- 360/200 = 1.8f
- 绘制时,我们没绘制一条边 将 Canvas 角度旋转 1.8f
- 起点:每次我们都从画板顶部开始,下移一个 Padding 再加上 mTextRect 的高度,也就是点钟文字高度,之后再加上一个
刻度线长度由于将刻度线与圆弧分隔开来,防止它们粘在一起 - 终点:笔起点多一个 刻度线长度即可
/**
* 画一圈梯度渲染的亮暗色渐变圆弧,重绘时不断旋转,上面盖一圈背景色的刻度线
*/
private void drawScaleLine() {
mCanvas.save();
// 画背景色刻度线
for (int i = 0; i < 100; i++) {
mCanvas.drawLine(getWidth() / 2, mPaddingTop + mScaleLength + mTextRect.height() / 2,
getWidth() / 2, mPaddingTop + 2 * mScaleLength + mTextRect.height() / 2, mScaleLinePaint);
mCanvas.rotate(1.8f, getWidth() / 2, getHeight() / 2);
}
mCanvas.restore();
}
大功告成
项目 Demo 地址:
https://github.com/FishInWater-1999/android_view_user_defined_first.git
如果有错欢迎在评论区指出,非常感谢~
祝大家编程愉快!
网友评论