自定义UI 自制表盘

Posted Notzuonotdied

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义UI 自制表盘相关的知识,希望对你有一定的参考价值。

系列文章目录

  1. 自定义UI 基础知识
  2. 自定义UI 绘制饼图
  3. 自定义UI 圆形头像
  4. 自定义UI 自制表盘


前言

这系列的文章主要是基于扔物线的HenCoderPlus课程的源码来分析学习。


创建绘制对象

我们需要创建一个画笔🖌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)rightbottom的入参分别为STAMP_WIDTSTAMP_HEIGHT,这里特意在下图的左上角将dash绘制了出来。
示意图 示意图
  • 那么,为什么刻度柱子会朝着圆心呢?(〃>皿<)

绘制表盘刻度

以下内容摘录自:HenCoder Android 开发进阶: 自定义 View 1-2 Paint 详解


PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style)中, shape参数是用来绘制的 Pathadvance是两个相邻的shape段之间的间隔。不过注意,这个间隔是两个shape段的起点的间隔,而不是前一个的终点和后一个的起点的距离;phaseDashPathEffect中一样,是虚线的偏移;最后一个参数style,是用来指定拐弯改变的时候shape的转换方式。style的类型为PathDashPathEffect.Style,是一个enum,具体有三个值:

  • TRANSLATE:位移
  • ROTATE:旋转
  • MORPH:变体
示意图

从图3(ROTATE)中,我们可以看出来,shape为三角形的虚线中,三角形都会朝着矩形的中心调整shape的位置,即旋转。这就是我们上一章节最后一个问题的答案。

绘制刻度的步骤:

  1. 准备刻度柱子的Pathdash
  2. 准备刻度弧线(和最外边弧线是一致的)
    1. 配置虚线的间距是弧线长度的 1 20 \\frac{1}{20} 201
    2. 虚线的stamp样式是上面准备好的dash
    3. android.graphics.PathDashPathEffect.Style样式需要设置为跟随弧线“旋转”,即PathDashPathEffect.Style.ROTATE
  3. 最后,在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:

绘制指针

示意图

目前我们已知表盘的圆点位置,即指针的起点已知,尚缺指针的终点位置。这里计算指针的位置比较麻烦,涉及到三角函数的计算。这里简单说下计算逻辑。

  1. 先计算指针所在位置与x轴的夹角的大小。
  2. 根据夹角的大小算出对应的弧度java.lang.Math#toRadians
    1. 如果忘了弧度的定义和计算方式了,请见:百度百科 - 弧度
    2. 如果忘了三角函数,请见:
      1. 正弦、余弦和正切
      2. 弧度制及同角三角函数计算公式
  3. 根据弧度算出对应的cos和sin的值。
    1. 因为Math#sinMath#cos提供的方法的入参就是这样的,所以只能这么计算。
    2. 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
    3. 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 自制表盘的主要内容,如果未能解决你的问题,请参考以下文章

自定义UI 自制表盘

自定义UI 简易图文混排

自定义UI 简易图文混排

自定义UI 简易图文混排

自定义UI 自定义布局

自定义UI 自定义布局