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的狀態分為三種EXACTLY
、AT_MOST
、UNSPECIFIED
,這裡只要單獨指定非精確值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
方法來處理各種複雜的動畫效果。