1. 程式人生 > >Android 自定義View之仿華為圓形載入進度條

Android 自定義View之仿華為圓形載入進度條

效果圖

這裡寫圖片描述

實現思路

可以看出該View可分為三個部分來實現

  • 最外圍的圓,該部分需要區分進度圓和底部的刻度圓,進度部分的刻度需要和底色刻度區分開來
  • 中間顯示的文字進度,需要讓文字在View中居中顯示
  • 旋轉的小圓點,小圓點需要模擬小球下落運動時的加速度效果,開始下落的時候慢,到最底部時最快,上來時速度再逐漸減慢

具體實現

先具體細分講解,部落格最後面給出全部原始碼

(1)首先為View建立自定義的xml屬性
在工程的values目錄下新建attrs.xml檔案

<resources>
    <!-- 仿華為圓形載入進度條 -->
    <declare-styleable
name="CircleLoading">
<attr name="indexColor" format="color"/> <attr name="baseColor" format="color"/> <attr name="dotColor" format="color"/> <attr name="textSize" format="dimension"/> <attr name="textColor" format="color"/> </declare-styleable
>
</resources>

各個屬性的作用:

  • indexColor:進度圓的顏色
  • baseColor:刻度圓底色
  • dotColor:小圓點顏色
  • textSize:文字大小
  • textColor:文字顏色

(2)新建CircleLoadingView類繼承View類,重寫它的三個構造方法,獲取使用者設定的屬性,同時指定預設值

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

    public CircleLoadingView(Context context, AttributeSet attrs) {
        this(context, attrs, 0
); } public CircleLoadingView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 獲取使用者配置屬性 TypedArray tya = context.obtainStyledAttributes(attrs, R.styleable.CircleLoading); baseColor = tya.getColor(R.styleable.CircleLoading_baseColor, Color.LTGRAY); indexColor = tya.getColor(R.styleable.CircleLoading_indexColor, Color.BLUE); textColor = tya.getColor(R.styleable.CircleLoading_textColor, Color.BLUE); dotColor = tya.getColor(R.styleable.CircleLoading_dotColor, Color.RED); textSize = tya.getDimensionPixelSize(R.styleable.CircleLoading_textSize, 36); tya.recycle(); initUI(); }

我們從View繪製的第一步開始
(3)測量onMeasure,首先需要測量出View的寬和高,並指定View在wrap_content時的最小範圍,對於View繪製流程還不熟悉的同學,可以先去了解下具體的繪製流程

重寫onMeasure方法,其中我們要考慮當View的寬高被指定為wrap_content時的情況,如果我們不對wrap_content的情況進行處理,那麼當使用者指定View的寬高為wrap_content時將無法正常顯示出View

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

        int myWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int myWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int myHeightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int myHeightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        // 獲取寬
        if (myWidthSpecMode == MeasureSpec.EXACTLY) {
            // match_parent/精確值
            mWidth = myWidthSpecSize;
        } else {
            // wrap_content
            mWidth = DensityUtil.dip2px(mContext, 120);
        }

        // 獲取高
        if (myHeightSpecMode == MeasureSpec.EXACTLY) {
            // match_parent/精確值
            mHeight = myHeightSpecSize;
        } else {
            // wrap_content
            mHeight = DensityUtil.dip2px(mContext, 120);
        }

        // 設定該view的寬高
        setMeasuredDimension(mWidth, mHeight);
    }

MeasureSpec的狀態分為三種EXACTLYAT_MOSTUNSPECIFIED,這裡只要單獨指定非精確值EXACTLY之外的情況就好了
本文中使用到的DensityUtil類,是為了將dp轉換為px來使用,以便適配不同的螢幕顯示效果

    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

(4)重寫onDraw,繪製需要顯示的內容
因為做的是單純的View而不是ViewGroup,內部沒有子控制元件需要確定位置,所以可直接跳過onLayout方法,直接開始對View進行繪製
分為三個部分繪製,繪製刻度圓,繪製文字值,繪製旋轉小圓點

    @Override
    protected void onDraw(Canvas canvas) {
        drawArcScale(canvas);
        drawTextValue(canvas);
        drawRotateDot(canvas);
    }
  • 繪製刻度圓

先畫一個小豎線,通過canvas.rotate()方法每次旋轉3.6度(總共360度,用100/360=3.6)得到一個刻度為100的圓,然後通過progress引數,得到要顯示的進度數,並把小於progress的刻度變成進度圓的顏色

    /**
     * 畫刻度
     */
    private void drawArcScale(Canvas canvas) {
        canvas.save();

        for (int i = 0; i < 100; i++) {
            if (progress > i) {
                mScalePaint.setColor(indexColor);
            } else {
                mScalePaint.setColor(baseColor);
            }
            canvas.drawLine(mWidth / 2, 0, mHeight / 2, DensityUtil.dip2px(mContext, 10), mScalePaint);
            // 旋轉的度數 = 100 / 360
            canvas.rotate(3.6f, mWidth / 2, mHeight / 2);
        }

        canvas.restore();
    }
  • 繪製中間文字

文字繪製的座標是以文字的左下角開始繪製的,所以需要先通過把文字裝載到一個矩形Rect,通過畫筆的getTextBounds方法取得字串的長度和寬度,通過動態計算,來使文字居中顯示

    /**
     * 畫內部數值
     */
    private void drawTextValue(Canvas canvas) {
        canvas.save();

        String showValue = String.valueOf(progress);
        Rect textBound = new Rect();
        mTextPaint.getTextBounds(showValue, 0, showValue.length(), textBound);    // 獲取文字的矩形範圍
        float textWidth = textBound.right - textBound.left;  // 獲得文字寬
        float textHeight = textBound.bottom - textBound.top; // 獲得文字高
        canvas.drawText(showValue, mWidth / 2 - textWidth / 2, mHeight / 2 + textHeight / 2, mTextPaint);

        canvas.restore();
    }
  • 繪製旋轉小圓點

這個小圓點就是簡單的繪製一個填充的圓形就好

    /**
     * 畫旋轉小圓點
     */
    private void drawRotateDot(final Canvas canvas) {
        canvas.save();

        canvas.rotate(mDotProgress * 3.6f, mWidth / 2, mHeight / 2);
        canvas.drawCircle(mWidth / 2, DensityUtil.dip2px(mContext, 10) + DensityUtil.dip2px(mContext, 5), DensityUtil.dip2px(mContext, 3), mDotPaint);

        canvas.restore();
    }

讓它自己動起來可以通過兩種方式,一種是開一個執行緒,線上程中改變mDotProgress的數值,並通過postInvalidate方法跨執行緒重新整理View的顯示效果

        new Thread() {
            @Override
            public void run() {
                while (true) {
                    mDotProgress++;
                    if (mDotProgress == 100) {
                        mDotProgress = 0;
                    }
                    postInvalidate();
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

開執行緒的方式不推薦使用,這是沒必要的開銷,而且執行緒不好控制,要實現讓小圓點在執行過程中開始和結束時慢,運動到中間時加快這種效果不好實現,所以最好的方式是使用屬性動畫,需要讓小圓點動起來時,呼叫以下方法就好了

    /**
     * 啟動小圓點旋轉動畫
     */
    public void startDotAnimator() {
        animator = ValueAnimator.ofFloat(0, 100);
        animator.setDuration(1500);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.setRepeatMode(ValueAnimator.RESTART);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                // 設定小圓點的進度,並通知介面重繪
                mDotProgress = (Float) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.start();
    }

在屬性動畫中可以通過setInterpolator方法指定不同的插值器,這裡要模擬小球掉下來的重力效果,所以需要使用AccelerateDecelerateInterpolator插值類,該類的效果就是在動畫開始時和結束時變慢,中間加快

(5)設定當前進度值
對外提供一個方法,用來更新當前圓的進度

    /**
     * 設定進度
     */
    public void setProgress(int progress) {
        this.progress = progress;
        invalidate();
    }

通過外部呼叫setProgress方法就可以跟更新當前圓的進度了

原始碼

/**
 * 仿華為圓形載入進度條
 * Created by zhuwentao on 2017-08-19.
 */
public class CircleLoadingView extends View {

    private Context mContext;

    // 刻度畫筆
    private Paint mScalePaint;

    // 小原點畫筆
    private Paint mDotPaint;

    // 文字畫筆
    private Paint mTextPaint;

    // 當前進度
    private int progress = 0;

    /**
     * 小圓點的當前進度
     */
    public float mDotProgress;

    // View寬
    private int mWidth;

    // View高
    private int mHeight;

    private int indexColor;

    private int baseColor;

    private int dotColor;

    private int textSize;

    private int textColor;

    private ValueAnimator animator;

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

    public CircleLoadingView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 獲取使用者配置屬性
        TypedArray tya = context.obtainStyledAttributes(attrs, R.styleable.CircleLoading);
        baseColor = tya.getColor(R.styleable.CircleLoading_baseColor, Color.LTGRAY);
        indexColor = tya.getColor(R.styleable.CircleLoading_indexColor, Color.BLUE);
        textColor = tya.getColor(R.styleable.CircleLoading_textColor, Color.BLUE);
        dotColor = tya.getColor(R.styleable.CircleLoading_dotColor, Color.RED);
        textSize = tya.getDimensionPixelSize(R.styleable.CircleLoading_textSize, 36);
        tya.recycle();

        initUI();
    }

    private void initUI() {
        mContext = getContext();

        // 刻度畫筆
        mScalePaint = new Paint();
        mScalePaint.setAntiAlias(true);
        mScalePaint.setStrokeWidth(DensityUtil.dip2px(mContext, 1));
        mScalePaint.setStrokeCap(Paint.Cap.ROUND);
        mScalePaint.setColor(baseColor);
        mScalePaint.setStyle(Paint.Style.STROKE);

        // 小圓點畫筆
        mDotPaint = new Paint();
        mDotPaint.setAntiAlias(true);
        mDotPaint.setColor(dotColor);
        mDotPaint.setStrokeWidth(DensityUtil.dip2px(mContext, 1));
        mDotPaint.setStyle(Paint.Style.FILL);

        // 文字畫筆
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(textColor);
        mTextPaint.setTextSize(textSize);
        mTextPaint.setStrokeWidth(DensityUtil.dip2px(mContext, 1));
        mTextPaint.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        drawArcScale(canvas);
        drawTextValue(canvas);
        drawRotateDot(canvas);
    }

    /**
     * 畫刻度
     */
    private void drawArcScale(Canvas canvas) {
        canvas.save();

        for (int i = 0; i < 100; i++) {
            if (progress > i) {
                mScalePaint.setColor(indexColor);
            } else {
                mScalePaint.setColor(baseColor);
            }
            canvas.drawLine(mWidth / 2, 0, mHeight / 2, DensityUtil.dip2px(mContext, 10), mScalePaint);
            // 旋轉的度數 = 100 / 360
            canvas.rotate(3.6f, mWidth / 2, mHeight / 2);
        }

        canvas.restore();
    }

    /**
     * 畫內部數值
     */
    private void drawTextValue(Canvas canvas) {
        canvas.save();

        String showValue = String.valueOf(progress);
        Rect textBound = new Rect();
        mTextPaint.getTextBounds(showValue, 0, showValue.length(), textBound);    // 獲取文字的矩形範圍
        float textWidth = textBound.right - textBound.left;  // 獲得文字寬
        float textHeight = textBound.bottom - textBound.top; // 獲得文字高
        canvas.drawText(showValue, mWidth / 2 - textWidth / 2, mHeight / 2 + textHeight / 2, mTextPaint);

        canvas.restore();
    }

    /**
     * 畫旋轉小圓點
     */
    private void drawRotateDot(final Canvas canvas) {
        canvas.save();

        canvas.rotate(mDotProgress * 3.6f, mWidth / 2, mHeight / 2);
        canvas.drawCircle(mWidth / 2, DensityUtil.dip2px(mContext, 10) + DensityUtil.dip2px(mContext, 5), DensityUtil.dip2px(mContext, 3), mDotPaint);

        canvas.restore();
    }

    /**
     * 啟動小圓點旋轉動畫
     */
    public void startDotAnimator() {
        animator = ValueAnimator.ofFloat(0, 100);
        animator.setDuration(1500);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.setRepeatMode(ValueAnimator.RESTART);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                // 設定小圓點的進度,並通知介面重繪
                mDotProgress = (Float) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.start();
    }

    /**
     * 設定進度
     */
    public void setProgress(int progress) {
        this.progress = progress;
        invalidate();
    }

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

        int myWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int myWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int myHeightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int myHeightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        // 獲取寬
        if (myWidthSpecMode == MeasureSpec.EXACTLY) {
            // match_parent/精確值
            mWidth = myWidthSpecSize;
        } else {
            // wrap_content
            mWidth = DensityUtil.dip2px(mContext, 120);
        }

        // 獲取高
        if (myHeightSpecMode == MeasureSpec.EXACTLY) {
            // match_parent/精確值
            mHeight = myHeightSpecSize;
        } else {
            // wrap_content
            mHeight = DensityUtil.dip2px(mContext, 120);
        }

        // 設定該view的寬高
        setMeasuredDimension(mWidth, mHeight);
    }
}

總結

  • 在的onDraw方法中需要避免頻繁的new物件,所以把一些如初始化畫筆Paint的方法放到了最前面的構造方法中進行。

  • 在分多個模組繪製時,應該使用canvas.save()canvas.restore()的組合,來避免不同模組繪製時的相互干擾,在這兩個方法中繪製相當於PS中的圖層概念,上一個圖層進行的修改不會影響到下一個圖層的顯示效果。

  • 在需要顯示動畫效果的地方使用屬性動畫來處理,可自定義的效果強,在系統提供的插值器類不夠用的情況下,我麼還可通過繼承Animation類,重寫它的applyTransformation方法來處理各種複雜的動畫效果。