1. 程式人生 > >Android自定義控制元件—仿儀表盤進度控制元件ArcProgressBar

Android自定義控制元件—仿儀表盤進度控制元件ArcProgressBar

開門見山,效果圖如下:
這裡寫圖片描述

這種效果經常會遇到,但卻一直不知道這個效果圖應該怎麼描述,所以暫且以“儀表盤進度控制元件”來描述,各位博友如果有更好的描述這種效果的詞彙,請回復博文告訴我,在此先謝謝各位博友了!

其實做出這樣的效果並不困難,只需要瞭解自定義控制元件的常規步驟,Canvas繪圖操作,外加一點點數學基礎就行了,因為在繪製控制元件的過程中,需要計算一些座標點和圓弧位置等資訊。

為了更加方便的使用該控制元件,該控制元件支援自定義控制元件屬性,並提供支援鏈式程式設計的方法供開發者設定各種引數,以下是控制元件實現的各個步驟。

1.自定義控制元件屬性,在佈局中直接設定控制元件引數
在res->values->attrs.xml中新增自定義的控制元件屬性如下:

<declare-styleable name="ArcProgressBar">
        <attr name="current_progress" format="float"/>
        <attr name="chart_title" format="string"/>
        <attr name="max_progress" format="float"/>
        <attr name="progress_unit" format="string"/>
</declare-styleable
>

PS:如不存在attrs.xml檔案,則可以新建任意支援自定義屬性的xml檔案,且檔名無需保持一致。

2.自定義AroProgressBar過程

2.1 新建ArcProgressBar類並繼承View
2.2 獲取自定義控制元件屬性並進行相關資源(畫筆,顏色等)的初始化
由於需要從佈局檔案中的自定義屬性獲取相關佈局初始化資料,所以必須實現如下建構函式並從自定義屬性物件中獲取相關屬性資料。

public ArcProgressBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes
(attrs, R.styleable.ArcProgressBar); float hours = typedArray.getFloat(R.styleable.ArcProgressBar_current_progress, 0); String chartTitle = typedArray.getString(R.styleable.ArcProgressBar_chart_title); float maxProgress = typedArray.getFloat(R.styleable.ArcProgressBar_max_progress, 0); String progressUnit = typedArray.getString(R.styleable.ArcProgressBar_progress_unit); if (hours > 0) { this.currentProgress = hours; } if (maxProgress > 0) { this.maxProgress = maxProgress; } if (!TextUtils.isEmpty(chartTitle)) { this.chartName = chartTitle; } if (!TextUtils.isEmpty(progressUnit)) { this.progressUnitString = progressUnit; } typedArray.recycle(); init(); }

需要注意的一點,在獲取自定義屬性完畢後,請呼叫typedArray.recycle();方法釋放自定義屬性物件。

其中init()方法中是對相關要使用到的畫筆進行初始化操作,程式碼如下:

private void init() {
        backArcPaint = new Paint();
        backArcPaint.setAntiAlias(true);
        backArcPaint.setColor(INNER_CIRCLE_BORDER_COLOR);
        backArcPaint.setStrokeWidth(arcStrokeWidth);
        backArcPaint.setStyle(Paint.Style.STROKE);
        backArcPaint.setStrokeCap(Paint.Cap.ROUND);

        fontArcPaint = new Paint();
        fontArcPaint.setAntiAlias(true);
        fontArcPaint.setColor(FONT_CIRCLE_BORDER_COLOR);
        fontArcPaint.setStrokeWidth(arcStrokeWidth);
        fontArcPaint.setStyle(Paint.Style.STROKE);
        fontArcPaint.setStrokeCap(Paint.Cap.ROUND);

        chartNamePaint = new Paint();
        chartNamePaint.setStyle(Paint.Style.FILL);
        chartNamePaint.setAntiAlias(true);
        chartNamePaint.setTextSize(chartNameTextSize);
        chartNamePaint.setColor(FONT_CIRCLE_BORDER_COLOR);

        unitTextWidth = chartNamePaint.measureText(progressUnitString);

        currentProgressNumberPaint = new Paint();
        currentProgressNumberPaint.setStyle(Paint.Style.FILL);
        currentProgressNumberPaint.setAntiAlias(true);
        currentProgressNumberPaint.setTextSize(unitTextSize);
        currentProgressNumberPaint.setColor(Color.WHITE);
    }

相關要涉及到的控制元件的成員變數宣告如下:

private int circleRectWidth;
    //圓弧邊框寬度
    private float arcStrokeWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
            10, getContext().getResources().getDisplayMetrics());
    //圖示名稱字元大小
    private float chartNameTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
            15, getContext().getResources().getDisplayMetrics());
    //圓形中心當前進度數字字元大小
    private float unitTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
            30, getContext().getResources().getDisplayMetrics());
    //底層圓弧畫筆
    private Paint backArcPaint;
    //前層圓弧畫筆
    private Paint fontArcPaint;
    //繪製圖標名稱的畫筆
    private Paint chartNamePaint;
    //繪製數字的畫筆
    private Paint currentProgressNumberPaint;
    //圓弧半徑
    private int circleRadius;
    //中心點X軸座標
    private int centerX;
    //中心店Y軸座標
    private int centerY;
    //半徑佔控制元件寬度的比例
    private final float RADIUS_RATIO = 0.3f;
    //圓弧開始繪製的角度
    private final int START_ANGLE = 135;
    //底層圓弧掃過的角度
    private final int INNER_CIRCLE_SWEEP_ANGLE = 270;
    //底層圓弧的顏色
    private final int INNER_CIRCLE_BORDER_COLOR = Color.parseColor("#aaf0f1f2");
    //上層圓弧的顏色
    private final int FONT_CIRCLE_BORDER_COLOR = Color.parseColor("#eef0f1f2");
    private final int BG_COLOR = Color.parseColor("#fe751a");
    //預設圖示名稱
    private String chartName = "無標題";
    //預設當前進度
    private float currentProgress = 0;
    //當前進度單位
    private String progressUnitString = "";
    //進度單位所佔的寬度
    private float unitTextWidth;
    //底部文案的y軸座標
    private float yPosBottomAlign;
    //最大進度數
    private float maxProgress = 8;
    private RectF rectF;

2.3 控制元件的測量過程
控制元件的測量過程需要重寫父類的onMeasure方法並通過setMeasuredDimension方法將最終的測量結果設定給控制元件。
程式碼如下:

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

        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthSpecMode == MeasureSpec.AT_MOST || heightSpecMode == MeasureSpec.AT_MOST) {
            float defaultSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                    200, getContext().getResources().getDisplayMetrics());
            widthSpecSize = (int) defaultSize;
            heightSpecSize = (int) defaultSize;
        }
        setMeasuredDimension(Math.min(widthSpecSize, heightSpecSize), Math.min(widthSpecSize, heightSpecSize));

        circleRectWidth = widthSpecSize;
        circleRadius = (int) (circleRectWidth * RADIUS_RATIO);
        centerX = circleRectWidth / 2;
        centerY = circleRectWidth / 2;
        float rad = (float) (45 * Math.PI / 180);
        yPosBottomAlign = (float) (circleRadius * Math.sin(rad) + centerY);
    }

這裡的測量過程就是獲取控制元件的寬和高,並取寬和高中較小的一個作為控制元件的寬高,因為我們實現的控制元件的寬高是一致的。
這裡需要特別注意的是對控制元件AT_MOST測量模式的處理,如果在佈局檔案中設定的寬高是wrap_content,則獲取到的寬高是0,這時候就需要對控制元件設定預設的寬高,這也是自定義控制元件中的應該需要做的處理過程之一。

2.4 控制元件的繪製過程
控制元件的繪製操作需要重寫父類的onDraw,並通過Canvas對圖形進行繪製。
每個控制元件的繪製都是有先後順序的,並且後面繪製的圖形如果在座標上與前面繪製的圖形有交集,則後面繪製的圖形會在座標存在交集的區域覆蓋前面繪製的圖形。
該控制元件的繪製過程程式碼如下:

@Override
    protected void onDraw(final Canvas canvas) {
        super.onDraw(canvas);
        drawChart(canvas, currentProgress);
    }

    private void drawChart(Canvas canvas, float loopIndex) {
        canvas.drawColor(BG_COLOR);
        //1.繪製背景圓弧
        if (rectF == null) {
            rectF = new RectF(centerX - circleRadius,//left
                    centerY - circleRadius,//top
                    centerX + circleRadius,//right
                    centerY + circleRadius);//bottom
        }

        canvas.drawArc(rectF, START_ANGLE, INNER_CIRCLE_SWEEP_ANGLE, false, backArcPaint);

        //2.繪製進度圓弧
        if (maxProgress > 0) {
            canvas.drawArc(rectF, START_ANGLE, loopIndex / maxProgress * 270, false, fontArcPaint);
        }

        //3.繪製底部文案
        float chartNameWidth = chartNamePaint.measureText(chartName);
        Paint.FontMetrics fontMetrics = chartNamePaint.getFontMetrics();
        float chartNameHeight = fontMetrics.descent - fontMetrics.ascent;
        canvas.drawText(chartName, centerX - chartNameWidth / 2, (float) (yPosBottomAlign + chartNameHeight * 1.5), chartNamePaint);

        //4.繪製中間的當前進度
        float hourNumberWidth = currentProgressNumberPaint.measureText(String.valueOf(loopIndex));
        float hourNumberHeight = currentProgressNumberPaint.getFontMetrics().bottom - currentProgressNumberPaint.getFontMetrics().top;
        //4.1繪製當前進度數字
        canvas.drawText(String.valueOf(loopIndex), centerX - hourNumberWidth / 2, centerY + chartNameHeight / 4, currentProgressNumberPaint);
        //4.1繪製進度單位
        canvas.drawText(progressUnitString, centerX - unitTextWidth / 2, centerY + chartNameHeight / 4 + hourNumberHeight / 2, chartNamePaint);
    }

至此,控制元件的實現過程已基本完成,但為了更方便地修改控制元件的相關屬性,需要暴漏一些公共方法供開發者使用,此控制元件暴漏的公共方法如下:

/**
     * 設定當前進度
     *
     * @param hour
     */
    public ArcProgressBar setCurrentProgress(float hour) {
        if (hour < 0) {
            currentProgress = 0f;
        } else if (hour > maxProgress) {
            currentProgress = maxProgress;
        } else {
            currentProgress = hour;
        }
        return this;
    }

    /**
     * 設定圖示名稱(底部)
     *
     * @param chartName
     */
    public ArcProgressBar setProgressUnit(String chartName) {
        if (TextUtils.isEmpty(chartName))
            return this;

        this.chartName = chartName;
        return this;
    }

    /**
     * 設定最大進度
     */
    public ArcProgressBar setMaxProgress(float maxHour) {
        if (maxHour <= 0) {
            return this;
        } else if (maxHour < currentProgress) {
            this.maxProgress = currentProgress;
        } else {
            this.maxProgress = maxHour;
        }
        return this;
    }

    /**
     * 重新整理介面
     * PS:引數設定完成後,務必呼叫此方法重新整理頁面
     */
    public void refresh() {
        invalidate();
    }

需要注意的是,在設定完相應的引數後,需呼叫refresh方法才會呼叫控制元件的重繪操作,這也是為了避免過多的重複繪製造成系統處理很多不必要的控制元件重繪過程。

如果開發者需要涉及到動態繪製當前進度的效果,使用者可以通過handler進行繪製或者繼承SurfaceView進行繪製,不過強烈推薦繼承SurfaceView進行繪製,因為在SurfaceView中其實是在子執行緒中完成的控制元件繪製過程,這樣就很大程度上降低了在UI執行緒繪製控制元件造成的效能問題,有關SurfaceView繪製控制元件的過程大家可以關注相關博文。