天天看點

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, );
        String chartTitle = typedArray.getString(R.styleable.ArcProgressBar_chart_title);
        float maxProgress = typedArray.getFloat(R.styleable.ArcProgressBar_max_progress, );
        String progressUnit = typedArray.getString(R.styleable.ArcProgressBar_progress_unit);

        if (hours > ) {
            this.currentProgress = hours;
        }
        if (maxProgress > ) {
            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,
            , getContext().getResources().getDisplayMetrics());
    //圖示名稱字元大小
    private float chartNameTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
            , getContext().getResources().getDisplayMetrics());
    //圓形中心目前進度數字字元大小
    private float unitTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
            , 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 = f;
    //圓弧開始繪制的角度
    private final int START_ANGLE = ;
    //底層圓弧掃過的角度
    private final int INNER_CIRCLE_SWEEP_ANGLE = ;
    //底層圓弧的顔色
    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 = ;
    //目前進度機關
    private String progressUnitString = "";
    //進度機關所占的寬度
    private float unitTextWidth;
    //底部文案的y軸坐标
    private float yPosBottomAlign;
    //最大進度數
    private float maxProgress = ;
    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,
                    , 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 / ;
        centerY = circleRectWidth / ;
        float rad = (float) ( * Math.PI / );
        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 > ) {
            canvas.drawArc(rectF, START_ANGLE, loopIndex / maxProgress * , false, fontArcPaint);
        }

        //3.繪制底部文案
        float chartNameWidth = chartNamePaint.measureText(chartName);
        Paint.FontMetrics fontMetrics = chartNamePaint.getFontMetrics();
        float chartNameHeight = fontMetrics.descent - fontMetrics.ascent;
        canvas.drawText(chartName, centerX - chartNameWidth / , (float) (yPosBottomAlign + chartNameHeight * ), 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 / , centerY + chartNameHeight / , currentProgressNumberPaint);
        //4.1繪制進度機關
        canvas.drawText(progressUnitString, centerX - unitTextWidth / , centerY + chartNameHeight /  + hourNumberHeight / , chartNamePaint);
    }
           

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

/**
     * 設定目前進度
     *
     * @param hour
     */
    public ArcProgressBar setCurrentProgress(float hour) {
        if (hour < ) {
            currentProgress = f;
        } 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 <= ) {
            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繪制控件的過程大家可以關注相關博文。

繼續閱讀