自定义 Android 钟表盘,这一篇就够了
关于本文:本文原先在我的 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
如果有错欢迎在评论区指出,非常感谢~
祝大家编程愉快!
- Android探索之旅(第二十二篇)成为自定义 View 大咖,这一篇就够了
- Android -------自定义View,有这一篇就够了
- Android:学习AIDL,这一篇文章就够了(下)
- 【转载】自定义View,有这一篇就够了
- Android:学习AIDL,这一篇文章就够了(上)
- 自定义view,有这一篇就够了
- Android:学习AIDL,这一篇文章就够了(上)
- [Android技术专题]APK瘦身看这一篇文章就够了
- Android 从源码出发深度理解Handler线程通讯机制(这一篇就够了)
- Android:学习AIDL,这一篇文章就够了
- Android:学习AIDL,这一篇文章就够了(上)
- Android:学习AIDL,这一篇文章就够了(下)
- android 自定义 view 实现表盘效果
- android一个简单的自定义表盘
- Android:学习AIDL,这一篇文章就够了(上)
- Android:学习AIDL,这一篇文章就够了(下)
- Android 绘图基础:Canvas画布——自定义View基础(绘制表盘、矩形、圆形、弧、渐变)
- 自定义View,有这一篇就够了
- 集成Android免费语音合成功能(在线、离线、离在线融合),有这一篇文章就够了(离线)
- Android:学习AIDL,这一篇文章就够了(上)