帶你體驗Android自定義圓形刻度羅盤 儀表盤 實現指針動態改變


 

帶你體驗Android自定義圓形刻度羅盤 儀表盤 實現指針動態改變

近期有一個自定義View的功能,類似於儀表盤的模型,可以將指針動態指定到某一個刻度上,話不多說,先上圖

 

先說下思路

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

沒有梯子請點擊這里下載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM