實現一個速度儀表盤的控件

推薦個音樂下載app【佳語音樂下載】
https://gitlab.com/gaopinqiang/checkversion/raw/master/Music_Download.apk

開發有時會用到儀表盤(例如測速,類似汽車儀表盤),這裏我們自定義個View來實現這個功能,還支持指針動畫,避免過於生硬。

先給個效果圖:
在這裏插入圖片描述

下面是具體的實現代碼(在真機上測試通過):
DashboardView.java

package com.example.dashboardView;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.SweepGradient;
import android.os.Build;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;

import com.example.testgesturedetector.R;


/**
 * 自定義測速儀表盤
 * 直接繼承View需要處理wrap_content 時onMeasure方法的寬高
 */
public class DashboardView extends View {

    /** 定義一些缺省值 */
    private int mRadius; // 扇形半徑
    private int mStartAngle = 150; // 起始角度,180-30度(左邊平行夾角30度)
    private int mSweepAngle = 240; // 繪製角度,360+30度 = 150+240度
    private int mMin = 0; // 最小值
    private int mMax = 100; // 最大值
    private int mSection = 8; // 值域(mMax-mMin)等分份數
    private int mPortion = 5; // 一個mSection等分份數
    private String mHeaderText = ""; // 表頭 Mbps
    private int mVelocity = mMin; // 實時速度
    private int mStrokeWidth; // 畫筆寬度
    private int mLength1; // 長刻度的相對圓弧的長度
    private int mLength2; // 刻度讀數頂部的相對圓弧的長度
    private int mPLRadius; // 指針長半徑
    private int mPSRadius; // 指針短半徑

    private int mPadding;
    private float mCenterX, mCenterY; // 圓心座標
    private Paint mPaint;
    private RectF mRectFArc;
    private Path mPath;
    private RectF mRectFInnerArc;
    private Rect mRectText;
    private String[] mTexts;
    private int[] mColors;

    private Context mContext;

    public DashboardView(Context context) {
        this(context, null);
        mContext = context;
    }

    public DashboardView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        mContext = context;
    }

    public DashboardView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        init();
    }

    @TargetApi(Build.VERSION_CODES.M)
    private void init() {
        mStrokeWidth = dp2px(3);
        mLength1 = dp2px(8) + mStrokeWidth;
        mLength2 = mLength1 + dp2px(4);

        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStrokeCap(Paint.Cap.ROUND);

        mRectFArc = new RectF();
        mPath = new Path();
        mRectFInnerArc = new RectF();
        mRectText = new Rect();

        mTexts = new String[mSection + 1]; // 需要顯示mSection + 1個刻度讀數
        for (int i = 0; i < mTexts.length; i++) {
            int n = (mMax - mMin) / mSection;
            mTexts[i] = String.valueOf(mMin + i * n);
        }

        // TODO: 2020/4/20  重置刻度盤
        mTexts[0] = "0M";
        mTexts[1] = "1M";
        mTexts[2] = "2M";
        mTexts[3] = "5M";
        mTexts[4] = "10M";
        mTexts[5] = "20M";
        mTexts[6] = "50M";
        mTexts[7] = "80M";
        mTexts[8] = "100M";

        mColors = new int[]{
                mContext.getColor(R.color.color_green),
                mContext.getColor(R.color.color_yellow),
                mContext.getColor(R.color.color_red)
        };
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        mPadding = Math.max(
                Math.max(getPaddingLeft(), getPaddingTop()),
                Math.max(getPaddingRight(), getPaddingBottom())
        );
        setPadding(mPadding, mPadding, mPadding, mPadding);

        int width = resolveSize(dp2px(260), widthMeasureSpec);
        mRadius = (width - mPadding * 2 - mStrokeWidth * 2) / 2;

        // 由起始角度確定的高度
        float[] point1 = getCoordinatePoint(mRadius, mStartAngle);
        // 由結束角度確定的高度
        float[] point2 = getCoordinatePoint(mRadius, mStartAngle + mSweepAngle);
        int height = (int) Math.max(point1[1] + mRadius + mStrokeWidth * 2,
                point2[1] + mRadius + mStrokeWidth * 2);

        // TODO: 2020/4/20 需要處理高度
//        setMeasuredDimension(width, height + getPaddingTop() + getPaddingBottom());
        setMeasuredDimension(width, width);

        mCenterX = mCenterY = getMeasuredWidth() / 2f;
        mRectFArc.set(
                getPaddingLeft() + mStrokeWidth,
                getPaddingTop() + mStrokeWidth,
                getMeasuredWidth() - getPaddingRight() - mStrokeWidth,
                getMeasuredWidth() - getPaddingBottom() - mStrokeWidth
        );

        mPaint.setTextSize(sp2px(16));
        mPaint.getTextBounds("0", 0, "0".length(), mRectText);
        mRectFInnerArc.set(
                getPaddingLeft() + mLength2 + mRectText.height() + dp2px(30),
                getPaddingTop() + mLength2 + mRectText.height() + dp2px(30),
                getMeasuredWidth() - getPaddingRight() - mLength2 - mRectText.height() - dp2px(30),
                getMeasuredWidth() - getPaddingBottom() - mLength2 - mRectText.height() - dp2px(30)
        );

        mPLRadius = mRadius - dp2px(30);
        mPSRadius = dp2px(25);
    }

    @TargetApi(Build.VERSION_CODES.M)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        /** 畫測速儀背景色 */
//        canvas.drawColor(mContext.getColor(R.color.color_dark));

        /** 畫圓弧 最外層 */
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(mStrokeWidth);
        mPaint.setColor(mContext.getColor(R.color.appBlue));
        canvas.drawArc(mRectFArc, mStartAngle, mSweepAngle, false, mPaint);

        /**
         * 畫長刻度
         * 畫好起始角度的一條刻度後通過canvas繞着原點旋轉來畫剩下的長刻度
         */
        double cos = Math.cos(Math.toRadians(mStartAngle - 180));
        double sin = Math.sin(Math.toRadians(mStartAngle - 180));
        float x0 = (float) (mPadding + mStrokeWidth + mRadius * (1 - cos));
        float y0 = (float) (mPadding + mStrokeWidth + mRadius * (1 - sin));
        float x1 = (float) (mPadding + mStrokeWidth + mRadius - (mRadius - mLength1) * cos);
        float y1 = (float) (mPadding + mStrokeWidth + mRadius - (mRadius - mLength1) * sin);

        canvas.save();
        canvas.drawLine(x0, y0, x1, y1, mPaint);
        float angle = mSweepAngle * 1f / mSection;
        for (int i = 0; i < mSection; i++) {
            canvas.rotate(angle, mCenterX, mCenterY);
            canvas.drawLine(x0, y0, x1, y1, mPaint);
        }
        canvas.restore();

        /**
         * 畫短刻度
         * 同樣採用canvas的旋轉原理
         */
        canvas.save();
        mPaint.setStrokeWidth(mStrokeWidth / 2f);
        float x2 = (float) (mPadding + mStrokeWidth + mRadius - (mRadius - 2 * mLength1 / 3f) * cos);
        float y2 = (float) (mPadding + mStrokeWidth + mRadius - (mRadius - 2 * mLength1 / 3f) * sin);
        canvas.drawLine(x0, y0, x2, y2, mPaint);
        angle = mSweepAngle * 1f / (mSection * mPortion);
        for (int i = 1; i < mSection * mPortion; i++) {
            canvas.rotate(angle, mCenterX, mCenterY);
            if (i % mPortion == 0) { // 避免與長刻度畫重合
                continue;
            }
            canvas.drawLine(x0, y0, x2, y2, mPaint);
        }
        canvas.restore();

        /**
         * 畫長刻度讀數
         */
        mPaint.setTextSize(sp2px(16));
        mPaint.setStyle(Paint.Style.FILL);
        float α;
        float[] p;
        angle = mSweepAngle * 1f / mSection;
        for (int i = 0; i <= mSection; i++) {
            α = mStartAngle + angle * i;
            p = getCoordinatePoint(mRadius - mLength2, α);
            if (α % 360 > 135 && α % 360 < 225) {
                mPaint.setTextAlign(Paint.Align.LEFT);
            } else if ((α % 360 >= 0 && α % 360 < 45) || (α % 360 > 315 && α % 360 <= 360)) {
                mPaint.setTextAlign(Paint.Align.RIGHT);
            } else {
                mPaint.setTextAlign(Paint.Align.CENTER);
            }

            if (!TextUtils.isEmpty(mHeaderText)) {
                mPaint.getTextBounds(mHeaderText, 0, mTexts[i].length(), mRectText);
            }

            int txtH = mRectText.height();
            if (i <= 1 || i >= mSection - 1) {
                canvas.drawText(mTexts[i], p[0], p[1] + txtH / 2, mPaint);
            } else if (i == 3) {
                canvas.drawText(mTexts[i], p[0] + txtH / 2, p[1] + txtH, mPaint);
            } else if (i == mSection - 3) {
                canvas.drawText(mTexts[i], p[0] - txtH / 2, p[1] + txtH, mPaint);
            } else {
                canvas.drawText(mTexts[i], p[0], p[1] + txtH, mPaint);
            }
        }


        /** 畫內層圓弧 漸變色*/
        /**
         mPaint.setStrokeCap(Paint.Cap.SQUARE);
         mPaint.setStyle(Paint.Style.STROKE);
         mPaint.setStrokeWidth(dp2px(10));
         mPaint.setShader(generateSweepGradient());
         canvas.drawArc(mRectFInnerArc, mStartAngle + 1, mSweepAngle - 2, false, mPaint);
         */

        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setShader(null);

        /** 畫表頭 沒有表頭就不畫 */
        if (!TextUtils.isEmpty(mHeaderText)) {
            mPaint.setTextSize(sp2px(16));
            mPaint.setTextAlign(Paint.Align.CENTER);
            mPaint.getTextBounds(mHeaderText, 0, mHeaderText.length(), mRectText);
            canvas.drawText(mHeaderText, mCenterX, mCenterY - mRectText.height() * 3, mPaint);
        }

        /** 畫指針 */
        float a = mStartAngle + mSweepAngle * (mVelocity - mMin) / (mMax - mMin); // 指針與水平線夾角
        mPaint.setColor(mContext.getColor(R.color.appBlue));
        int r = mRadius / 10;
        canvas.drawCircle(mCenterX, mCenterY, dp2px(9), mPaint);//中心圓


        mPaint.setColor(mContext.getColor(R.color.color_light));
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(dp2px(3));
        canvas.drawCircle(mCenterX, mCenterY, dp2px(12), mPaint);//空心圓


        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setStrokeWidth(dp2px(2));
        mPaint.setColor(mContext.getColor(R.color.appBlue));
//        float[] p1 = getCoordinatePoint(mPLRadius, a);

//        Path path = new Path();
//        path.moveTo(p1[0], p1[1]);// 此點爲多邊形的起點
//        path.lineTo(mCenterX-10, mCenterY+10);
//        path.lineTo(mCenterX+10, mCenterY-10);
//        path.close(); // 使這些點構成封閉的多邊形
//        canvas.drawPath(path, mPaint);

        mPaint.setColor(mContext.getColor(R.color.appBlue));
        int d = dp2px(5); // 指針由兩個等腰三角形構成,d爲共底邊長的一半
        mPath.reset();
        float[] p1 = getCoordinatePoint(mPLRadius, a);
        mPath.moveTo(p1[0], p1[1]);
        float[] p2 = getCoordinatePoint(d, a-90);
        mPath.lineTo(p2[0], p2[1]);
        float[] p3 = getCoordinatePoint(d, a + 90);
        mPath.lineTo(p3[0], p3[1]);
//        float[] p4 = getCoordinatePoint(d, a + 180);
//        mPath.lineTo(p4[0], p4[1]);
        mPath.close();
        canvas.drawPath(mPath, mPaint);


//        canvas.drawLine(p1[0], p1[1], mCenterX, mCenterY, mPaint);
//        float[] p2 = getCoordinatePoint(mPSRadius, θ + 180);
//        canvas.drawLine(mCenterX, mCenterY, p2[0], p2[1], mPaint);

        /**  畫實時度數值 */
        /**
         mPaint.setColor(mContext.getColor(R.color.colorPrimary));
         mPaint.setStrokeWidth(dp2px(2));
         int xOffset = dp2px(22);
         if (mVelocity >= 100) {
         drawDigitalTube(canvas, mVelocity / 100, -xOffset);
         drawDigitalTube(canvas, (mVelocity - 100) / 10, 0);
         drawDigitalTube(canvas, mVelocity % 100 % 10, xOffset);
         } else if (mVelocity >= 10) {
         drawDigitalTube(canvas, -1, -xOffset);
         drawDigitalTube(canvas, mVelocity / 10, 0);
         drawDigitalTube(canvas, mVelocity % 10, xOffset);
         } else {
         drawDigitalTube(canvas, -1, -xOffset);
         drawDigitalTube(canvas, -1, 0);
         drawDigitalTube(canvas, mVelocity, xOffset);
         }
         */

    }

    /** 數碼管樣式 */
    private void drawDigitalTube(Canvas canvas, int num, int xOffset) {
        float x = mCenterX + xOffset;
        float y = mCenterY + dp2px(40);
        int lx = dp2px(5);
        int ly = dp2px(10);
        int gap = dp2px(2);

        // 1
        mPaint.setAlpha(num == -1 || num == 1 || num == 4 ? 25 : 255);
        canvas.drawLine(x - lx, y, x + lx, y, mPaint);
        // 2
        mPaint.setAlpha(num == -1 || num == 1 || num == 2 || num == 3 || num == 7 ? 25 : 255);
        canvas.drawLine(x - lx - gap, y + gap, x - lx - gap, y + gap + ly, mPaint);
        // 3
        mPaint.setAlpha(num == -1 || num == 5 || num == 6 ? 25 : 255);
        canvas.drawLine(x + lx + gap, y + gap, x + lx + gap, y + gap + ly, mPaint);
        // 4
        mPaint.setAlpha(num == -1 || num == 0 || num == 1 || num == 7 ? 25 : 255);
        canvas.drawLine(x - lx, y + gap * 2 + ly, x + lx, y + gap * 2 + ly, mPaint);
        // 5
        mPaint.setAlpha(num == -1 || num == 1 || num == 3 || num == 4 || num == 5 || num == 7
                || num == 9 ? 25 : 255);
        canvas.drawLine(x - lx - gap, y + gap * 3 + ly,
                x - lx - gap, y + gap * 3 + ly * 2, mPaint);
        // 6
        mPaint.setAlpha(num == -1 || num == 2 ? 25 : 255);
        canvas.drawLine(x + lx + gap, y + gap * 3 + ly,
                x + lx + gap, y + gap * 3 + ly * 2, mPaint);
        // 7
        mPaint.setAlpha(num == -1 || num == 1 || num == 4 || num == 7 ? 25 : 255);
        canvas.drawLine(x - lx, y + gap * 4 + ly * 2, x + lx, y + gap * 4 + ly * 2, mPaint);
    }

    private int dp2px(int dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
                Resources.getSystem().getDisplayMetrics());
    }

    private int sp2px(int sp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp,
                Resources.getSystem().getDisplayMetrics());
    }

    public float[] getCoordinatePoint(int radius, float angle) {
        float[] point = new float[2];

        double arcAngle = Math.toRadians(angle); //將角度轉換爲弧度
        if (angle < 90) {
            point[0] = (float) (mCenterX + Math.cos(arcAngle) * radius);
            point[1] = (float) (mCenterY + Math.sin(arcAngle) * radius);
        } else if (angle == 90) {
            point[0] = mCenterX;
            point[1] = mCenterY + radius;
        } else if (angle > 90 && angle < 180) {
            arcAngle = Math.PI * (180 - angle) / 180.0;
            point[0] = (float) (mCenterX - Math.cos(arcAngle) * radius);
            point[1] = (float) (mCenterY + Math.sin(arcAngle) * radius);
        } else if (angle == 180) {
            point[0] = mCenterX - radius;
            point[1] = mCenterY;
        } else if (angle > 180 && angle < 270) {
            arcAngle = Math.PI * (angle - 180) / 180.0;
            point[0] = (float) (mCenterX - Math.cos(arcAngle) * radius);
            point[1] = (float) (mCenterY - Math.sin(arcAngle) * radius);
        } else if (angle == 270) {
            point[0] = mCenterX;
            point[1] = mCenterY - radius;
        } else {
            arcAngle = Math.PI * (360 - angle) / 180.0;
            point[0] = (float) (mCenterX + Math.cos(arcAngle) * radius);
            point[1] = (float) (mCenterY - Math.sin(arcAngle) * radius);
        }

        return point;
    }

    private SweepGradient generateSweepGradient() {
        SweepGradient sweepGradient = new SweepGradient(mCenterX, mCenterY,
                mColors,
                new float[]{0, 140 / 360f, mSweepAngle / 360f}
        );

        Matrix matrix = new Matrix();
        matrix.setRotate(mStartAngle - 3, mCenterX, mCenterY);
        sweepGradient.setLocalMatrix(matrix);

        return sweepGradient;
    }

    public int getVelocity() {
        return mVelocity;
    }

    public void setVelocity(int velocity) {
        if (mVelocity == velocity || velocity < mMin || velocity > mMax) {
            return;
        }
        mVelocity = velocity;
        postInvalidate();
    }

    public int getmMax() {
        return mMax;
    }

    public int getmSection() {
        return mSection;
    }

    public int getmPortion() {
        return mPortion;
    }

}

xml佈局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.dashboardView.TestDashViewActivity">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_marginLeft="48dp"
        android:layout_marginRight="48dp"
        android:layout_height="300dp"
        android:layout_gravity="center"
        android:layout_marginTop="32dp"
        >
        <com.example.dashboardView.DashboardView
            android:id="@+id/dbv"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            />
    </LinearLayout>
</LinearLayout>

color.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#008577</color>
    <color name="colorPrimaryDark">#00574B</color>
    <color name="colorAccent">#D81B60</color>

    <color name="appBlue">#008577</color>
    <color name="color_light">#f4f4f4</color>
    <color name="color_green">#00ff00</color>
    <color name="color_yellow">#ffff00</color>
    <color name="color_red">#ff0000</color>

</resources>

使用:

package com.example.dashboardView;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.animation.LinearInterpolator;

import com.example.testgesturedetector.R;

public class TestDashViewActivity extends Activity {
    private static final String TAG = "TestDashViewActivity";

    private DashboardView dbv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_dash_view);

        dbv = findViewById(R.id.dbv);

        startShowSpeedAnimator("50Mbps");
        
    }


    private boolean isAnimFinished = true;//播放動畫控制
    private void startShowSpeedAnimator(String speedStr){
        float speed ;
        try {
            speed = Float.parseFloat(speedStr.replaceAll("[a-zA-Z\u4e00-\u9fa5]", "").replaceAll(" ",""));
        } catch (NumberFormatException e) {
            e.printStackTrace();
            Log.e(TAG,"startShowSpeedAnimator error = " + e.getMessage());
            return;
        }

        if(speed == 0){
            Log.d(TAG,"測試過程中統計的速率爲0,不顯示");
            return;
        }

        int velocity = getRealVelocity(speed);

        if (isAnimFinished) {
            Log.i(TAG,"velocity = " + velocity);
            ObjectAnimator animator = ObjectAnimator.ofInt(dbv, "mRealTimeValue", dbv.getVelocity() ,velocity);
            animator.setDuration(500).setInterpolator(new LinearInterpolator());

            animator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationStart(Animator animation) {
                    isAnimFinished = false;
                    Log.i(TAG,"onAnimationStart");
                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    Log.i(TAG,"onAnimationEnd");
                    isAnimFinished = true;
                }

                @Override
                public void onAnimationCancel(Animator animation) {
                    isAnimFinished = true;
                }
            });

            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    int value = (int) animation.getAnimatedValue();
                    dbv.setVelocity(value);
                    //Log.v(TAG,"onAnimationUpdate value = " + value);
                }
            });
            animator.start();
        }

    }

    private int getRealVelocity(float speed){
        float velocity = 0;
        if(dbv == null){
            return 0;
        }

        int max = dbv.getmMax();
        int section = dbv.getmSection();
        int portion = dbv.getmPortion();
        float bigScale = (float) (max*1.0/section);
        float smallScale = (float) (bigScale*1.0/portion);

        if(speed >= 100){
            velocity = 100;
        }else if(speed > 80){
            velocity = (float) (((speed-80)/4.0)*smallScale + 7*bigScale);
        }else if(speed > 50){
            velocity = (float) (((speed-50)/6.0)* smallScale+ 6*bigScale);
        }else if(speed > 20){
            velocity = (float) (((speed-20)/6.0)*smallScale + 5*bigScale);
        }else if(speed > 10){
            velocity = (float) (((speed-10)/2.0)*smallScale + 4*bigScale);
        }else if(speed > 5){
            velocity = (float) (((speed-5.0))*smallScale + 3*bigScale);
        }else if(speed > 2){
            velocity = (float) (((speed-2)/0.6)*smallScale + 2*bigScale);
        }else if(speed > 1){
            velocity = (float) (((speed-1)/0.2)*smallScale + 1*bigScale);
        }else if(speed > 0){
            velocity = (float) (((speed)/0.2)*smallScale);
        }

        Log.d(TAG,"speed = " + speed + " || velocity = " + velocity + " || (int)velocity = " + (int)velocity);
        return (int)velocity;
    }


}

功能已實現,但是並沒有做很好的封裝,有興趣的同學可以自行修改。可以根據自己的業務需要來定製自己的儀表盤。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章