Android widget - 完全可配的炫酷仪表盘
Widget部件介绍

项目里需要一个完全可自定义的仪表盘,其中需要分三个警示断来提示,这里以温度为例,0°C以下用蓝色刻度,70°C以下用绿色刻度,70°C以上红色刻度。

由于需要展示十几个这种图标,并且更新频率接近实时,所以对性能要求比较高。

改了几版后达到要求,最终效果图如下:

为了保持移植到其他项目的便利性,未使用xml的属性值来定义。   并且只有一个文件,复制粘贴简单;大部分参数都设有默认值,并且支持所有的参数可以通过配置来修改。

使用说明及Demo

GaugeView gaugeView = findViewById(R.id.gaugu);
        
GaugeView.Config cfg = new GaugeView.Config();
cfg.labelText = "温度"; // 所有项均可在此配置修改
gaugeView.config(cfg); 

// 中间值格式,最大值,最小值,刻度线(dialStep的值,必须被最大值减最小值整除),第一个警戒线,第二个警戒线
gaugeView.init("%.1f°C", 100f, -30f, 10, 10, 70);

gaugeView.updateVal(30f);

GaugeView.java 源代码

package org.tcshare.widgets;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.RectF;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;

import androidx.annotation.Nullable;

import java.text.DecimalFormat;

/**
 * @author 爱T鱼
 * @date 2021年1月5日
 */
public class GaugeView extends View {
    protected int bgRound1 = Color.parseColor("#99122a73");
    protected int bgRound2 = Color.parseColor("#66122a73");
    protected int bgRound3 = Color.parseColor("#22142f80");
    protected int bgRound4 = Color.parseColor("#0e2267");
    protected int circle1 = Color.parseColor("#0d1d4c");
    protected int bgRadius1;
    protected int bgRadius2;
    protected int bgRadius3;
    protected int bgRadius4;
    protected int circleRadius1;
    protected int circleRadius2;

    // 背景圆画笔
    protected Paint roundPaint;
    protected int mWidth;
    protected int mHeight;
    protected int mCx;
    protected int mCy;
    protected int radius;
    // 刻度画笔
    protected Paint pointerPaint;
    protected float textSizeDial = 14; // 刻度字体大小
    protected Paint.FontMetrics pointerFontMetrics;
    // 标题画笔
    protected Paint labelPaint;
    // 长刻度线半径
    protected int radiusDial;
    protected float maxVal = 150f;
    protected float minVal = -50f;

    protected int lowDialLimit = 20; // 低段温度警戒线
    protected int highDialLimit = 80; // 中段警戒线
    protected int colorDialLower = Color.parseColor("#00faff"); // 低中高三个色段表盘刻度
    protected int colorDialMiddle = Color.GREEN;
    protected int colorDialHigh = Color.RED;

    protected int lengthLDial = 16;// 长刻度线的长度
    protected int lengthSDial = 8;// 短刻度线的长度
    protected float strokeLDial = 3;
    protected float strokeSDial = 1;
    protected float circleRadius1StrokeWidth = 2f;
    protected float circleRadius2StrokeWidth = 4f;
    protected float pointerLineDegree = 135; // 多少角度
    protected float arcStartDegree = pointerLineDegree; // 弧线起始角度
    protected Paint valPaint;

    protected Paint.FontMetrics labelFontMetrics;
    protected Paint.FontMetrics valFontMetrics;
    protected String valText = "";
    protected float realVal; // 真实的值
    protected float valTextSize = 40f;// 值字体大小
    protected int valTextColor = Color.WHITE;// 值字体颜色
    protected String labelText = "";
    protected float labelTextSize = 20f;// 标签字体
    protected int labelTextColor = Color.WHITE;// 标签字体颜色
    protected Paint arcPaint;
    protected RectF mArcRect = new RectF(), mCircleArcRect1 = new RectF(), mCircleArcRect2 = new RectF(); // 指针(半扇形)及描边线
    protected float arcPaintStrokeWidth = 2f; // 描边宽度
    protected int arcColor = Color.parseColor("#ff8e00");
    protected int[] colors = new int[]{Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT, arcColor};
    protected int[] colorsArcLine = new int[]{arcColor, arcColor, Color.TRANSPARENT};
    protected Shader arcShader;
    protected Shader arcLineShader;
    protected float sweepAngle = 0;
    protected String formatter = "%.0f";
    protected int drawDialNum = 100;
    protected int dialStep = 10; // 分度
    protected float valRange;
    protected float stepDegree;
    protected float sRoateDegree;
    protected DecimalFormat decimalFormat = new DecimalFormat(".0");
    private Bitmap pLineCache; // 绘制点线缓存,该步骤耗时长

    public GaugeView(Context context) {
        super(context);
        init();
    }


    public GaugeView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public GaugeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public GaugeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }


    protected void init() {
        // 初始化圆背景画笔
        roundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        // 扫描扇
        arcPaint = new Paint();
        arcPaint.setAntiAlias(true);


        // 刻度
        pointerPaint = new Paint();
        pointerPaint.setAntiAlias(true);
        pointerPaint.setTextSize(textSizeDial);
        pointerPaint.setTextAlign(Paint.Align.CENTER);
        pointerFontMetrics = pointerPaint.getFontMetrics();

        // 标题
        labelPaint = new Paint();
        labelPaint.setAntiAlias(true);
        labelPaint.setTextAlign(Paint.Align.CENTER);
        labelPaint.setFakeBoldText(true);
        labelPaint.setTextSize(labelTextSize);
        labelPaint.setColor(labelTextColor);
        labelFontMetrics = labelPaint.getFontMetrics();

        // 值
        valPaint = new Paint();
        valPaint.setAntiAlias(true);
        valPaint.setTextAlign(Paint.Align.CENTER);
//        valPaint.setFakeBoldText(true);
        valPaint.setTextSize(valTextSize);
        valPaint.setColor(valTextColor);
        valFontMetrics = valPaint.getFontMetrics();

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawBG(canvas);
        drawArc(canvas);
        drawPointerLine(canvas);
        drawValText(canvas);
        drawLabelText(canvas);

    }

    private void drawArc(Canvas canvas) {
        canvas.save();
        arcPaint.setStyle(Paint.Style.FILL);
        arcPaint.setShader(arcShader);
        canvas.drawArc(mArcRect, arcStartDegree, sweepAngle, true, arcPaint);

        arcPaint.setStyle(Paint.Style.STROKE);
        arcPaint.setColor(arcColor);
        arcPaint.setShader(null);
        arcPaint.setStrokeWidth(arcPaintStrokeWidth);
        canvas.drawArc(mArcRect, arcStartDegree, sweepAngle, false, arcPaint);

        arcPaint.setStrokeWidth(arcPaintStrokeWidth);
        canvas.drawArc(mCircleArcRect1, arcStartDegree, sweepAngle, false, arcPaint);
        canvas.drawArc(mCircleArcRect2, arcStartDegree, sweepAngle, false, arcPaint);

        arcPaint.setStrokeWidth(arcPaintStrokeWidth);
//        arcPaint.setShader(arcLineShader);
        canvas.translate(mCx, mCy);
        canvas.rotate(arcStartDegree + sweepAngle);
        canvas.drawLine(radiusDial, 0, circleRadius2, 0, arcPaint);
        canvas.restore();
    }

    private void drawLabelText(Canvas canvas) {
        canvas.save();
        canvas.translate(mCx, mCy + (bgRadius3 + circleRadius1) / 2f);
        int textBaseLine = (int) (0 + (labelFontMetrics.bottom - labelFontMetrics.top) / 2 - labelFontMetrics.bottom);
        canvas.drawText(labelText, 0, textBaseLine, labelPaint);
        canvas.restore();
    }

    private void drawValText(Canvas canvas) {
        if (valText != null) {
            canvas.save();
            canvas.translate(mCx, mCy);
            int textBaseLine = (int) (0 + (valFontMetrics.bottom - valFontMetrics.top) / 2 - valFontMetrics.bottom);
            canvas.drawText(valText, 0, textBaseLine, valPaint);
            canvas.restore();
        }
    }

    private void drawPointerLine(Canvas c) {
        if(pLineCache == null) {
            pLineCache = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
            Canvas pointerLineCacheCanvas = new Canvas(pLineCache);
//        pointerLineCacheCanvas.save();
            pointerLineCacheCanvas.translate(mCx, mCy);
            pointerLineCacheCanvas.rotate(pointerLineDegree);

            for (int i = 0; i <= valRange; i++) {

                if (i < lowDialLimit) {
                    pointerPaint.setColor(colorDialLower);
                } else if (i <= highDialLimit) {
                    pointerPaint.setColor(colorDialMiddle);
                } else {
                    pointerPaint.setColor(colorDialHigh);
                }

                if (i % dialStep == 0) {     //长表针
                    pointerPaint.setStrokeWidth(strokeLDial);
                    pointerLineCacheCanvas.drawLine(radiusDial, 0, radiusDial - lengthLDial, 0, pointerPaint);

//                String text = String.format("%.0f", minVal + i); // format 性能跟不上
                    drawPointerText(pointerLineCacheCanvas, decimalFormat.format(minVal + i), i);
                } else {    //短表针
                    pointerPaint.setStrokeWidth(strokeSDial);
                    int offset = (lengthLDial - lengthSDial) / 2;
                    pointerLineCacheCanvas.drawLine(radiusDial - offset, 0, radiusDial - lengthSDial, 0, pointerPaint);
                }
                pointerLineCacheCanvas.rotate(sRoateDegree);
            }
//        pointerLineCacheCanvas.restore();
        }
        c.drawBitmap(pLineCache,0,0, null);
    }

    private void drawPointerText(Canvas canvas, String text, int i) {
        canvas.save();

        int currentCenterX = (int) (radiusDial - lengthLDial - strokeLDial - pointerPaint.measureText(String.valueOf(text)) / 2);
        canvas.translate(currentCenterX, 0);
        canvas.rotate(360 - pointerLineDegree - i * sRoateDegree);

        int textBaseLine = (int) (0 + (pointerFontMetrics.bottom - pointerFontMetrics.top) / 2 - pointerFontMetrics.bottom);

        canvas.drawText(text, 0, textBaseLine, pointerPaint);
        canvas.restore();
    }


    private void drawBG(Canvas canvas) {
        canvas.save();
        roundPaint.setStyle(Paint.Style.FILL);
        roundPaint.setColor(bgRound1);
        roundPaint.setStrokeWidth(0);
        canvas.drawCircle(mCx, mCy, bgRadius1, roundPaint);
        roundPaint.setColor(bgRound2);
        canvas.drawCircle(mCx, mCy, bgRadius2, roundPaint);
        roundPaint.setColor(bgRound3);
        canvas.drawCircle(mCx, mCy, bgRadius3, roundPaint);
        roundPaint.setColor(bgRound4);
        canvas.drawCircle(mCx, mCy, bgRadius4, roundPaint);
        roundPaint.setColor(circle1);
        roundPaint.setStyle(Paint.Style.STROKE);
        roundPaint.setStrokeWidth(circleRadius1StrokeWidth);
        canvas.drawCircle(mCx, mCy, circleRadius1, roundPaint);
        roundPaint.setStrokeWidth(circleRadius2StrokeWidth);
        canvas.drawCircle(mCx, mCy, circleRadius2, roundPaint);
        canvas.restore();
    }

    @SuppressLint("DrawAllocation")
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 获取控件区域宽高
        int minimumWidth = getSuggestedMinimumWidth();
        int minimumHeight = getSuggestedMinimumHeight();
        mWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
        mHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
        // 获取x/y轴中心点
        mCx = mWidth / 2;
        mCy = mHeight / 2;
        radius = Math.min(mWidth, mHeight) / 2;
        bgRadius1 = radius;
        bgRadius2 = (int) (radius * 0.95f);
        bgRadius3 = (int) (radius * 0.88f);
        bgRadius4 = (int) (radius * 0.6f);
        circleRadius1 = (int) (radius * 0.64f);
        circleRadius2 = (int) (radius * 0.62f);

        // 长刻度线
        radiusDial = bgRadius3;

        mArcRect.set(mCx - radiusDial, mCy - radiusDial, mCx + radiusDial, mCy + radiusDial);
        mCircleArcRect1.set(mCx - circleRadius1, mCy - circleRadius1, mCx + circleRadius1, mCy + circleRadius1);
        mCircleArcRect2.set(mCx - circleRadius2, mCy - circleRadius2, mCx + circleRadius2, mCy + circleRadius2);
        arcShader = new RadialGradient(mCx, mCy, bgRadius2, colors, null, Shader.TileMode.REPEAT);
        arcLineShader = new LinearGradient(radiusDial, 0, circleRadius2, 0, colorsArcLine, null, Shader.TileMode.REPEAT);
        pLineCache = null; // 重建缓存画布, 绘制点线耗时长

    }


    /**
     * @param formatter 中间值格式化
     * @param maxVal 最大值
     * @param minVal 最小值
     * @param dialStep      必须能被最大范围整除 (maxVal - minVal) / dialStep = 0
     * @param lowDialLimit 警戒线1
     * @param highDialLimit 警戒线2
     */
    public void init(String formatter, float maxVal, float minVal, int dialStep, int lowDialLimit, int highDialLimit) {
        this.dialStep = dialStep;
        float drawDegree = (360 - (180 - pointerLineDegree) * 2);
        this.maxVal = maxVal;
        this.minVal = minVal;
        this.valRange = maxVal - minVal;
        this.formatter = formatter;
        this.drawDialNum = (int) Math.ceil(valRange / dialStep);
        this.stepDegree = drawDegree / drawDialNum;
        this.lowDialLimit = (int) (lowDialLimit - minVal);
        this.highDialLimit = (int) (highDialLimit - minVal);
        this.sRoateDegree = stepDegree / dialStep; // 短刻度旋转角度
        invalidate();
    }

    public void updateVal(float val) {
        realVal = Math.max(Math.min(val, maxVal), minVal);
        valText = String.format(formatter, realVal);
        sweepAngle = (realVal - minVal) / valRange * (360 - (180 - pointerLineDegree) * 2);


        invalidate();
    }

    private int resolveMeasured(int measureSpec, int desired) {
        int specSize = MeasureSpec.getSize(measureSpec);
        int result;
        switch (MeasureSpec.getMode(measureSpec)) {
            case MeasureSpec.UNSPECIFIED:
                result = desired;
                break;
            case MeasureSpec.AT_MOST:
                result = Math.max(specSize, desired);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            default:
                result = specSize;
                break;
        }

        return result;
    }
    private float dp2px(float dp){
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,this.getContext().getResources().getDisplayMetrics());
    }
    public void config(GaugeView.Config cfg) {
        this.bgRound1 = cfg.bgRound1;
        this.bgRound2 = cfg.bgRound2;
        this.bgRound3 = cfg.bgRound3;
        this.bgRound4 = cfg.bgRound4;
        this.circle1 = cfg.circle1;
        this.textSizeDial = dp2px(cfg.textSizeDial); // 刻度字体大小
        this.maxVal = cfg.maxVal;
        this.minVal = cfg.minVal;

        this.lowDialLimit = cfg.lowDialLimit; // 低段温度警戒线
        this.highDialLimit = cfg.highDialLimit; // 中段警戒线
        this.colorDialLower = cfg.colorDialLower; // 低中高三个色段表盘刻度
        this.colorDialMiddle = cfg.colorDialMiddle;
        this.colorDialHigh = cfg.colorDialHigh;

        this.lengthLDial = cfg.lengthLDial;// 长刻度线的长度
        this.lengthSDial = cfg.lengthSDial;// 短刻度线的长度
        this.strokeLDial = cfg.strokeLDial;
        this.strokeSDial = cfg.strokeSDial;
        this.circleRadius1StrokeWidth = cfg.circleRadius1StrokeWidth;
        this.circleRadius2StrokeWidth = cfg.circleRadius2StrokeWidth;
        this.pointerLineDegree = cfg.pointerLineDegree; // 多少角度
        this.arcStartDegree = pointerLineDegree; // 弧线起始角度
        this.valTextSize = dp2px(cfg.valTextSize);// 值字体大小
        this.valTextColor = cfg.valTextColor;// 值字体颜色
        this.labelText = cfg.labelText;
        this.labelTextSize = dp2px(cfg.labelTextSize);// 标签字体
        this.labelTextColor = cfg.labelTextColor;// 标签字体颜色
        this.arcPaintStrokeWidth = cfg.arcPaintStrokeWidth; // 描边宽度
        this.arcColor = cfg.arcColor;
        this.colors = cfg.colors != null ? cfg.colors : new int[]{Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT, cfg.arcColor};
        this.colorsArcLine = cfg.colorsArcLine != null ? cfg.colorsArcLine : new int[]{cfg.arcColor, cfg.arcColor, Color.TRANSPARENT};
        ;
        this.formatter = cfg.formatter;
        this.drawDialNum = cfg.drawDialNum;
        this.dialStep = cfg.dialStep; // 分度
        this.decimalFormat= cfg.decimalFormat;
        init();
        invalidate();
    }

    public static class Config {
        public int bgRound1 = Color.parseColor("#99122a73");
        public int bgRound2 = Color.parseColor("#66122a73");
        public int bgRound3 = Color.parseColor("#22142f80");
        public int bgRound4 = Color.parseColor("#0e2267");
        public int circle1 = Color.parseColor("#0d1d4c");
        public float textSizeDial = 12; // 刻度字体大小
        public float maxVal = 150f;
        public float minVal = -50f;

        public int lowDialLimit = 20; // 低段温度警戒线
        public int highDialLimit = 80; // 中段警戒线
        public int colorDialLower = Color.parseColor("#00faff"); // 低中高三个色段表盘刻度
        public int colorDialMiddle = Color.GREEN;
        public int colorDialHigh = Color.RED;

        public int lengthLDial = 16;// 长刻度线的长度
        public int lengthSDial = 8;// 短刻度线的长度
        public float strokeLDial = 3;
        public float strokeSDial = 1;
        public float circleRadius1StrokeWidth = 2f;
        public float circleRadius2StrokeWidth = 4f;
        public float pointerLineDegree = 135; // 多少角度
        public float valTextSize = 30f;// 值字体大小
        public int valTextColor = Color.WHITE;// 值字体颜色
        public String labelText = "";
        public float labelTextSize = 14f;// 标签字体
        public int labelTextColor = Color.WHITE;// 标签字体颜色
        public float arcPaintStrokeWidth = 2f; // 描边宽度
        public int arcColor = Color.parseColor("#f6a800");
        public int[] colors;
        public int[] colorsArcLine;
        public String formatter = "%.0f";
        public int drawDialNum = 100;
        public int dialStep = 10; // 分度
        public DecimalFormat decimalFormat= new DecimalFormat(".0");

    }

}

widget 链接集合

评论列表: