Android Canvas绘制图形-拾音器

Android Canvas 绘制图形 — 拾音器

最近在网上看到一个拾音器的设计图,看起来挺美观的,于是我就想把它实现出来,不多废话,先看图,左边是设计图,右边是实现图
设计图实现效果图

除了指针不同,其他的大体上一样

Android Canvas介绍

Canvas作为绘制图形的直接对象,提供了以下几个非常有用的方法

  • Canvas.save()
  • Canvas.restore()
  • Canvas.translate()
  • Canvas.roate()

Canvas.save()这个方法,从字面上理解就是保存的意思,而它的作用也正是将之前的画布保存起来,让后续的操作能像在新的画布上一样操作,这个PhotoShop的图层是一个概念
Canvas.restore()这个方法,可以理解为合并图层,就是将之前保存下来的图层合并为一个图层
Canvas.translate()这个方法,可以理解为移动坐标系,很多人在用时,理解为移动画布,所以在计算坐标时会出一些问题,在这里把他理解为移动坐标系更加恰当。画布的初始坐标系是左上角,如果我们调用translate(x, y)之后,则表示将原点(0, 0)移动到了(x, y),后面的坐标计算都是在这一点上进行的
Canvas.roate()可以理解为旋转坐标系,用法和Canvas.translate()相似,但多了个角度的参数,调用roate(degree, x, y)之后,则表示将以(x, y)为原点的坐标系旋转degree个角度


Paint理解为画笔,在Canvas上所有的图形,都需要这个对象,以下有几个常用方法

  • paint.setAntiAlias()
  • paint.setStyle()
  • paint.setStrokeWidth()
  • paint.setColor()

paint.setAntiAlias()是给画笔设置是否抗锯齿,参数为布尔类型
paint.setStyle()是给画笔设置画笔样式
paint.setStrokeWidth()给画笔设置画笔宽度
paint.setColor()给画笔设置画笔颜色

实现

有了上面的理论基础,现在我们可以来实现上面的效果了,先来分析一下上面效果图中有什么

  1. 刻度盘——外面的半圆刻度盘
  2. 刻度线——整数的深色粗线和其余德灰色细线
  3. 圆形底盘——共三个圆形底盘,最上面的深蓝色圆形要在指针上面
  4. 指针

新建一个类,继承View

public class TunerView extends View {
    private int mWidth;
    private int mHeight;

    public TunerView(Context context){
        super(context);
        //获取屏幕的宽高
        WindowManager windowManager = 
            (WindowManager) getContext().Context.WINDOW_SERVICE);

        mWidth = windowManager.getDefaultDisplay().getWidth();
        mHeight = windowManager.getDefaultDisplay().getHeight();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        ...
    }
}

第一步(Step 1):画刻度盘

由于画布一开始绘制文字时,文字是垂直的,而在设计图中刻度盘是从左往右,文字是水平的,所以需要先对画布坐标系进行旋转

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int xc = mWidth / 2;    //屏幕中心X坐标
    int yc = mHeight / 2;   //屏幕中心Y坐标
    int radius = xc - 30;   //圆半径
    /**
     * 正常情况下,一开始写文字,都是垂直的,但我们的仪表盘
     * 要求从左边开始画,文字是水平的,所以需要先对画布坐标系进行逆时针旋转90°
     */
    canvas.rotate(-90, xc, yc);

    //画刻度盘
    Paint paintDegree = new Paint();
    paintDegree.setStrokeWidth(3);
    //画刻度线条
    for (int i = -50; i <= 50; i++){
        //区别整点和非整点的线条样式
        if (i % 10 == 0){
            paintDegree.setStrokeWidth(5);
            paintDegree.setTextSize(30);
            paintDegree.setAntiAlias(true);
            paintDegree.setColor(getResources().getColor(R.color.colorAccent));
            //这里画线的坐标计算,需要自行去理解,这里我就不多加解释了
            canvas.drawLine(xc, yc - radius, xc, yc - radius + 40, paintDegree);
            String degree = String.valueOf(i);
            canvas.drawText(degree,
                    //这里使用了Paint对象的measureText()方法,
                    //该方法是传入一个String类型的参数,经过计算之后返回该String对象中
                    //文字所占用的宽度
                    xc - paintDegree.measureText(degree) / 2,
                    yc - radius + 70,
                    paintDegree);
            /**
             * 由于本案中两个整点之间只被分为了5分,所以每份的间隔就是2
             */
        } else if (i % 2 == 0){
            paintDegree.setStrokeWidth(3);
            paintDegree.setTextSize(15);
            paintDegree.setAntiAlias(true);
            paintDegree.setColor(getResources().getColor(R.color.grey));
            canvas.drawLine(xc, yc - radius + 10, xc, yc - radius + 30, paintDegree);

        }

        //每画完一条刻度线条,就需要对画布坐标系进行旋转,通过旋转简化坐标运算
        //旋转角度为:总度数 / 总份数
        //本案例中,总度数为180°, 分值区间为(-50 ~ 50),所以总份数为100份
        canvas.rotate((float)(180 / 100), xc, yc);
    }

    //画圆形底盘
    ...
}

运行效果:
完成刻度盘

第二步(Step 2):画园形底盘

画实心圆形底盘,通过调用paint.setColor()来设置颜色,调用paint.drawCircle()方法来画出圆形

//画大圆盘
Paint bigCircle = new Paint();
bigCircle.setColor(getResources().getColor(R.color.colorPrimary));
bigCircle.setAntiAlias(true);
canvas.drawCircle(xc, yc, xc - 180, bigCircle);
//画中等圆盘
Paint midCircle = new Paint();
midCircle.setColor(getResources().getColor(R.color.colorPrimaryDark));
midCircle.setAntiAlias(true);
canvas.drawCircle(xc, yc, xc - 280, midCircle);
//画小圆盘
Paint smallCircle = new Paint();
smallCircle.setColor(getResources().getColor(R.color.colorAccent));
smallCircle.setAntiAlias(true);
canvas.drawCircle(xc, yc, xc - 320, smallCircle);

drawCircle()方法有四个参数,

  • cx:圆心的x坐标,float类型
  • cy:圆心的y坐标,float类型
  • radius:圆形的半径,float类型
  • paint:画笔对象,Paint类型

运行效果:
画三个圆形底盘

第三步(Step 3):画指针
画指针其实和画线是一个道理,所以我们直接调用drawLine()方法就可以了

//画指针
Paint paintCursor = new Paint();
paintCursor.setStrokeWidth(3);
paintCursor.setAntiAlias(true);
paintCursor.setColor(getResources().getColor(R.color.colorAccent));
canvas.drawLine(xc, yc, xc - radius + 80, yc + 10, paintCursor);

drawLine()方法有5个参数

  • startX:线条的起始点x坐标,类型float
  • startY:线条的起始点y坐标,类型float
  • stopX:线条的结束点x坐标,类型float
  • stopY:线条的结束点y坐标,类型float
  • paint:画笔对象,Paint类型

运行效果:
指针绘制

这里注意一下,由于我们的指针使用的颜色和最小的那个圆形的颜色是一样的,所以运行效果里看不出问题,如果换一个颜色,就会发现,指针在最小的圆形上面,而设计图上的指针是在最小的圆形下面,那么这要怎么解决呢?不要方,只需要把绘制最小那个圆的canvas.drawCircle()方法放到绘制指针的drawLine()方法之后就可以了

第四步(Step 4):画正下方的文字

画正下方的文字,只需要调用canvas.drawText()方法就可以了

Paint pitchPaint = new Paint();
pitchPaint.setAntiAlias(true);  //设置抗锯齿
pitchPaint.setTextSize(40); 
pitchPaint.setColor(getResources().getColor(R.color.colorAccent));
String pitch = "Pitch";
canvas.drawText(pitch,
        xc - pitchPaint.measureText(pitch) / 2,
        //这里加上180,是因为要加上最外面那个大的圆形的半径
        //再加上80,是为了让文字和最大的那个圆产生间隙
        yc + 180 + 80,
        pitchPaint);

运行效果:
绘制正下方的文字

可以看到文字的方向不对,为什么文字的方向不对呢?这是因为我们在绘制刻度盘的时候,我们对画布坐标系进行了旋转,画完刻度盘之后,坐标系被旋转了180°,但由于在绘制刻度盘之前,我们对整个画布坐标系进行逆时针旋转了90°,所以实际上只旋转了90°,因此,我们看到绘制的文字不是在正下方,而是在左边,被旋转了90°,那么这要怎么解决呢?最简单的方法当然是在绘制文字之前,把画布坐标系旋转回来。所以需要添加一行代码:

canvas.rotate(-90, xc, yc);

运行效果
最终效果

最后

由于上面的案例代码里用到了很多写死的数字代码(Hard Code),这不利于代码的灵活性,而且,既然有指针,就应该让指针根据一个提供的输入值,来让指针指向对应的数值位置,所以,我对代码进行了改良,代码如下:

/**
 * Created by zhongzilu on 2016/5/25 0025.
 */
public class DialView extends View {

    private float mWidth;
    private float mHeight;
    private float mAngle = 0;   //指针旋转角度,值为0时指针垂直显示
    private static int mMaxValue = 50;  //分值区间的最大值
    private static int mMinValue = -50; //分值区间的最小值
    /**输入值,通过输入值来计算指针的旋转角度,即{@mAngle}的值,
     * 最终在界面上呈现的效果是指针指向输入值的刻度上*/
    private float mValue = 0;
    /**刻度盘呈现的总弧度,本案例中总弧度为180,呈半圆形*/
    private float mArc = 180;

    public DialView(Context context, AttributeSet attrs) {
        super(context, attrs);

        //获取屏幕的宽高
        WindowManager windowManager = (WindowManager) getContext().getSystemService(
                Context.WINDOW_SERVICE);
        mWidth = windowManager.getDefaultDisplay().getWidth();
        mHeight = windowManager.getDefaultDisplay().getHeight();
    }

    /**
     * 注意:
     * 下方的所以坐标计算和长度计算都是依据在屏幕宽度为621px情况下的
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        float xc = mWidth / 2;    //圆中心X坐标
        float yc = mHeight / 2;   //圆中心Y坐标
        float radius = (float)(xc - xc * 0.05314);   //圆半径

        //画刻度盘
        Paint paintDegree = new Paint();

        /**
         * 正常情况下,一开始画线或写文字,都是垂直的,但我们的仪表盘
         * 要求从左边开始画,文字是水平的,所以需要先进行画布旋转90°
         */
        canvas.rotate((-mArc / 2), xc, yc);
        System.out.println("before rotate==>" + (-mArc / 2));
        /**
         * 本案例中,设置的分值区间为(-50 ~ 50)
         */
        for (int i = mMinValue; i <= mMaxValue; i++){
            //区别整点和非整点
            if (i % 10 == 0){
                //在屏幕宽度为621下,大小为5
                paintDegree.setStrokeWidth((float)(mWidth * 0.008051));
                //刻度字体大小是依据是:在屏幕宽度为621的分辨率下,刚好为30
                paintDegree.setTextSize((float)(mWidth * 0.048309));
                paintDegree.setAntiAlias(true);
                paintDegree.setColor(getResources().getColor(R.color.colorAccent));
                canvas.drawLine(xc, yc - radius, xc, (float)(yc - radius + xc * 0.101449), paintDegree);
                String degree = String.valueOf(i);
                canvas.drawText(degree,
                        //这里使用了Paint对象的measureText()方法,
                        //该方法是传入一个String类型的参数,经过计算之后返回该String对象中
                        //文字所占用的宽度
                        xc - paintDegree.measureText(degree) / 2,

                        //下方mWidth * 0.048309 = 在屏幕宽度为621下,长度为30
                        yc - radius + (float)(xc * 0.101449 + mWidth * 0.048309),
                        paintDegree);
                /**
                 * 由于两个整点之间只被分为了5分,所以每份的间隔就是2
                 */
            } else if (i % 2 == 0){
                paintDegree.setStrokeWidth((float)(mWidth * 0.004830));
                //在屏幕宽度为621下,字体大小为15
                paintDegree.setTextSize((float)(mWidth * 0.024154));
                paintDegree.setAntiAlias(true);
                paintDegree.setColor(getResources().getColor(R.color.grey));
                /**
                 * 画灰色短横线,坐标计算依据是灰色短横向长度为深色长横线的一半,
                 * 并且,两种刻度线的中点在同一个圆的圆弧上
                 * 由于深色刻度线的长度 = 正中深色小圆的半径 = 屏幕宽度一半的0.101449倍
                 */
                canvas.drawLine(xc, 
                        (float)(yc - radius + xc * 0.101449 / 4), 
                        xc, 
                        (float)(yc - radius + xc * 0.101449 / 4 * 3), 
                        paintDegree);

            }

            //通过旋转画布简化坐标运算
            canvas.rotate(mArc / (float)(mMaxValue - mMinValue), xc, yc);
        }

        /**
         * 三个圆盘的半径和屏幕宽度的一半的比例为
         * 屏幕宽度一半 :大圆 :中圆 :小圆 =
         * 1 :0.481481 :0.201288 :0.101449
         */
        //画大圆盘
        Paint bigCircle = new Paint();
        bigCircle.setColor(getResources().getColor(R.color.colorPrimary));
        bigCircle.setAntiAlias(true);
        canvas.drawCircle(xc, yc, (float)(xc * 0.481481), bigCircle);
        //画中等圆盘
        Paint midCircle = new Paint();
        midCircle.setColor(getResources().getColor(R.color.colorPrimaryDark));
        midCircle.setAntiAlias(true);
        canvas.drawCircle(xc, yc, (float)(xc * 0.201288), midCircle);
        //画小圆盘
        Paint smallCircle = new Paint();
        smallCircle.setColor(getResources().getColor(R.color.colorAccent));
        smallCircle.setAntiAlias(true);
        canvas.drawCircle(xc, yc, (float)(xc * 0.101449), smallCircle);
        canvas.save();

        //画指针
        Paint paintCursor = new Paint();
        //在屏幕宽度为621下,大小为3
        paintCursor.setStrokeWidth((float)(mWidth * 0.004830));
        paintCursor.setAntiAlias(true);
        paintCursor.setColor(getResources().getColor(R.color.colorAccent));
        /**
         * 思路:要想让指针以圆心为中心旋转一定角度,要么旋转画布,要么根据坐标来画,
         * 由于旋转角度比根据坐标更简单,所以就用旋转角度的方式来实现
         *
         * 指针具体旋转多少度,得根据算法来计算
         *
         * 算法:角度 = 输入值 * ( 总弧度 / 分值总数)
         *
         * 举例:本案例中给出的分值区间为(-50 ~ 50),所以分值总数为100
         * 假设现在输入值为30,那么角度就为54°
         */
        mAngle = mValue * (mArc / (float) (mMaxValue - mMinValue));
        System.out.println("mAngle ==>" + mAngle);
        canvas.rotate(mAngle, xc, yc);
        /**
         * xc * 0.101449 = 深色刻度线条的长度
         * mWidth * 0.064412 = 在屏幕宽度为621下,长度为40
         * 所以下面第三个参数的值可以理解为距离深色刻度线下方有40倍数的间隙
         */
        canvas.drawLine(xc, yc,
                xc - radius + (float)(xc * 0.101449 + mWidth * 0.064412),
                yc + 3, paintCursor);

        //覆盖在指针上的圆
        /**
         * 由于之前的坐标系已经发生了旋转,所以要在正下方写上文字,就需要旋转回来
         * 当然也可以通过去计算坐标来显示在正下方,但旋转画布的方式更加简单和更容易理解
         */
        canvas.rotate(-(mAngle + mArc / 2), xc, yc);

        Paint pitchPaint = new Paint();
        pitchPaint.setAntiAlias(true);
        //在屏幕宽度为621下,字体大小为40
        pitchPaint.setTextSize((float)(mWidth * 0.048309));
        pitchPaint.setColor(getResources().getColor(R.color.colorAccent));
        String pitch = "Pitch";
        canvas.drawText(pitch,
                xc - pitchPaint.measureText(pitch) / 2,
                //这里加上180,是因为要加上最外面那个大的圆形的半径
                //再加上(mWidth * 0.123188),是为了让文字和最大的那个圆产生间隙
                //在屏幕宽度为621下,间隙为80
                yc + (float)(xc * 0.481481 + mWidth * 0.123188),
                pitchPaint);

        //值
        Paint pitchValuePaint = new Paint();
        pitchValuePaint.setAntiAlias(true);
        pitchValuePaint.setTextSize((float)(mWidth * 0.080515));
        pitchValuePaint.setColor(getResources().getColor(R.color.colorAccent));
        String value = "- -";
        canvas.drawText(value,
                xc - pitchValuePaint.measureText(value) / 2,
                yc + (float)(xc * 0.481481 + mWidth * 0.209339),
                pitchValuePaint);

        canvas.restore();
    }
}

经过修改过后,只需要修改全局变量mValue的值,就可以使指针指向界面上对应的值了,比如mValue = 30,运行效果
指针旋转测试

总结

总的来说,没什么难度,关键点在于绘图时的坐标计算,以及对旋转画布的理解,旋转画布是旋转的画布坐标系。在本案例中,我没有对指针的旋转做动画处理,是因为本文重在讲解Canvas画图,各位也可以自行加上指针旋转动画代码。好了,以上就是今天的内容,各位可以发挥自己的想象力,绘制出更多更有趣的图形。我是钟子路,Thanks for watching!

作者zhongzilu
源码https://github.com/zhongzilu/TunerView

本文总阅读量