自定义表盘

自定义表盘

之前看过一个大神得文章,也是自定义精美表盘的博客,正好自己在自定义View这一块比较薄弱,所以决定用自己得方法来实现一个表盘样式,来锻炼自己:

实现这个表盘大体需要如下几个步骤:
- 绘制外层圆形
- 绘制表盘的刻度
- 绘制表盘上的时间
- 绘制时分秒的指针
- 绘制指针交叉点
- 计算当前时间,重新绘制


首先介绍一下相关的属性

    /**
     * 设置画布中心点
     */
    private float centerX, centerY;
    /**
     * 画笔
     */
    private Paint mPaint;
    /**
     * 时分秒 指针的颜色
     */
    private int mHourColor, mMinuteColor, mSecondColor;
    /**
     * 时分秒 指针的宽度
     */
    private float mHourWidth, mMinuteWidth, mSecondWidth;
    /**
     * 字体的大小
     */
    private float mTextSize;
    /**
     * 字体的颜色
     */
    private int mTextColor;
    /**
     * 圆形的半径
     */
    private float mRadius;
    /**
     * 刻度的颜色
     */
    private int scaleBoldColor, scaleColor;
    /**
     * 刻度的宽度
     */
    private float scaleBoldWidth, scaleWidth;
    /**
     * 两个刻度之间的角度
     */
    private float minDegree;
    /**
     * 两个整点刻度之间的角度
     */
    private float maxDegree;
    /**
     * 时分秒指针转过的角度
     */
    private float hourDegree, minutDegree, secondDegree;

以及相关属性的值的获取:
这里写图片描述
当然此方法都用在构造方法中。对自定义View构造方法不理解的同学可以自行查阅资料。

接下来就具体讲解一下绘制过程
首先贴上会使用的方法:dp转化为pix

   /**
     * dp转px
     */
    public static int dp2px(Context ctx, float dp) {
        float density = ctx.getResources().getDisplayMetrics().density;
        int px = (int) (dp * density + 0.5f);// 4.9->5 4.4->4
        return px;
    }

绘制之前我们当然还要计算相关的属性,比如半径,画布的中心点:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        centerX = w / 2;//中心点横坐标
        centerY = h / 2;//中心点纵坐标
        //半径取宽和高的最小值的一半,再留出一点距离
        mRadius = Math.min(w, h) / 2 - dp2px(getContext(), 10);
    }

1. 绘制外部圆环

绘制一个圆当然是手到擒来呀!

   /**
     * 圆
     *
     * @param canvas
     */
    private void drawCircle(Canvas canvas) {
        canvas.save();
        Path path = new Path();
        path.addCircle(0, 0, mRadius, Path.Direction.CW);
        mPaint.setStrokeWidth(dp2px(getContext(), 1));
        mPaint.setColor(Color.WHITE);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawPath(path, mPaint);
        canvas.restore();
    }

效果图
这里写图片描述
canvas.save() 和 canvas.restore() 方法,我个人的理解就是用来保存和恢复当前绘制阶段canvas的状态,使得不同的绘制内容都有对应的canvas的状态,以不受其他阶段的影响 ; 至于Path,它可以绘制线,圆,矩形,椭圆,圆角矩形等内容,至于它的使用,我也是在一个大神的自定义View系列博客中一步一步学习的,写的是相当不错,你敢戳一下吗?,保证你满载而归 !!!

绘制表盘刻度

先上代码块:

 /**
     * 刻度
     *
     * @param canvas
     */
    private void drawScale(Canvas canvas) 
    {
        canvas.save();
        mPaint.setStyle(Paint.Style.FILL);
        //60个刻度
        for (int i = 0; i < 60; i++) 
        {
            if (i % 5 == 0) 
            {//整点的刻度
                mPaint.setColor(scaleBoldColor);
                mPaint.setStrokeWidth(scaleBoldWidth);
                canvas.drawLine(0, mRadius - dp2px(getContext(), 5), 0, mRadius - dp2px(getContext(), 20), mPaint);
            }
             else 
             {
                mPaint.setColor(scaleColor);
                mPaint.setStrokeWidth(scaleWidth);
                canvas.drawLine(0, mRadius - dp2px(getContext(), 5), 0, mRadius - dp2px(getContext(), 15), mPaint);
            }
            //画完刻度之后再旋转画布
            canvas.rotate(minDegree);
        }
        canvas.restore();
    }

效果图
这里写图片描述
首先设置画笔的颜色,填充方式等。表盘上有60个刻度,我们不可能一个一去绘制的,所以要在for循环里面去完成,从效果图可以看到整点对应的刻度都是加深的颜色,并且比普通的刻度稍微长一点,因此我们要分两种情况去绘制了。首先,和数字对其的刻度:我们也称之为整点刻度,我们使用 (i % 5 == 0)来做判断依据,当余数为0的时候正好指针指着整点刻度,此时我们就会只一条线,

canvas.drawLine(0, mRadius - dp2px(getContext(), 5), 0, mRadius - dp2px(getContext(), 20), mPaint);

前四个值是,线条的起点坐标和结束坐标,第五个就是我们的画笔。我们先绘制最正上方的那个线条,起点坐标(0, mRadius - dp2px(getContext(), 5)),终点坐标(0, mRadius - dp2px(getContext(), 20)),我们是用当前的半径减去一个固定值而已,这个很容易理解!再者,我们要绘制两个整点刻度之间的刻度,不管是整点刻度还是普通的刻度,它们之间的角度都是一样的 即 :minDegree = 360 / 60 ,我们称之为小角度,当然后面还会讲到一个大角度的。

 canvas.drawLine(0, mRadius - dp2px(getContext(), 5), 0, mRadius - dp2px(getContext(), 15), mPaint);

同绘制整点刻度是一个道理。结束纵坐标只不过要比上一种稍微大一点,
最后再进行画布的旋转操作;

//画完刻度之后再旋转画布
 canvas.rotate(minDegree);

到此刻度已经绘制完成

绘制数字

代码:

 /**
     * 字 drawText 第二个参数的意思默认是字符串的左边在屏幕的位置,第三个是指定这个字符baseline在屏幕上的位置
     *
     * @param canvas
     */
    private void drawText(Canvas canvas) {
        canvas.save();
        String[] text = new String[]{"12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"};
        mPaint.setColor(mTextColor);
        mPaint.setTextSize(mTextSize);
        mPaint.setStrokeWidth(dp2px(getContext(), 0.5f));
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setTextAlign(Paint.Align.CENTER);
        float degree12 = -90;//初始角度
        double radian;//弧度
        for (int i = 0; i < 12; i++) {
            Rect textBounds = new Rect();
            mPaint.getTextBounds(text[i], 0, text[i].length(), textBounds);
            radian = 2 * Math.PI / 360 * degree12;//角度转化为弧度
            canvas.drawText(text[i], (float) ((mRadius - dp2px(getContext(), 30)) * Math.cos(radian)),(float) ((mRadius - dp2px(getContext(), 30)) * Math.sin(radian)) + dp2px(getContext(), 5), mPaint);
            degree12 += maxDegree;
        }
        canvas.restore();
    }

效果:
这里写图片描述

同样我们先设置画笔,同时设置字体的位置绘制矩形的中心,Paint.Align.CENTER 这个属性比计算快多了 ,不过不是很准确,关于自定义View绘制字体请自行查阅资料。
在drawText的时候,我是先大概计算一下数字位置的坐标,具体计算过程看图:
这里写图片描述
我们假设最正上方的当前角度为-90度,如图所示位置的角度则为30度,字体的位置坐标大概就死内圆和直线的交点处。计算公式则为:内圆半径 x 当前的角度的正余弦值。

//横坐标
((mRadius - dp2px(getContext(), 30)) * Math.cos(radian))

由于纵坐标有些偏差 所以偷懒了一下 在纵坐标上加了一个固定值:

(float) ((mRadius - dp2px(getContext(), 30)) * Math.sin(radian)) + dp2px(getContext(), 5)

在循环到下一个位置的时候,角度要加上30度。在这里要注意的是:Math.cos() 和 Math.sin() 需要填入的是弧度,所以我们在绘制之前先将对应的角度转化为弧度了,公式:弧度 = 2 * Math.PI / 360 * 角度。这样就顺利完成了。

绘制指针

代码:

 /**
     * 指针
     *
     * @param canvas
     */
    private void drawClockPoint(Canvas canvas) {
        caculateDegree();
        //绘制时针
        canvas.save();
        canvas.rotate(hourDegree);
        mPaint.setColor(mHourColor);
        mPaint.setStrokeWidth(mHourWidth);
        RectF hourRectF = new RectF(-mHourWidth / 2, -mRadius + dp2px(getContext(), 80),
                mHourWidth / 2, mRadius / 9);
        canvas.drawRoundRect(hourRectF, 10, 10, mPaint);
        canvas.restore();

        //绘制分针
        canvas.save();
        canvas.rotate(minutDegree);
        mPaint.setColor(mMinuteColor);
        mPaint.setStrokeWidth(mMinuteWidth);
        RectF minuteRectF = new RectF(-mMinuteWidth / 2, -mRadius + dp2px(getContext(), 50),
                mMinuteWidth / 2, mRadius / 9);
        canvas.drawRoundRect(minuteRectF, 10, 10, mPaint);
        canvas.restore();

        //绘制秒针
        canvas.save();
        canvas.rotate(secondDegree);
        mPaint.setColor(mSecondColor);
        mPaint.setStrokeWidth(mSecondWidth);
        RectF secondRectF = new RectF(-mSecondWidth / 2, -mRadius + dp2px(getContext(), 30),
                mSecondWidth / 2, mRadius / 9);
        canvas.drawRoundRect(secondRectF, 10, 10, mPaint);
        canvas.restore();

        mPaint.setColor(mSecondColor);
        canvas.drawCircle(0, 0, dp2px(getContext(), 8), mPaint);
    }

效果图:
这里写图片描述

在绘制指针的时候,使用了canvas绘制圆角矩形的方法,首先要设置的是rectf的四个值,这个也可以理解为当前圆角矩形的左上角和右下角的坐标值,由于我们将坐标轴移至了中心点,坐标计算起来还是特别方便的,默认会的圆角矩形都是指向12点方向,这样就很容易理解了。

绘制中心点

这个很容易理解啦,就是绘制一个圆,要求和秒针的颜色是一样的 ,这样看起来比较美观,我随意设置了一个半径;

canvas.drawCircle(0, 0, dp2px(getContext(), 8), mPaint);

计算当前时间

代码:

  /**
     * 计算当前时分秒指针的角度
     */
    private void caculateDegree() {
        Calendar mCanendar = Calendar.getInstance();
        int hour = mCanendar.get(Calendar.HOUR_OF_DAY);//24小时制 ,HOUR的话是12小时制
        int minute = mCanendar.get(Calendar.MINUTE);
        int second = mCanendar.get(Calendar.SECOND);

        hourDegree = (hour % 24) * 360 / 12;
        minutDegree = minute * 360 / 60;
        secondDegree = second * 360 / 60;
    }

这里我们区分了12小时制和24小时制,并计算出当前时间时分秒指针应该在的正确角度。不过没有完善的是,秒针在走动的时候,分针和时针是不动的,这个和事实是不同的,这里暂时就不计算了。

完整代码

所有方法在ondraw方法中调用啦,不过还多了一句就是:

postInvalidateDelayed(1000);

在这里模拟秒针转动,每隔一秒重新绘制一次。

完整的自定义代码如下:


    /**
     * 设置画布中心点
     */
    private float centerX, centerY;
    /**
     * 画笔
     */
    private Paint mPaint;
    /**
     * 时分秒 指针的颜色
     */
    private int mHourColor, mMinuteColor, mSecondColor;
    /**
     * 时分秒 指针的宽度
     */
    private float mHourWidth, mMinuteWidth, mSecondWidth;
    /**
     * 字体的大小
     */
    private float mTextSize;
    /**
     * 字体的颜色
     */
    private int mTextColor;
    /**
     * 圆形的半径
     */
    private float mRadius;
    /**
     * 刻度的颜色
     */
    private int scaleBoldColor, scaleColor;
    /**
     * 刻度的宽度
     */
    private float scaleBoldWidth, scaleWidth;
    /**
     * 两个刻度之间的角度
     */
    private float minDegree;
    /**
     * 两个整点刻度之间的角度
     */
    private float maxDegree;
    /**
     * 时分秒指针转过的角度
     */
    private float hourDegree, minutDegree, secondDegree;

    public CustomClock(Context context) {
        super(context);
        initView(context, null);
    }

    public CustomClock(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView(context, attrs);
    }

    public CustomClock(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context, attrs);
    }

    private void initView(Context context, AttributeSet attrs) {
        //获取自定义属性
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomClock);
        mHourColor = typedArray.getColor(R.styleable.CustomClock_hourPointColor, Color.BLACK);
        mHourWidth = typedArray.getDimension(R.styleable.CustomClock_hourPointWidth, dp2px(context, 3f));
        mMinuteColor = typedArray.getColor(R.styleable.CustomClock_minutePointColor, Color.BLUE);
        mMinuteWidth = typedArray.getDimension(R.styleable.CustomClock_minutePointWidth, dp2px(context, 2f));
        mSecondWidth = typedArray.getDimension(R.styleable.CustomClock_secondPointWidth, dp2px(context, 1f));
        mSecondColor = typedArray.getColor(R.styleable.CustomClock_secondPointColor, Color.RED);
        scaleWidth = typedArray.getDimension(R.styleable.CustomClock_scaleWidth, dp2px(context, 1f));
        scaleColor = typedArray.getColor(R.styleable.CustomClock_scaleColor, Color.parseColor("#858585"));
        scaleBoldColor = typedArray.getColor(R.styleable.CustomClock_scaleBoldColor, Color.parseColor("#040304"));
        scaleBoldWidth = typedArray.getDimension(R.styleable.CustomClock_scaleBoldWidth, dp2px(context, 1.5f));
        mTextColor = typedArray.getColor(R.styleable.CustomClock_textColor, Color.parseColor("#141214"));
        mTextSize = typedArray.getDimension(R.styleable.CustomClock_textSize, dp2px(context, 14));
        //回收typedArray对象
        typedArray.recycle();
        minDegree = 6;
        maxDegree = 30;
        initPaint();
    }

    private void initPaint() {
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setAntiAlias(true);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        centerX = w / 2;
        centerY = h / 2;
        mRadius = Math.min(w, h) / 2 - dp2px(getContext(), 10);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        //将画布原点移至view中心点
        canvas.translate(centerX, centerY);
        //绘制圆形
        drawCircle(canvas);
        //绘制刻度
        drawScale(canvas);
        //绘制字体
        drawText(canvas);
        //绘制时分秒指针
        drawClockPoint(canvas);
        canvas.restore();
        //延迟一秒刷新
        postInvalidateDelayed(1000);
    }

    /**
     * 圆
     *
     * @param canvas
     */
    private void drawCircle(Canvas canvas) {
        canvas.save();
        Path path = new Path();
        path.addCircle(0, 0, mRadius, Path.Direction.CW);
        mPaint.setStrokeWidth(dp2px(getContext(), 1));
        mPaint.setColor(Color.WHITE);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawPath(path, mPaint);
        canvas.restore();
    }

    /**
     * 刻度
     *
     * @param canvas
     */
    private void drawScale(Canvas canvas) {
        canvas.save();
        mPaint.setStyle(Paint.Style.FILL);
        //60个刻度
        for (int i = 0; i < 60; i++) {
            if (i % 5 == 0) {//整点的刻度
                mPaint.setColor(scaleBoldColor);
                mPaint.setStrokeWidth(scaleBoldWidth);
                canvas.drawLine(0, mRadius - dp2px(getContext(), 5), 0, mRadius - dp2px(getContext(), 20), mPaint);
            } else {
                mPaint.setColor(scaleColor);
                mPaint.setStrokeWidth(scaleWidth);
                canvas.drawLine(0, mRadius - dp2px(getContext(), 5), 0, mRadius - dp2px(getContext(), 15), mPaint);
            }
            //画完刻度之后再旋转画布
            canvas.rotate(minDegree);
        }
        canvas.restore();
    }

    /**
     * 字 drawText 第二个参数的意思默认是字符串的左边在屏幕的位置,第三个是指定这个字符baseline在屏幕上的位置
     *
     * @param canvas
     */
    private void drawText(Canvas canvas) {
        canvas.save();
        String[] text = new String[]{"12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"};
        mPaint.setColor(mTextColor);
        mPaint.setTextSize(mTextSize);
        mPaint.setStrokeWidth(dp2px(getContext(), 0.5f));
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setTextAlign(Paint.Align.CENTER);
        float degree12 = -90;//初始角度
        double radian;//弧度
        for (int i = 0; i < 12; i++) {
            Rect textBounds = new Rect();
            mPaint.getTextBounds(text[i], 0, text[i].length(), textBounds);
            radian = 2 * Math.PI / 360 * degree12;//角度转化为弧度
            canvas.drawText(text[i], (float) ((mRadius - dp2px(getContext(), 30)) * Math.cos(radian)),
                    (float) ((mRadius - dp2px(getContext(), 30)) * Math.sin(radian)) + dp2px(getContext(), 5), mPaint);
            degree12 += maxDegree;

        }
        canvas.restore();
    }

    /**
     * 指针
     *
     * @param canvas
     */
    private void drawClockPoint(Canvas canvas) {
        caculateDegree();
        //绘制时针
        canvas.save();
        canvas.rotate(hourDegree);
        mPaint.setColor(mHourColor);
        mPaint.setStrokeWidth(mHourWidth);
        RectF hourRectF = new RectF(-mHourWidth / 2, -mRadius + dp2px(getContext(), 80),
                mHourWidth / 2, mRadius / 9);
        canvas.drawRoundRect(hourRectF, 10, 10, mPaint);
        canvas.restore();

        //绘制分针
        canvas.save();
        canvas.rotate(minutDegree);
        mPaint.setColor(mMinuteColor);
        mPaint.setStrokeWidth(mMinuteWidth);
        RectF minuteRectF = new RectF(-mMinuteWidth / 2, -mRadius + dp2px(getContext(), 50),
                mMinuteWidth / 2, mRadius / 9);
        canvas.drawRoundRect(minuteRectF, 10, 10, mPaint);
        canvas.restore();

        //绘制秒针
        canvas.save();
        canvas.rotate(secondDegree);
        mPaint.setColor(mSecondColor);
        mPaint.setStrokeWidth(mSecondWidth);
        RectF secondRectF = new RectF(-mSecondWidth / 2, -mRadius + dp2px(getContext(), 30),
                mSecondWidth / 2, mRadius / 9);
        canvas.drawRoundRect(secondRectF, 10, 10, mPaint);
        canvas.restore();

        mPaint.setColor(mSecondColor);
        canvas.drawCircle(0, 0, dp2px(getContext(), 8), mPaint);
    }

    /**
     * 计算当前时分秒指针的角度
     */
    private void caculateDegree() {
        Calendar mCanendar = Calendar.getInstance();
        int hour = mCanendar.get(Calendar.HOUR_OF_DAY);//24小时制 ,HOUR的话是12小时制
        int minute = mCanendar.get(Calendar.MINUTE);
        int second = mCanendar.get(Calendar.SECOND);

        hourDegree = (hour % 24) * 360 / 12;
        minutDegree = minute * 360 / 60;
        secondDegree = second * 360 / 60;
    }

    /**
     * dp转px
     */
    public static int dp2px(Context ctx, float dp) {
        float density = ctx.getResources().getDisplayMetrics().density;
        int px = (int) (dp * density + 0.5f);// 4.9->5 4.4->4
        return px;
    }

到此:整个自定义表盘就绘制完成了,由于自定义能力不足,所以绘制的时候,也查阅了不少资料,虽然看着很简单,但是自己动手实现起来会有这样那样的问题,总之一句话:基础不牢,任重而道远哪!如有错误,请指正。谢谢!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值