美文网首页css相关
Android Canvas实现仪表盘功能

Android Canvas实现仪表盘功能

作者: BigP | 来源:发表于2019-03-14 16:22 被阅读45次

业务分析

主要是实现一个类似汽车码表的功能,一根指针,指向设定的数值,当值发生变化时,指针会随着数据摆动,定位至设定的数值。也许文字表述有点不清楚,直接上效果图吧!


效果图

实现思路

要实现这样一种效果,其实并不复杂,抽丝剥茧,一步一步来,首先第一步,为了以后复用,这一定是一个自定义View,然后根据所给画布的大小,先确定我们要画的仪表盘在哪个区域进行绘制,

  • 绘制区域的确定
    由于其他人要引用我们的View时,你不可能预先告诉他,我们的View有大小限制,你需要设置多少宽高,我们的自定义View才能正常显示,这显然是不现实的。因此为了预防这种情况的发生,我们需要先确定仪表盘的绘制区域:


    两种极限情况

    从上图中可以看到,不管你给定的宽高是多少,我们的绘制区域如图所示,先确定所能画的最大半径,由于并不是半圆,左右各超过了10°,所以在计算最大半径时,需要用到三角函数:

protected void onSizeChanged(int width, int height, int oldw, int oldh) {
    super.onSizeChanged(width, height, oldw, oldh);
    if (width > 0 && height > 0) {
        mRightDownX = width;
        mRightDownY = height;
        // 根据被赋予的画布大小,计算半径
        // 判断高度,高度的分界线为r+sinConner*r(此时的半径为width/2)
        if (mRightDownY >= ((1 + Math.sin(CONNER * Math.PI / 180)) * (mRightDownX / 2))) {
            mCircleRadius = mRightDownX / 2 - PADDING;
        } else {
            // 获取与要画圆的正切的正方形的边长
            float minLength = Math.min(mRightDownX, mRightDownY) - PADDING;
            // 获取圆的半径
            // 由于不是半圆,要多画10度,需要算出这10度的y的高度(高度/(1+sin10))
            mCircleRadius = (float) ((minLength / (1 + Math.sin(CONNER * Math.PI / 180))));
        }
        mCenterX = mStartX + mCircleRadius;
        mCenterY = mStartY + mCircleRadius;
        postInvalidate();
    }
}

onSizeChanged回调中,可以获得设置的View的宽高,利用这个宽高,计算我们仪表盘的最大半径mCircleRadius

  • 画布坐标系
    由于画仪表盘时,会有很多涉及角度和弧度的绘制操作,因此这边讲一下在canvas中的角度对应的坐标系:


    角度
  • 绘制基础表盘
    确定了半径之后,接下来的工作就容易很多了,首先画仪表盘的框架:阴影圆弧,最外层的弧线,中间的刻度和文字等。
private void drawOutArc(Canvas canvas) {
    initPaint();
    // 有阴影的大圆弧
    float radiusWidth = mCircleRadius / 2;
    mPaint.setStrokeWidth(radiusWidth);
    mPaint.setAntiAlias(true);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setColor(SysUtils.getColor(R.color.circle_board_bg));
    canvas.drawArc(new RectF(mStartX + radiusWidth / 2, mStartY + radiusWidth / 2,
            mCircleRadius * 2 + mStartX - radiusWidth / 2, mCircleRadius * 2 + mStartY - radiusWidth / 2),
        180 - CONNER, 180 + CONNER * 2, false, mPaint);
    // 最外面圆弧线条
    float strokeWidth = SysUtils.convertDpToPixel(1);
    mPaint.setStrokeWidth(strokeWidth);
    mPaint.setColor(SysUtils.getColor(R.color.circle_board_default_radius));
    canvas.drawArc(new RectF(mStartX, mStartY, mCircleRadius * 2 + mStartX,
        mCircleRadius * 2 + mStartY), 180 - CONNER, 180 + CONNER * 2, false, mPaint);
    // 中间的刻度
    mPaint.setColor(SysUtils.getColor(R.color.circle_board_middle_line));
    canvas.drawLine(mCenterX, mStartY + SysUtils.convertDpToPixel(2), mCenterX, mStartY + SysUtils.convertDpToPixel(7), mPaint);
    //写文字
    initPaint();
    int spSize = 13;
    float scaledSizeInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
        spSize, getResources().getDisplayMetrics());
    mPaint.setTextSize(scaledSizeInPixels);
    mPaint.setTextAlign(Paint.Align.CENTER);
    mPaint.setColor(SysUtils.getColor(R.color.circle_text_color));
    // 7是刻度底端,13是行高
    canvas.drawText("均值" + mText, mCenterX, mStartY + SysUtils.convertDpToPixel(7 + 13), mPaint);
}
  • 画中间的点和指针
private void drawCenter(Canvas canvas) {
    initPaint();
    // 中间圆点的半径
    int radius = SysUtils.convertDpToPixel(4);
    mPaint.setStrokeWidth(SysUtils.convertDpToPixel(10));
    mPaint.setAntiAlias(true);
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(SysUtils.getColor(R.color.circle_board_dot_red));
    // 画指针
    Path path = new Path();
    // 设计到三个点,组成三角指针的形状(因指针头要圆头,需要四个点定位)
    float fingureLength = mCircleRadius - SysUtils.convertDpToPixel(15);
    float[] point1 = CustomerViewUtils.getCoordinatePoint(mCenterX, mCenterY, fingureLength, mCurrentAngle - 0.4f);
    float[] point11 = CustomerViewUtils.getCoordinatePoint(mCenterX, mCenterY, fingureLength, mCurrentAngle + 0.4f);
    path.lineTo(point1[0], point1[1]);
    float[] point2 = CustomerViewUtils.getCoordinatePoint(mCenterX, mCenterY, radius / 2, mCurrentAngle - 90);
    path.lineTo(point2[0], point2[1]);
    float[] point3 = CustomerViewUtils.getCoordinatePoint(mCenterX, mCenterY, radius / 2, mCurrentAngle + 90);
    path.lineTo(point3[0], point3[1]);
    path.lineTo(point11[0], point11[1]);
    path.lineTo(point1[0], point1[1]);
    path.close();
    canvas.drawPath(path, mPaint);
    // 先画背景色的圆
    mPaint.setColor(SysUtils.getColor(R.color.circle_board_dot_out));
    canvas.drawCircle(mCenterX, mCenterY, radius + SysUtils.convertDpToPixel(2), mPaint);
    // 画中心红色的圆
    mPaint.setColor(SysUtils.getColor(R.color.circle_board_dot_red));
    canvas.drawCircle(mCenterX, mCenterY, radius, mPaint);

    mPaint.setStrokeCap(Paint.Cap.ROUND);

}

这里用到了一个计算方法,是通过角度来计算点在圆弧上的落点坐标的:

/**
 * 依圆心坐标,半径,扇形角度,计算出扇形终射线与圆弧交叉点的xy坐标
 * 0为水平 270是正上方
 *
 * @param radius   半径
 * @param cirAngle 角度
 * @return x,y
 */
public static float[] getCoordinatePoint(float centerX, float centerY, float radius, float cirAngle) {
    float[] point = new float[2];
    //将角度转换为弧度
    double arcAngle = Math.toRadians(cirAngle);
    if (cirAngle < 90) {
        point[0] = (float) (centerX + Math.cos(arcAngle) * radius);
        point[1] = (float) (centerY + Math.sin(arcAngle) * radius);
    } else if (cirAngle == 90) {
        point[0] = centerX;
        point[1] = centerY + radius;
    } else if (cirAngle > 90 && cirAngle < 180) {
        arcAngle = Math.PI * (180 - cirAngle) / 180.0;
        point[0] = (float) (centerX - Math.cos(arcAngle) * radius);
        point[1] = (float) (centerY + Math.sin(arcAngle) * radius);
    } else if (cirAngle == 180) {
        point[0] = centerX - radius;
        point[1] = centerY;
    } else if (cirAngle > 180 && cirAngle < 270) {
        arcAngle = Math.PI * (cirAngle - 180) / 180.0;
        point[0] = (float) (centerX - Math.cos(arcAngle) * radius);
        point[1] = (float) (centerY - Math.sin(arcAngle) * radius);
    } else if (cirAngle == 270) {
        point[0] = centerX;
        point[1] = centerY - radius;
    } else {
        arcAngle = Math.PI * (360 - cirAngle) / 180.0;
        point[0] = (float) (centerX + Math.cos(arcAngle) * radius);
        point[1] = (float) (centerY - Math.sin(arcAngle) * radius);
    }

    return point;
}
  • 画外层渐变色的圆弧
private void drawGradientArc(Canvas canvas) {
    initPaint();
    mPaint.setStrokeWidth(SysUtils.convertDpToPixel(1.5f));
    mPaint.setAntiAlias(true);
    mPaint.setStyle(Paint.Style.STROKE);
    int[] colors = {SysUtils.getColor(R.color.circle_start_color), SysUtils.getColor(R.color.circle_end_color)};
    // SweepGradient默认是从3点钟位置开始渐变的,需要旋转一哈
    SweepGradient gradient = new SweepGradient(mCenterX, mCenterY, colors, null);
    Matrix matrix = new Matrix();
    matrix.setRotate(180 - CONNER, mCenterX, mCenterY);
    gradient.setLocalMatrix(matrix);
    mPaint.setShader(gradient);
    // 当前圆弧对应的展开角度,就是指针的角度-170度
    canvas.drawArc(new RectF(mStartX, mStartY, mCircleRadius * 2 + mStartX,
        mCircleRadius * 2 + mStartY), 180 - CONNER, mCurrentAngle - 170, false, mPaint);
}
  • 设值和指针动画
    仪表盘算是基本完成了,剩下的就是数值的设置和指针的摆动动画了:
/**
 * 设置均值
 */
public void setMiddleValue(String deGree) {
    if (TextUtils.isEmpty(deGree)) {
        return;
    }
    float degree = SysUtils.parseFloat(deGree);
    if (degree > 0.0f) {
        mTotalValue = degree * 2;
        mText = deGree + "%";
        postInvalidate();
    } else if ("0".equals(deGree)) {
        mTotalValue = 0;
        mText = "0%";
        postInvalidate();
    }
}

设置当前的数值:

public void setCurrentValue(String value) {
    // 如果非法,直接设置为170度
    if (TextUtils.isEmpty(value)) {
        mFinalAngle = 180 - CONNER;
    } else {
        float v = SysUtils.parseFloat(value);
        // 计算数值对应的角度
        if (v <= 0.0f) {
            // 0对应180度
            mFinalAngle = 180;
        } else if (v > mTotalValue) {
            // 如果大于均值的两倍,直接拉满
            mFinalAngle = CONNER + 360;
        } else {
            if (mTotalValue > 0.0f) {
                float degree = 180 * (v / mTotalValue);
                mFinalAngle = 180 + degree;
            } else {
                // 总值小于等于0,设置的值大于0,应该直接拉满
                mFinalAngle = CONNER + 360;
            }
        }
    }
    ValueAnimator valueAnimator = ValueAnimator.ofFloat(mCurrentAngle, mFinalAngle);
    valueAnimator.setDuration(500);
    valueAnimator.setInterpolator(new Interpolator() {
        @Override
        public float getInterpolation(float v) {
            return 1 - (1 - v) * (1 - v) * (1 - v);
        }
    });
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            mCurrentAngle = (float) valueAnimator.getAnimatedValue();
            postInvalidate();
        }
    });
    valueAnimator.start();
}

总结

至此,一个完整的仪表盘功能就实现了,弄明白了基本思路,在此基础上加加减减,亦是一件很容易的事了~

源码地址

https://github.com/wx9265661/SmallDemos2

相关文章

网友评论

    本文标题:Android Canvas实现仪表盘功能

    本文链接:https://www.haomeiwen.com/subject/lmutmqtx.html