关于本文:本文原先在个人 CSDN 博客发布(由图片水印能发现),整理以往博客过程当中,发现当时总结的很仔细,因此将其迁移到这里,但愿对你们在自定义 View 方面,能有所帮助 💗html
Android 自定义 View 应用很是普遍,最近逛 github 是偶然发现一个 Demo 感受写的很好,我结合着这个项目的内容,给你们讲讲如何绘制时钟表盘,也算是加深下本身对自定义 View 的理解,涉及内容比较多,你们慢慢吸取。java
开始以前,先让你们看看最后的效果android
让咱们先搭建这个 Viewc++
<?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> 复制代码
小时圆环组成分为外围的圆弧和四个小时数字,因此咱们须要的东西很明确了。git
重写构造方法:github
/* 暗色,圆弧、刻度线、时针、渐变起始色 */ 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 。canvas
因此绘制外围圆环,首先要定义一个 RectF 变量用于绘制圆环,在定义一个 Rect 变量,用于绘制文字。markdown
注 mCanvas 绘图类是 onDraw 中的参数,咱们在 onDraw 中将它保存起来ide
// 测量文字大小 private Rect mTextRect = new Rect(); private RectF mCircleRectF = new RectF(); /* 小时圆圈线条宽度 */ private float mCircleStrokeWidth = 4; /** * 画最外圈的时间 十二、三、六、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(); } 复制代码
包正绘图是圆形的前提是:
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 又是什么做用呢?不如咱们将其山区看看效果:
开始绘制先前,咱们先要准备下一些工具,
/* 刻度线长度 */ 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份 ,
/** * 画一圈梯度渲染的亮暗色渐变圆弧,重绘时不断旋转,上面盖一圈背景色的刻度线 */ 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 地址: github.com/FishInWater…
若是有错欢迎在评论区指出,很是感谢~
祝你们编程愉快!