【安卓】手擼一個帶點兒科技感的儀表盤

本文只是運用一些自定義View的基礎知識,大家如果沒有興趣跟着步驟一步一步來,可以直接拖到最後複製源代碼。
如果你想對這部分知識進行下複習和梳理,也可以跟着步驟來看下,文中內容若有疏漏,還望不吝賜教。

效果圖

科技儀表盤

分析

自定義View最主要的是梳理清楚繪製的邏輯。弄明白邏輯了,一步一步來,複雜的View也就沒有那麼複雜了。

繪製步驟整理如下:

  1. 繪製一個發光的弧形
  2. 繪製刻度和數字
  3. 繪製指針陰影
  4. 繪製中間黑色圓形背景
  5. 繪製錶針
  6. 繪製深藍色發光圓形
  7. 繪製錶盤文字
  8. 添加底部控件

爲了讓大家看起來更直觀一些,我用圖片將每個步驟記錄下來,如下圖所示:
科技感儀表盤繪製流程
(當然,順序也並非一定如此,只要理清楚繪製的順序邏輯即可)

實現

View 需要用到發光的效果,我們採用 shader 來實現,那麼我們需要首先關閉硬件加速。

// 關閉硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, null);

1. 繪製一個發光的弧形

canvas.translate(getPaddingLeft() + radiusDial, getPaddingTop() + radiusDia
arcPaint.setShader(null);
arcPaint.setStyle(Paint.Style.STROKE);
arcPaint.setAntiAlias(true);
arcPaint.setAlpha(70);
arcPaint.setStyle(Paint.Style.STROKE);
arcPaint.setStrokeWidth(strokeWidthDial);
arcPaint.setShadowLayer(10, 0, 0, Color.parseColor("#FFFFFF"));
arcPaint.setColor(Color.parseColor("#38F9FD"));
canvas.drawArc(mRect, 150, (360 - openAngle), false, arcPaint);

2. 繪製刻度和數字

canvas.rotate(150);
for (int i = 0; i < clockPointNum + 1; i++) {
    pointerPaint.setColor(colorDialMiddle);
    if (i % 10 == 0) {     //長錶針
        pointerPaint.setStrokeWidth(3);
        canvas.drawLine(radiusDial - DEFAULT_border - strokeWidthDial, 0, radiusDial - strokeWidthDial - dp2px(15), 0, pointerPaint);
        drawPointerText(canvas, i);
    } else if (i % 5 == 0f) {    //短錶針
        pointerPaint.setStrokeWidth(2);
        canvas.drawLine(radiusDial - DEFAULT_border - strokeWidthDial, 0, radiusDial - strokeWidthDial - dp2px(9), 0, pointerPaint);
    }
    canvas.rotate((360 - openAngle) / clockPointNum);
}
canvas.rotate(-((180 - openAngle) / 2 + ((360 - openAngle) / clockPointNum)));
/**
 * 繪製刻度數字
 */
private void drawPointerText(Canvas canvas, int i) {
    canvas.save();
    pointerPaint.setColor(getResources().getColor(R.color.white));
    int currentCenterX = (int) (radiusDial - strokeWidthDial - dp2px(21) - pointerPaint.measureText(String.valueOf(i)) / 2);
    canvas.translate(currentCenterX, 0);
    canvas.rotate(360 - 150 - ((360 - openAngle) / clockPointNum) * i);        //座標系總旋轉角度爲360度
    int textBaseLine = (int) (0 + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom);
    canvas.drawText(String.valueOf(i + clockMinValue), 0, textBaseLine, pointerPaint);
}

3. 繪製指針陰影

int currentDegree = (int) ((currentValue - clockMinValue) * ((360 - openAngle) / clockPointNum) + 150);
canvas.rotate(currentDegree);
int[] colorSweep = {0xAAFFE9EC, 0x0028E9EC, 0xAA28E9EC};
float[] position = {0f, 0.9f, 1f};
SweepGradient mShader = new SweepGradient(0, 0, colorSweep, position);
arcPaint.setShader(mShader);
arcPaint.setStyle(Paint.Style.STROKE);
arcPaint.setStrokeWidth((float) (radiusDial * 0.4));
arcPaint.clearShadowLayer();
RectF mRect = new RectF((float) (-mRealRadius - DEFAULT_border + radiusDial * 0.2), (float) (-mRealRadius - DEFAULT_border + radiusDial * 0.2),
        (float) (mRealRadius + DEFAULT_border - radiusDial * 0.2), (float) (mRealRadius + DEFAULT_border - radiusDial * 0.2));
canvas.drawArc(mRect, 360 - (currentDegree - 150), (currentDegree - 150), false, arcPaint);

4. 繪製中間黑色圓形背景

canvas.restore();
canvas.translate(getPaddingLeft() + radiusDial, getPaddingTop() + radiusDial);
Paint pointerPaint = new Paint();
pointerPaint.setAntiAlias(true);
pointerPaint.setStyle(Paint.Style.FILL);
pointerPaint.setColor(Color.parseColor("#05002D"));
canvas.drawCircle(0, 0, (float) (radiusDial * 0.6), pointerPaint);

5. 繪製錶針

canvas.save();
int currentDegree = (int) ((currentValue - clockMinValue) * ((360 - openAngle) / clockPointNum) + 150);
canvas.rotate(currentDegree);
titlePaint.setColor(Color.WHITE);
titlePaint.setAntiAlias(true);
pointerPath.moveTo(radiusDial - dp2px(12), 0);
pointerPath.lineTo(0, -dp2px(5));
pointerPath.lineTo(-12, 0);
pointerPath.lineTo(0, dp2px(5));
pointerPath.close();
canvas.drawPath(pointerPath, titlePaint);
canvas.save();
canvas.restore();

6. 繪製深藍色發光圓形

canvas.rotate(0);
canvas.restore();
Paint pointerPaint = new Paint();
pointerPaint.setAntiAlias(true);
pointerPaint.setStyle(Paint.Style.FILL);
pointerPaint.setColor(Color.parseColor("#050D3D"));
pointerPaint.setShadowLayer(15, 0, 0, Color.parseColor("#006EC6"));
canvas.drawCircle(0, 0, (float) (radiusDial * 0.4), pointerPaint);

7. 繪製錶盤文字

titlePaint.setColor(Color.WHITE);
titlePaint.setColor(titleDialColor);
titlePaint.setTextSize(titleDialSize);
canvas.drawText(formatData(currentValue), 0, 0, titlePaint);
titlePaint.setColor(Color.parseColor("#38F9FD"));
titlePaint.setTextSize(sp2px(14));
canvas.drawText("(" + dataUnit + ")", 0, dp2px(18), titlePaint);

8. 添加底部控件

這部分代碼就比較靈活了,儀表盤主體繪製出來以後,可以在佈局文件中增加其它底部控件。並設置相應點擊事件等。在此略過不表。

源碼

attr.xml

    <!-- 儀表盤自定義屬性 -->
    <declare-styleable name="DashboardView">
        <attr name="color_dial_lower" format="color" />
        <attr name="color_dial_middle" format="color" />
        <attr name="color_dial_high" format="color" />
        <attr name="text_size_dial" format="dimension" />
        <attr name="stroke_width_dial" format="dimension" />
        <attr name="radius_circle_dial" format="dimension" />
        <attr name="text_title_dial" format="string" />
        <attr name="text_title_size" format="dimension" />
        <attr name="text_title_color" format="color" />
        <attr name="text_size_value" format="dimension" />
        <attr name="animator_play_time" format="integer" />
    </declare-styleable>

DashboardView.java

public class DashboardView extends View {

    private static final int DEFAULT_COLOR_MIDDLE = Color.parseColor("#228fbd");
    private static final int DEFAULT_COLOR_TITLE = Color.WHITE;
    private static final int DEFAULT_TEXT_SIZE_DIAL = 11;
    private static final int DEFAULT_STROKE_WIDTH = 2;
    private static final int DEFAULT_RADIUS_DIAL = 128;
    private static final int DEFAULT_TITLE_SIZE = 22;
    private static final int DEFAULT_ANIM_PLAY_TIME = 2000;
    private static final int DEFAULT_border = 5;

    private int colorDialMiddle;
    private int textSizeDial;
    private int strokeWidthDial;
    private int titleDialSize;
    private int titleDialColor;
    private int animPlayTime;
    private float openAngle = 120;// 底部開口的角度
    private int radiusDial;
    private int mRealRadius;
    private float currentValue;
    private int clockPointNum = 100;
    private int clockMinValue = 0;
    private String dataUnit = "℃";

    private Paint arcPaint;
    private RectF mRect;
    private Paint pointerPaint;
    private Paint.FontMetrics fontMetrics;
    private Paint titlePaint;
    private Path pointerPath;

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

    public DashboardView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DashboardView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
//        關閉硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE, null);
//        初始化屬性
        initAttrs(context, attrs);
        initPaint();
    }

    private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.DashboardView);
        colorDialMiddle = attributes.getColor(R.styleable.DashboardView_color_dial_middle, DEFAULT_COLOR_MIDDLE);
        textSizeDial = (int) attributes.getDimension(R.styleable.DashboardView_text_size_dial, sp2px(DEFAULT_TEXT_SIZE_DIAL));
        strokeWidthDial = (int) attributes.getDimension(R.styleable.DashboardView_stroke_width_dial, dp2px(DEFAULT_STROKE_WIDTH));
        radiusDial = (int) attributes.getDimension(R.styleable.DashboardView_radius_circle_dial, dp2px(DEFAULT_RADIUS_DIAL));
        titleDialSize = (int) attributes.getDimension(R.styleable.DashboardView_text_title_size, dp2px(DEFAULT_TITLE_SIZE));
        titleDialColor = attributes.getColor(R.styleable.DashboardView_text_title_color, DEFAULT_COLOR_TITLE);
        animPlayTime = attributes.getInt(R.styleable.DashboardView_animator_play_time, DEFAULT_ANIM_PLAY_TIME);
        attributes.recycle();
    }

    private void initPaint() {
        arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        arcPaint.setStyle(Paint.Style.STROKE);
        arcPaint.setStrokeWidth(strokeWidthDial);
        arcPaint.setShadowLayer(10, 0, 0, Color.parseColor("#35FCFB"));

        pointerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        pointerPaint.setTextSize(textSizeDial);
        pointerPaint.setTextAlign(Paint.Align.CENTER);
        fontMetrics = pointerPaint.getFontMetrics();

        titlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        titlePaint.setTextAlign(Paint.Align.CENTER);
        titlePaint.setFakeBoldText(true);

        pointerPath = new Path();
    }

    public void setClockPointNum(int clockPointNum) {
        this.clockPointNum = clockPointNum;
        postInvalidate();
    }

    public void setClockValueArea(int clockMinValue, int clockMaxValue, String dataUnit) {
        this.clockMinValue = clockMinValue;
        this.dataUnit = dataUnit;
        setClockPointNum(clockMaxValue - clockMinValue);
    }

    @SuppressLint("DrawAllocation")
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int mWidth, mHeight;
        if (widthMode == MeasureSpec.EXACTLY) {
            mWidth = widthSize;
        } else {
            mWidth = getPaddingLeft() + radiusDial * 2 + getPaddingRight();
            if (widthMode == MeasureSpec.AT_MOST) {
                mWidth = Math.min(mWidth, widthSize);
            }
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            mHeight = heightSize;
        } else {
            mHeight = getPaddingTop() + radiusDial * 2 + getPaddingBottom();
            if (heightMode == MeasureSpec.AT_MOST) {
                mHeight = Math.min(mHeight, heightSize);
            }
        }

        setMeasuredDimension(mWidth, mHeight);

        radiusDial = Math.min((getMeasuredWidth() - getPaddingLeft() - getPaddingRight()),
                (getMeasuredHeight() - getPaddingTop() - getPaddingBottom())) / 2;
        mRealRadius = radiusDial - strokeWidthDial / 2 - DEFAULT_border * 2;
        mRect = new RectF(-mRealRadius - DEFAULT_border, -mRealRadius - DEFAULT_border,
                mRealRadius + DEFAULT_border, mRealRadius + DEFAULT_border);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
//        step1 畫圓弧
        drawArc(canvas);
//        step2 繪製刻度和數字
        drawPointerLine(canvas);
//        step3 畫指針陰影
        drawPointShadow(canvas);
//        step4 繪製中間黑色圓形背景
        drawBlackCircle(canvas);
//        step5 繪製錶針
        drawPointer(canvas);
//        step6 繪製深藍色發光圓形
        drawBlueCircle(canvas);
//        step7 繪製錶盤中的數字
        drawCircleText(canvas);
    }

    private void drawPointShadow(Canvas canvas) {
        int currentDegree = (int) ((currentValue - clockMinValue) * ((360 - openAngle) / clockPointNum) + 150);
        canvas.rotate(currentDegree);

        int[] colorSweep = {0xAAFFE9EC, 0x0028E9EC, 0xAA28E9EC};
        float[] position = {0f, 0.9f, 1f};
        SweepGradient mShader = new SweepGradient(0, 0, colorSweep, position);

        arcPaint.setShader(mShader);
        arcPaint.setStyle(Paint.Style.STROKE);
        arcPaint.setStrokeWidth((float) (radiusDial * 0.4));
        arcPaint.clearShadowLayer();
        RectF mRect = new RectF((float) (-mRealRadius - DEFAULT_border + radiusDial * 0.2), (float) (-mRealRadius - DEFAULT_border + radiusDial * 0.2),
                (float) (mRealRadius + DEFAULT_border - radiusDial * 0.2), (float) (mRealRadius + DEFAULT_border - radiusDial * 0.2));
        canvas.drawArc(mRect, 360 - (currentDegree - 150), (currentDegree - 150), false, arcPaint);
    }

    private void drawPointer(Canvas canvas) {
        canvas.save();
        int currentDegree = (int) ((currentValue - clockMinValue) * ((360 - openAngle) / clockPointNum) + 150);

        canvas.rotate(currentDegree);
        titlePaint.setColor(Color.WHITE);
        titlePaint.setAntiAlias(true);
        pointerPath.moveTo(radiusDial - dp2px(12), 0);
        pointerPath.lineTo(0, -dp2px(5));
        pointerPath.lineTo(-12, 0);
        pointerPath.lineTo(0, dp2px(5));
        pointerPath.close();
        canvas.drawPath(pointerPath, titlePaint);

        canvas.save();
        canvas.restore();
    }

    private void drawCircleText(Canvas canvas) {
        titlePaint.setColor(Color.WHITE);
        titlePaint.setColor(titleDialColor);
        titlePaint.setTextSize(titleDialSize);
        canvas.drawText(formatData(currentValue), 0, 0, titlePaint);
        titlePaint.setColor(Color.parseColor("#38F9FD"));
        titlePaint.setTextSize(sp2px(14));
        canvas.drawText("(" + dataUnit + ")", 0, dp2px(18), titlePaint);
    }

    private void drawBlueCircle(Canvas canvas) {
        canvas.rotate(0);
        canvas.restore();
        Paint pointerPaint = new Paint();
        pointerPaint.setAntiAlias(true);
        pointerPaint.setStyle(Paint.Style.FILL);
        pointerPaint.setColor(Color.parseColor("#050D3D"));
        pointerPaint.setShadowLayer(15, 0, 0, Color.parseColor("#006EC6"));
        canvas.drawCircle(0, 0, (float) (radiusDial * 0.4), pointerPaint);
    }

    private void drawBlackCircle(Canvas canvas) {
        canvas.restore();
        canvas.translate(getPaddingLeft() + radiusDial, getPaddingTop() + radiusDial);
        Paint pointerPaint = new Paint();
        pointerPaint.setAntiAlias(true);
        pointerPaint.setStyle(Paint.Style.FILL);
        pointerPaint.setColor(Color.parseColor("#05002D"));
        canvas.drawCircle(0, 0, (float) (radiusDial * 0.6), pointerPaint);
    }

    private void drawArc(Canvas canvas) {
        canvas.translate(getPaddingLeft() + radiusDial, getPaddingTop() + radiusDial);
        arcPaint.setShader(null);
        arcPaint.setStyle(Paint.Style.STROKE);
        arcPaint.setAntiAlias(true);
        arcPaint.setAlpha(70);
        arcPaint.setStyle(Paint.Style.STROKE);
        arcPaint.setStrokeWidth(strokeWidthDial);
        arcPaint.setShadowLayer(10, 0, 0, Color.parseColor("#FFFFFF"));
        arcPaint.setColor(Color.parseColor("#38F9FD"));
        canvas.drawArc(mRect, 150, (360 - openAngle), false, arcPaint);
    }

    private void drawPointerLine(Canvas canvas) {
//        旋轉畫布 (座標系)
        canvas.rotate(150);

        for (int i = 0; i < clockPointNum + 1; i++) {
            pointerPaint.setColor(colorDialMiddle);

            if (i % 10 == 0) {     //長錶針
                pointerPaint.setStrokeWidth(3);
                canvas.drawLine(radiusDial - DEFAULT_border - strokeWidthDial, 0, radiusDial - strokeWidthDial - dp2px(15), 0, pointerPaint);
                drawPointerText(canvas, i);
            } else if (i % 5 == 0f) {    //短錶針
                pointerPaint.setStrokeWidth(2);
                canvas.drawLine(radiusDial - DEFAULT_border - strokeWidthDial, 0, radiusDial - strokeWidthDial - dp2px(9), 0, pointerPaint);
            }
            canvas.rotate((360 - openAngle) / clockPointNum);
        }
        canvas.rotate(-((180 - openAngle) / 2 + ((360 - openAngle) / clockPointNum)));
    }

    private void drawPointerText(Canvas canvas, int i) {
        canvas.save();
        pointerPaint.setColor(getResources().getColor(R.color.white));
        int currentCenterX = (int) (radiusDial - strokeWidthDial - dp2px(21) - pointerPaint.measureText(String.valueOf(i)) / 2);
        canvas.translate(currentCenterX, 0);

        canvas.rotate(360 - 150 - ((360 - openAngle) / clockPointNum) * i);        //座標系總旋轉角度爲360度

        int textBaseLine = (int) (0 + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom);
        canvas.drawText(String.valueOf(i + clockMinValue), 0, textBaseLine, pointerPaint);
        canvas.restore();
    }

    public void setCompleteDegree(float degree) {
        ValueAnimator animator = ValueAnimator.ofFloat(currentValue, degree);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentValue = (float) (Math.round((float) animation.getAnimatedValue() * 10)) / 10;
                invalidate();
            }
        });
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.setDuration(animPlayTime);
        animator.start();
    }

    protected int dp2px(int dpVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, getResources().getDisplayMetrics());
    }

    protected int sp2px(int spVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spVal, getResources().getDisplayMetrics());
    }

    protected String formatData(float num) {
        DecimalFormat decimalFormat = new DecimalFormat("###.#");
        return decimalFormat.format(num);
    }

}


如果本文對你有所幫助,還望可以點個贊哈~~

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