自定义UI 自制表盘
Posted Notzuonotdied
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义UI 自制表盘相关的知识,希望对你有一定的参考价值。
系列文章目录
前言
这系列的文章主要是基于扔物线的HenCoderPlus课程的源码来分析学习。
- 扔物线课程源码:Dashboard.java
- android官方文档:自定义绘制
创建绘制对象
我们需要创建一个画笔🖌Paint
来绘制我们的表盘。
public class Dashboard extends View {
// 画笔
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
public Dashboard(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
{
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(Utils.dp2px(3));
}
}
设置布局位置
如果您的视图不需要对其大小进行特殊控制,您只需替换一个方法,即
onSizeChanged()
。系统会在首次为您的视图分配大小时调用onSizeChanged()
,如果视图大小由于任何原因而改变,系统会再次调用该方法。请在onSizeChanged()
中计算位置、尺寸以及其他与视图大小相关的任何值,而不要在每次绘制时都重新计算。
摘录自Andorid官方文档:处理布局事件
public class Dashboard extends View {
// 画笔
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 表盘圆心的位置
private int ww;
private int hh;
public Dashboard(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
{
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(Utils.dp2px(3));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
ww = getWidth() / 2;
hh = getHeight() / 2;
}
}
自定义绘制内容
先上一下自制表盘的效果图吧:
表盘参数说明
参数 | 说明 | 备注 |
---|---|---|
表盘圆心 | (ww, hh) | |
表盘半径 | 150 dp | |
表盘指针长度 | 100 dp | |
刻度柱子规格 | 2 dp x 18 dp | 实质上是一个矩形。 |
表盘刻度数 | 21 个 | |
表盘起始角度 | 150° | |
表盘扫过的角度 | 240° |
绘制表盘弧度
public class Dashboard extends View {
// 表盘起始角度
private static final int START_ANGLE = 150;
// 表盘扫过的角度
private static final int SWEEP_ANGLE = 240;
// 表盘的半径
private static final float RADIUS = Utils.dp2px(150);
// 画笔
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 表盘圆心的位置
private int ww;
private int hh;
// 省略了部分重复的代码
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float l = ww - RADIUS;
float t = hh - RADIUS;
float r = ww + RADIUS;
float b = hh + RADIUS;
// 画线
canvas.drawArc(l, t, r, b, START_ANGLE, SWEEP_ANGLE, false, paint);
}
}
绘制表盘刻度
这一章节使用的是android.graphics.Paint#setPathEffect
方法。具体使用可见扔物线官方博客介绍2.5 setPathEffect(PathEffect effect),这里就不赘述了。
准备刻度柱子
public class Dashboard extends View {
// 虚线标签的宽度
private static final float STAMP_WIDTH = Utils.dp2px(2);
private static final float STAMP_HEIGHT = Utils.dp2px(18);
// 画笔
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 虚线
Path dash = new Path();
// 省略了重复的部分
{
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(Utils.dp2px(3));
// 高度是18dp,宽度是2dp
dash.addRect(0, 0, STAMP_WIDTH, STAMP_HEIGHT,
// Clockwise的缩写,表示顺时针方向绘制
Path.Direction.CW);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 将刻度柱子画出来。可以明显看出来刻度柱子的宽高大概是2dpx18dp的样子。
canvas.save();
canvas.translate(0, Utils.dp2px(150));
canvas.drawPath(dash, paint);
canvas.restore();
float l = ww - RADIUS;
float t = hh - RADIUS;
float r = ww + RADIUS;
float b = hh + RADIUS;
// 画线
canvas.drawArc(l, t, r, b, START_ANGLE, SWEEP_ANGLE, false, paint);
}
}
- 为了方便各位客观直观的了解
android.graphics.Path#addRect(float, float, float, float, android.graphics.Path.Direction)
的right
和bottom
的入参分别为STAMP_WIDT
和STAMP_HEIGHT
,这里特意在下图的左上角将dash
绘制了出来。
- 那么,为什么刻度柱子会朝着圆心呢?(〃>皿<)
绘制表盘刻度
PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style)
中, shape
参数是用来绘制的 Path
;advance
是两个相邻的shape
段之间的间隔。不过注意,这个间隔是两个shape
段的起点的间隔,而不是前一个的终点和后一个的起点的距离;phase
和DashPathEffect
中一样,是虚线的偏移;最后一个参数style
,是用来指定拐弯改变的时候shape
的转换方式。style
的类型为PathDashPathEffect.Style
,是一个enum
,具体有三个值:
TRANSLATE
:位移ROTATE
:旋转MORPH
:变体
从图3(ROTATE
)中,我们可以看出来,shape
为三角形的虚线中,三角形都会朝着矩形的中心调整shape
的位置,即旋转。这就是我们上一章节最后一个问题的答案。
绘制刻度的步骤:
- 准备刻度柱子的
Path
:dash
- 准备刻度弧线(和最外边弧线是一致的)
- 配置虚线的间距是弧线长度的 1 20 \\frac{1}{20} 201。
- 虚线的stamp样式是上面准备好的
dash
。 android.graphics.PathDashPathEffect.Style
样式需要设置为跟随弧线“旋转”,即PathDashPathEffect.Style.ROTATE
。
- 最后,在
onDraw
中,将刻度线绘制出来。
public class Dashboard extends View {
// 虚线标签的宽度
private static final float STAMP_WIDTH = Utils.dp2px(2);
private static final float STAMP_HEIGHT = Utils.dp2px(18);
// 画笔
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 虚线
Path dash = new Path();
// 刻度弧线
Path arc = new Path();
// 虚线类型,用于绘制刻度
PathDashPathEffect effect;
// 省略了重复的部分
{
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(Utils.dp2px(3));
// 高度是18dp,宽度是2dp
dash.addRect(0, 0, STAMP_WIDTH, STAMP_HEIGHT,
// Clockwise的缩写,表示顺时针方向绘制
Path.Direction.CW);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
ww = getWidth() / 2;
hh = getHeight() / 2;
float l = ww - RADIUS;
float t = hh - RADIUS;
float r = ww + RADIUS;
float b = hh + RADIUS;
arc.reset(); // 清空
arc.addArc(l, t, r, b, START_ANGLE, SWEEP_ANGLE);
// 计算刻度弧的长度
PathMeasure pathMeasure = new PathMeasure(arc, false);
// 刻度柱子有21个,刻度之间有20个间隔
// (刻度弧的长度 - 每个刻度柱子的宽度) / 20
float spacing = (pathMeasure.getLength() - STAMP_WIDTH) / (STAMP_COUNT - 1);
// 设置虚线的样式
effect = new PathDashPathEffect(
// 虚线柱子样式(stamp)
dash,
// 刻度间距
spacing,
// 第一个柱子的偏移量为0
0,
// 跟随弧线“旋转”
PathDashPathEffect.Style.ROTATE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float l = ww - RADIUS;
float t = hh - RADIUS;
float r = ww + RADIUS;
float b = hh + RADIUS;
// 画线
canvas.drawArc(l, t, r, b, START_ANGLE, SWEEP_ANGLE, false, paint);
// 画刻度
paint.setPathEffect(effect);
canvas.drawArc(l, t, r, b, START_ANGLE, SWEEP_ANGLE, false, paint);
paint.setPathEffect(null);
}
}
关于Path.Direction.CW和Path.Direction.CCW:
- CW:Clockwise的缩写,表示顺时针旋转。
- CCW:Counter Clockwise的缩写,表示逆时针旋转。
- 具体原理可见:HenCoder Android 开发进阶: 自定义 View 1-1 绘制基础
- 直接搜索
CW
就可以找到对应的解释了。 - 单词解释摘录自:同步电机上写着 CW/CCW 是什么意思?
- 直接搜索
绘制指针
目前我们已知表盘的圆点位置,即指针的起点已知,尚缺指针的终点位置。这里计算指针的位置比较麻烦,涉及到三角函数的计算。这里简单说下计算逻辑。
- 先计算指针所在位置与x轴的夹角的大小。
- 根据夹角的大小算出对应的弧度:
java.lang.Math#toRadians
- 如果忘了弧度的定义和计算方式了,请见:百度百科 - 弧度。
- 如果忘了三角函数,请见:
- 根据弧度算出对应的cos和sin的值。
- 因为
Math#sin
、Math#cos
提供的方法的入参就是这样的,所以只能这么计算。 - 令 l l l表示弧长, L L L表示指针长度,则有: d x = M a t h . c o s ( l ) × L dx = Math.cos(l) \\times L dx=Math.cos(l)×L。
- 令 l l l表示弧长, L L L表示指针长度,则有: d y = M a t h . s i n ( l ) × L dy = Math.sin(l) \\times L dy=Math.sin(l)×L。
- 因为
public class Dashboard extends View {
// 指针的长度
private static final float LENGTH = Utils.dp2px(100);
// 画笔
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 刻度数
private static final int STAMP_COUNT = 21;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 省略重复的部分
// 1. 计算指针的角度
// 2. 计算指针对应的弧度(因为Math#cos和Math#sin的入参都是弧度,所以需要转换下)
double markAngleRadians = Math.toRadians(getAngleFromMark(5));
float startX = ww;
float startY = hh;
float stopX = (float) Math.cos(markAngleRadians) * LENGTH + ww; // dx + ww
float stopY = (float) Math.sin(markAngleRadians) * LENGTH + hh; // dy + ww
// 画指针
canvas.drawLine(startX, startY, stopX, stopY, paint);
}
/**
* 表盘扫过的的角度是SWEEP_ANGLE(240°),分为20段刻度。
*
* @param mark 刻度(区间范围:[0, 20])
*/
int getAngleFromMark(int mark) {
// 起始角度 + 扫过总角度 / 刻度段数 * 当前刻度数
return START_ANGLE + SWEEP_ANGLE / (STAMP_COUNT - 1) * mark;
}
}
三角函数的API:
public final class Math {
/**
* Converts an angle measured in degrees to an approximately
* equivalent angle measured in radians. The conversion from
* degrees to radians is generally inexact.
*
* @param angdeg an angle, in degrees
* @return the measurement of the angle {@code angdeg}
* in radians.
* @since 1.2
*/
public static double toRadians(double angdeg) {
return angdeg / 180.0 * PI;
}
/**
* Returns the trigonometric sine of an angle. Special cases:
* <ul><li>If the argument is NaN or an infinity, then the
* result is NaN.
* <li>If the argument is zero, then the result is a zero with the
* same sign as the argument.</ul>
*
* <p>The computed result must be within 1 ulp of the exact result.
* Results must be semi-monotonic.
*
* @param a an angle, in radians.
* @return the sine of the argument.
*/
@CriticalNative
public static native double sin(double a);
/**
* Returns the trigonometric cosine of an angle. Special cases:
* <ul><li>If the argument is NaN or an infinity, then the
* result is NaN.</ul>
*
* <p>The computed result must be within 1 ulp of the exact result.
* Results must be semi-monotonic.
*
* @param a an angle, in radians.
* @return the cosine of the argument.
*/
@CriticalNative
public static native double cos(double a);
}
附录
源码
public class Dashboard extends View {
// 表盘起始角度
private static final int START_ANGLE = 150;
// 表盘扫过的角度
private static final int SWEEP_ANGLE = 240;
// 表盘的半径
private static final float RADIUS = Utils.dp2px(150);
// 指针的长度
private static final float LENGTH = Utils.dp2px(100);
// 虚线标签的宽度
private static final float STAMP_WIDTH = Utils.dp2px(2);
private static final float STAMP_HEIGHT = Utils.dp2px(18);
// 刻度数
private static final int STAMP_COUNT = 21;
// 画笔
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
// 虚线
Path dash = new Path();
// 刻度弧线
Path arc = 以上是关于自定义UI 自制表盘的主要内容,如果未能解决你的问题,请参考以下文章