自定义UI 自制表盘

Posted Notzuonotdied

tags:

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

系列文章目录

  1. 自定义UI 基础知识
  2. 自定义UI 绘制饼图
  3. 自定义UI 圆形头像
  4. 自定义UI 自制表盘
  5. 自定义UI 简易图文混排
  6. 自定义UI 使用Camera做三维变换
  7. 自定义UI 属性动画
  8. 自定义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 \\frac120 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 属性动画