带你体验Android自定义圆形刻度罗盘 仪表盘 实现指针动态改变

近期有一个自定义View的功能,类似于仪表盘的模型,可以将指针动态指定到某一个刻度上,话不多说,先上图

带你体验Android自定义圆形刻度罗盘 仪表盘 实现指针动态改变-LMLPHP

先说下思路

1.先获取自定义的一些属性,初始化一些资源

2.在onMeasure中测量控件的具体大小

3.然后就在onDraw中先绘制有渐变色的圆弧形色带

4.再绘制几个大的刻度和刻度值

5.再绘制两个大刻度之间的小刻度

6.再绘制处于正中间的圆和三角形指针

7.最后绘制实时值

其实这也从侧面体现了一个自定义view的流程

1.继承View,重写构造方法

2.加载自定义属性和其它资源

3.重写onMeasure方法去确定控件的大小

4.重写onDraw方法去绘制

5.如果有点击事件的话,还得重写onTouchEvent或者dispatchTouchEvent去处理点击事件

来上代码吧,具体注释已经写的很详细了

  1.  
    public class NoiseboardView extends View {
  2.  
     
  3.  
    final String TAG = "NoiseboardView";
  4.  
     
  5.  
    private int mRadius; // 圆弧半径
  6.  
    private int mBigSliceCount; // 大份数
  7.  
    private int mScaleCountInOneBigScale; // 相邻两个大刻度之间的小刻度个数
  8.  
    private int mScaleColor; // 刻度颜色
  9.  
    private int mScaleTextSize; // 刻度字体大小
  10.  
    private String mUnitText = ""; // 单位
  11.  
    private int mUnitTextSize; // 单位字体大小
  12.  
    private int mMinValue; // 最小值
  13.  
    private int mMaxValue; // 最大值
  14.  
    private int mRibbonWidth; // 色条宽
  15.  
     
  16.  
    private int mStartAngle; // 起始角度
  17.  
    private int mSweepAngle; // 扫过角度
  18.  
     
  19.  
    private int mPointerRadius; // 三角形指针半径
  20.  
    private int mCircleRadius; // 中心圆半径
  21.  
     
  22.  
    private float mRealTimeValue = 0.0f; // 实时值
  23.  
     
  24.  
    private int mBigScaleRadius; // 大刻度半径
  25.  
    private int mSmallScaleRadius; // 小刻度半径
  26.  
    private int mNumScaleRadius; // 数字刻度半径
  27.  
     
  28.  
    private int mViewColor_green; // 字体颜色
  29.  
    private int mViewColor_yellow; // 字体颜色
  30.  
    private int mViewColor_orange; // 字体颜色
  31.  
    private int mViewColor_red; // 字体颜色
  32.  
     
  33.  
    private int mViewWidth; // 控件宽度
  34.  
    private int mViewHeight; // 控件高度
  35.  
    private float mCenterX;//中心点圆坐标x
  36.  
    private float mCenterY;//中心点圆坐标y
  37.  
     
  38.  
    private Paint mPaintScale;//圆盘上大小刻度画笔
  39.  
    private Paint mPaintScaleText;//圆盘上刻度值画笔
  40.  
    private Paint mPaintCirclePointer;//绘制中心圆,指针
  41.  
    private Paint mPaintValue;//绘制实时值
  42.  
    private Paint mPaintRibbon;//绘制色带
  43.  
     
  44.  
    private RectF mRectRibbon;//存储色带的矩形数据
  45.  
    private Rect mRectScaleText;//存储刻度值的矩形数据
  46.  
    private Path path;//绘制指针的路径
  47.  
     
  48.  
    private int mSmallScaleCount; // 小刻度总数
  49.  
    private float mBigScaleAngle; // 相邻两个大刻度之间的角度
  50.  
    private float mSmallScaleAngle; // 相邻两个小刻度之间的角度
  51.  
     
  52.  
    private String[] mGraduations; // 每个大刻度的刻度值
  53.  
    private float initAngle;//指针实时角度
  54.  
     
  55.  
    private SweepGradient mSweepGradient ;//设置渐变
  56.  
    private int[] color = new int[7];//渐变颜色组
  57.  
     
  58.  
    public NoiseboardView(Context context) {
  59.  
    this(context, null);
  60.  
    }
  61.  
     
  62.  
    public NoiseboardView(Context context, AttributeSet attrs) {
  63.  
    this(context, attrs, 0);
  64.  
    }
  65.  
     
  66.  
    public NoiseboardView(Context context, AttributeSet attrs, int defStyleAttr) {
  67.  
    super(context, attrs, defStyleAttr);
  68.  
    //自定义属性
  69.  
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NoiseboardView, defStyleAttr, 0);
  70.  
     
  71.  
    mRadius = a.getDimensionPixelSize(R.styleable.NoiseboardView_radius, dpToPx(80));
  72.  
    mBigSliceCount = a.getInteger(R.styleable.NoiseboardView_bigSliceCount, 5);
  73.  
    mScaleCountInOneBigScale = a.getInteger(R.styleable.NoiseboardView_sliceCountInOneBigSlice, 5);
  74.  
    mScaleColor = a.getColor(R.styleable.NoiseboardView_scaleColor, Color.WHITE);
  75.  
    mScaleTextSize = a.getDimensionPixelSize(R.styleable.NoiseboardView_scaleTextSize, spToPx(12));
  76.  
    mUnitText = a.getString(R.styleable.NoiseboardView_unitText);
  77.  
    mUnitTextSize = a.getDimensionPixelSize(R.styleable.NoiseboardView_unitTextSize, spToPx(14));
  78.  
    mMinValue = a.getInteger(R.styleable.NoiseboardView_minValue, 0);
  79.  
    mMaxValue = a.getInteger(R.styleable.NoiseboardView_maxValue, 150);
  80.  
    mRibbonWidth = a.getDimensionPixelSize(R.styleable.NoiseboardView_ribbonWidth, 0);
  81.  
     
  82.  
    a.recycle();
  83.  
    init();
  84.  
    }
  85.  
     
  86.  
    private void init() {
  87.  
     
  88.  
    //起始角度是从水平正方向即(钟表3点钟方向)开始从0算的,扫过的角度是按顺时针方向算
  89.  
    mStartAngle = 175;
  90.  
    mSweepAngle = 190;
  91.  
     
  92.  
    mPointerRadius = mRadius / 3 * 2;
  93.  
    mCircleRadius = mRadius / 17;
  94.  
     
  95.  
    mSmallScaleRadius = mRadius - dpToPx(10);
  96.  
    mBigScaleRadius = mRadius - dpToPx(18);
  97.  
    mNumScaleRadius = mRadius - dpToPx(20);
  98.  
     
  99.  
    mSmallScaleCount = mBigSliceCount * 5;
  100.  
    mBigScaleAngle = mSweepAngle / (float) mBigSliceCount;
  101.  
    mSmallScaleAngle = mBigScaleAngle / mScaleCountInOneBigScale;
  102.  
    mGraduations = getMeasureNumbers();
  103.  
     
  104.  
    //确定控件的宽度 padding值,在构造方法执行完就被赋值
  105.  
    mViewWidth = getPaddingLeft() + mRadius * 2 + getPaddingRight() + dpToPx(4);
  106.  
    mViewHeight = mViewWidth;
  107.  
    mCenterX = mViewWidth / 2.0f;
  108.  
    mCenterY = mViewHeight / 2.0f;
  109.  
     
  110.  
    mPaintScale = new Paint();
  111.  
    mPaintScale.setAntiAlias(true);
  112.  
    mPaintScale.setColor(mScaleColor);
  113.  
    mPaintScale.setStyle(Paint.Style.STROKE);
  114.  
    mPaintScale.setStrokeCap(Paint.Cap.ROUND);
  115.  
     
  116.  
    mPaintScaleText = new Paint();
  117.  
    mPaintScaleText.setAntiAlias(true);
  118.  
    mPaintScaleText.setColor(mScaleColor);
  119.  
    mPaintScaleText.setStyle(Paint.Style.STROKE);
  120.  
     
  121.  
    mPaintCirclePointer = new Paint();
  122.  
    mPaintCirclePointer.setAntiAlias(true);
  123.  
     
  124.  
    mRectScaleText = new Rect();
  125.  
    path = new Path();
  126.  
     
  127.  
    mPaintValue = new Paint();
  128.  
    mPaintValue.setAntiAlias(true);
  129.  
    mPaintValue.setStyle(Paint.Style.STROKE);
  130.  
    mPaintValue.setTextAlign(Paint.Align.CENTER);
  131.  
    mPaintValue.setTextSize(mUnitTextSize);
  132.  
     
  133.  
    initAngle = getAngleFromResult(mRealTimeValue);
  134.  
     
  135.  
    mViewColor_green = getResources().getColor(R.color.green_value);
  136.  
    mViewColor_yellow = getResources().getColor(R.color.yellow_value);
  137.  
    mViewColor_orange = getResources().getColor(R.color.orange_value);
  138.  
    mViewColor_red = getResources().getColor(R.color.red_value);
  139.  
    color[0] = mViewColor_red;
  140.  
    color[1] = mViewColor_red;
  141.  
    color[2] = mViewColor_green;
  142.  
    color[3] = mViewColor_green;
  143.  
    color[4] = mViewColor_yellow;
  144.  
    color[5] = mViewColor_orange;
  145.  
    color[6] = mViewColor_red;
  146.  
     
  147.  
    //色带画笔
  148.  
    mPaintRibbon = new Paint();
  149.  
    mPaintRibbon.setAntiAlias(true);
  150.  
    mPaintRibbon.setStyle(Paint.Style.STROKE);
  151.  
    mPaintRibbon.setStrokeWidth(mRibbonWidth);
  152.  
    mSweepGradient = new SweepGradient(mCenterX, mCenterY,color,null);
  153.  
    mPaintRibbon.setShader(mSweepGradient);//设置渐变 从X轴正方向取color数组颜色开始渐变
  154.  
     
  155.  
    if (mRibbonWidth > 0) {
  156.  
    int r = mRadius - mRibbonWidth / 2 + dpToPx(1) ;
  157.  
    mRectRibbon = new RectF(mCenterX - r, mCenterY - r, mCenterX + r, mCenterY + r);
  158.  
    }
  159.  
    }
  160.  
     
  161.  
    /**
  162.  
    * 确定每个大刻度的值
  163.  
    * @return
  164.  
    */
  165.  
    private String[] getMeasureNumbers() {
  166.  
    String[] strings = new String[mBigSliceCount + 1];
  167.  
    for (int i = 0; i <= mBigSliceCount; i++) {
  168.  
    if (i == 0) {
  169.  
    strings[i] = String.valueOf(mMinValue);
  170.  
    } else if (i == mBigSliceCount) {
  171.  
    strings[i] = String.valueOf(mMaxValue);
  172.  
    } else {
  173.  
    strings[i] = String.valueOf(((mMaxValue - mMinValue) / mBigSliceCount) * i);
  174.  
    }
  175.  
    }
  176.  
    return strings;
  177.  
    }
  178.  
     
  179.  
    /**
  180.  
    * <dt>UNSPECIFIED : 0 << 30 = 0</dt>
  181.  
    * <dd>
  182.  
    * 父控件没有对子控件做限制,子控件可以是自己想要的尺寸
  183.  
    * 其实就是子空间在布局里没有设置宽高,但布局里添加控件都要设置宽高,所以这种情况暂时没碰到
  184.  
    * </dd>
  185.  
    *
  186.  
    * <dt>EXACTLY : 1 << 30 = 1073741824</dt>
  187.  
    * <dd>
  188.  
    * 父控件给子控件决定了确切大小,子控件将被限定在给定的边界里。
  189.  
    * 如果是填充父窗体(match_parent),说明父控件已经明确知道子控件想要多大的尺寸了,也是这种模式
  190.  
    * </dd>
  191.  
    *
  192.  
    * <dt>AT_MOST : 2 << 30 = -2147483648</dt>
  193.  
    * <dd>
  194.  
    * 在布局设置wrap_content,父控件并不知道子控件到底需要多大尺寸(具体值),
  195.  
    * 需要子控件在onMeasure测量之后再让父控件给他一个尽可能大的尺寸以便让内容全部显示
  196.  
    * 如果在onMeasure没有指定控件大小,默认会填充父窗体,因为在view的onMeasure源码中,
  197.  
    * AT_MOST(相当于wrap_content )和EXACTLY (相当于match_parent )两种情况返回的测量宽高都是specSize,
  198.  
    * 而这个specSize正是父控件剩余的宽高,所以默认onMeasure方法中wrap_content 和match_parent 的效果是一样的,都是填充剩余的空间。
  199.  
    * </dd>
  200.  
    *
  201.  
    * @param widthMeasureSpec
  202.  
    * @param heightMeasureSpec
  203.  
    */
  204.  
    @Override
  205.  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  206.  
     
  207.  
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);//从约束规范中获取模式
  208.  
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);//从约束规范中获取尺寸
  209.  
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
  210.  
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
  211.  
     
  212.  
    //在布局中设置了具体值
  213.  
    if (widthMode == MeasureSpec.EXACTLY)
  214.  
    mViewWidth = widthSize;
  215.  
     
  216.  
    //在布局中设置 wrap_content,控件就取能完全展示内容的宽度(同时需要考虑屏幕的宽度)
  217.  
    if (widthMode == MeasureSpec.AT_MOST)
  218.  
    mViewWidth = Math.min(mViewWidth, widthSize);
  219.  
     
  220.  
    if (heightMode == MeasureSpec.EXACTLY) {
  221.  
    mViewHeight = heightSize;
  222.  
    } else {
  223.  
     
  224.  
    float[] point1 = getCoordinatePoint(mRadius, mStartAngle);
  225.  
    float[] point2 = getCoordinatePoint(mRadius, mStartAngle + mSweepAngle);
  226.  
    float maxY = Math.max(Math.abs(point1[1]) - mCenterY, Math.abs(point2[1]) - mCenterY);
  227.  
    float f = mCircleRadius + dpToPx(2) + dpToPx(25) ;
  228.  
    float max = Math.max(maxY, f);
  229.  
    mViewHeight = (int) (max + mRadius + getPaddingTop() + getPaddingBottom() + dpToPx(2) * 2);
  230.  
     
  231.  
    if (heightMode == MeasureSpec.AT_MOST)
  232.  
    mViewHeight = Math.min(mViewHeight, heightSize);
  233.  
    }
  234.  
     
  235.  
    //保存测量宽度和测量高度
  236.  
    setMeasuredDimension(mViewWidth, mViewHeight);
  237.  
    }
  238.  
     
  239.  
     
  240.  
    @Override
  241.  
    protected void onDraw(Canvas canvas) {
  242.  
    // 绘制色带
  243.  
    canvas.drawArc(mRectRibbon, 170, 199, false, mPaintRibbon);
  244.  
     
  245.  
    mPaintScale.setStrokeWidth(dpToPx(2));
  246.  
    for (int i = 0; i <= mBigSliceCount; i++) {
  247.  
    //绘制大刻度
  248.  
    float angle = i * mBigScaleAngle + mStartAngle;
  249.  
    float[] point1 = getCoordinatePoint(mRadius, angle);
  250.  
    float[] point2 = getCoordinatePoint(mBigScaleRadius, angle);
  251.  
    canvas.drawLine(point1[0], point1[1], point2[0], point2[1], mPaintScale);
  252.  
     
  253.  
    //绘制圆盘上的数字
  254.  
    mPaintScaleText.setTextSize(mScaleTextSize);
  255.  
    String number = mGraduations[i];
  256.  
    mPaintScaleText.getTextBounds(number, 0, number.length(), mRectScaleText);
  257.  
    if (angle % 360 > 135 && angle % 360 < 215) {
  258.  
    mPaintScaleText.setTextAlign(Paint.Align.LEFT);
  259.  
    } else if ((angle % 360 >= 0 && angle % 360 < 45) || (angle % 360 > 325 && angle % 360 <= 360)) {
  260.  
    mPaintScaleText.setTextAlign(Paint.Align.RIGHT);
  261.  
    } else {
  262.  
    mPaintScaleText.setTextAlign(Paint.Align.CENTER);
  263.  
    }
  264.  
    float[] numberPoint = getCoordinatePoint(mNumScaleRadius, angle);
  265.  
    if (i == 0 || i == mBigSliceCount) {
  266.  
    canvas.drawText(number, numberPoint[0], numberPoint[1] + (mRectScaleText.height() / 2), mPaintScaleText);
  267.  
    } else {
  268.  
    canvas.drawText(number, numberPoint[0], numberPoint[1] + mRectScaleText.height(), mPaintScaleText);
  269.  
    }
  270.  
    }
  271.  
     
  272.  
    //绘制小的子刻度
  273.  
    mPaintScale.setStrokeWidth(dpToPx(1));
  274.  
    for (int i = 0; i < mSmallScaleCount; i++) {
  275.  
    if (i % mScaleCountInOneBigScale != 0) {
  276.  
    float angle = i * mSmallScaleAngle + mStartAngle;
  277.  
    float[] point1 = getCoordinatePoint(mRadius, angle);
  278.  
    float[] point2 = getCoordinatePoint(mSmallScaleRadius, angle);
  279.  
     
  280.  
    mPaintScale.setStrokeWidth(dpToPx(1));
  281.  
    canvas.drawLine(point1[0], point1[1], point2[0], point2[1], mPaintScale);
  282.  
    }
  283.  
    }
  284.  
     
  285.  
    if (mRealTimeValue <= 40) {
  286.  
    mPaintValue.setColor(mViewColor_green);
  287.  
    mPaintCirclePointer.setColor(mViewColor_green);
  288.  
    } else if (mRealTimeValue > 40 && mRealTimeValue <= 90) {
  289.  
    mPaintValue.setColor(mViewColor_yellow);
  290.  
    mPaintCirclePointer.setColor(mViewColor_yellow);
  291.  
    } else if (mRealTimeValue > 90 && mRealTimeValue <= 120) {
  292.  
    mPaintValue.setColor(mViewColor_orange);
  293.  
    mPaintCirclePointer.setColor(mViewColor_orange);
  294.  
    } else {
  295.  
    mPaintValue.setColor(mViewColor_red);
  296.  
    mPaintCirclePointer.setColor(mViewColor_red);
  297.  
    }
  298.  
     
  299.  
    //绘制中心点的圆
  300.  
    mPaintCirclePointer.setStyle(Paint.Style.STROKE);
  301.  
    mPaintCirclePointer.setStrokeWidth(dpToPx(4));
  302.  
    canvas.drawCircle(mCenterX, mCenterY, mCircleRadius + dpToPx(3), mPaintCirclePointer);
  303.  
     
  304.  
    //绘制三角形指针
  305.  
    path.reset();
  306.  
    mPaintCirclePointer.setStyle(Paint.Style.FILL);
  307.  
    float[] point1 = getCoordinatePoint(mCircleRadius / 2, initAngle + 90);
  308.  
    path.moveTo(point1[0], point1[1]);
  309.  
    float[] point2 = getCoordinatePoint(mCircleRadius / 2, initAngle - 90);
  310.  
    path.lineTo(point2[0], point2[1]);
  311.  
    float[] point3 = getCoordinatePoint(mPointerRadius, initAngle);
  312.  
    path.lineTo(point3[0], point3[1]);
  313.  
    path.close();
  314.  
    canvas.drawPath(path, mPaintCirclePointer);
  315.  
     
  316.  
    // 绘制三角形指针底部的圆弧效果
  317.  
    canvas.drawCircle((point1[0] + point2[0]) / 2, (point1[1] + point2[1]) / 2, mCircleRadius / 2, mPaintCirclePointer);
  318.  
     
  319.  
    //绘制实时值
  320.  
    canvas.drawText(trimFloat(mRealTimeValue)+" "+ mUnitText, mCenterX, mCenterY - mRadius / 3 , mPaintValue);
  321.  
    }
  322.  
     
  323.  
    private int dpToPx(int dp) {
  324.  
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
  325.  
    }
  326.  
     
  327.  
    private int spToPx(int sp) {
  328.  
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
  329.  
    }
  330.  
     
  331.  
    /**
  332.  
    * 依圆心坐标,半径,扇形角度,计算出扇形终射线与圆弧交叉点的xy坐标
  333.  
    */
  334.  
    public float[] getCoordinatePoint(int radius, float cirAngle) {
  335.  
    float[] point = new float[2];
  336.  
     
  337.  
    double arcAngle = Math.toRadians(cirAngle); //将角度转换为弧度
  338.  
    if (cirAngle < 90) {
  339.  
    point[0] = (float) (mCenterX + Math.cos(arcAngle) * radius);
  340.  
    point[1] = (float) (mCenterY + Math.sin(arcAngle) * radius);
  341.  
    } else if (cirAngle == 90) {
  342.  
    point[0] = mCenterX;
  343.  
    point[1] = mCenterY + radius;
  344.  
    } else if (cirAngle > 90 && cirAngle < 180) {
  345.  
    arcAngle = Math.PI * (180 - cirAngle) / 180.0;
  346.  
    point[0] = (float) (mCenterX - Math.cos(arcAngle) * radius);
  347.  
    point[1] = (float) (mCenterY + Math.sin(arcAngle) * radius);
  348.  
    } else if (cirAngle == 180) {
  349.  
    point[0] = mCenterX - radius;
  350.  
    point[1] = mCenterY;
  351.  
    } else if (cirAngle > 180 && cirAngle < 270) {
  352.  
    arcAngle = Math.PI * (cirAngle - 180) / 180.0;
  353.  
    point[0] = (float) (mCenterX - Math.cos(arcAngle) * radius);
  354.  
    point[1] = (float) (mCenterY - Math.sin(arcAngle) * radius);
  355.  
    } else if (cirAngle == 270) {
  356.  
    point[0] = mCenterX;
  357.  
    point[1] = mCenterY - radius;
  358.  
    } else {
  359.  
    arcAngle = Math.PI * (360 - cirAngle) / 180.0;
  360.  
    point[0] = (float) (mCenterX + Math.cos(arcAngle) * radius);
  361.  
    point[1] = (float) (mCenterY - Math.sin(arcAngle) * radius);
  362.  
    }
  363.  
     
  364.  
    Log.e("getCoordinatePoint","radius="+radius+",cirAngle="+cirAngle+",point[0]="+point[0]+",point[1]="+point[1]);
  365.  
    return point;
  366.  
    }
  367.  
     
  368.  
    /**
  369.  
    * 通过实时数值得到指针角度
  370.  
    */
  371.  
    private float getAngleFromResult(float result) {
  372.  
    if (result > mMaxValue)
  373.  
    return 360.0f;
  374.  
    return mSweepAngle * (result - mMinValue) / (mMaxValue - mMinValue) + mStartAngle;
  375.  
    }
  376.  
     
  377.  
    /**
  378.  
    * float类型如果小数点后为零则显示整数否则保留
  379.  
    */
  380.  
    public static String trimFloat(float value) {
  381.  
    if (Math.round(value) - value == 0) {
  382.  
    return String.valueOf((long) value);
  383.  
    }
  384.  
    return String.valueOf(value);
  385.  
    }
  386.  
     
  387.  
     
  388.  
    public float getRealTimeValue() {
  389.  
    return mRealTimeValue;
  390.  
    }
  391.  
     
  392.  
    /**
  393.  
    * 实时设置读数值
  394.  
    * @param realTimeValue
  395.  
    */
  396.  
    public void setRealTimeValue(float realTimeValue) {
  397.  
    if (realTimeValue > mMaxValue) return;
  398.  
    mRealTimeValue = realTimeValue;
  399.  
    initAngle = getAngleFromResult(mRealTimeValue);
  400.  
    invalidate();
  401.  
    }
  402.  
     
  403.  
    }

具体代码请看Github

没有梯子请点击这里下载

05-11 13:27