Android自定義View仿華為圓形加載進(jìn)度條
View仿華為圓形加載進(jìn)度條效果圖
實(shí)現(xiàn)思路
可以看出該View可分為三個部分來實(shí)現(xiàn)
最外圍的圓,該部分需要區(qū)分進(jìn)度圓和底部的刻度圓,進(jìn)度部分的刻度需要和底色刻度區(qū)分開來
中間顯示的文字進(jìn)度,需要讓文字在View中居中顯示
旋轉(zhuǎn)的小圓點(diǎn),小圓點(diǎn)需要模擬小球下落運(yùn)動時的加速度效果,開始下落的時候慢,到最底部時最快,上來時速度再逐漸減慢
具體實(shí)現(xiàn)
先具體細(xì)分講解,博客最后面給出全部源碼
(1)首先為View創(chuàng)建自定義的xml屬性
在工程的values目錄下新建attrs.xml文件
<resources> <!-- 仿華為圓形加載進(jìn)度條 --> <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:進(jìn)度圓的顏色
baseColor:刻度圓底色
dotColor:小圓點(diǎn)顏色
textSize:文字大小
textColor:文字顏色
(2)新建CircleLoadingView類繼承View類,重寫它的三個構(gòu)造方法,獲取用戶設(shè)置的屬性,同時指定默認(rèn)值
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繪制流程還不熟悉的同學(xué),可以先去了解下具體的繪制流程
淺談Android View繪制三大流程探索及常見問題
重寫onMeasure方法,其中我們要考慮當(dāng)View的寬高被指定為wrap_content時的情況,如果我們不對wrap_content的情況進(jìn)行處理,那么當(dāng)使用者指定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); } // 設(shè)置該view的寬高 setMeasuredDimension(mWidth, mHeight); }
MeasureSpec的狀態(tài)分為三種EXACTLY、AT_MOST、UNSPECIFIED,這里只要單獨(dú)指定非精確值EXACTLY之外的情況就好了。
本文中使用到的DensityUtil類,是為了將dp轉(zhuǎn)換為px來使用,以便適配不同的屏幕顯示效果
public static int dip2px(Context context, float dpValue) { final float scale = context.getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); }
(4)重寫onDraw,繪制需要顯示的內(nèi)容
因?yàn)樽龅氖菃渭兊腣iew而不是ViewGroup,內(nèi)部沒有子控件需要確定位置,所以可直接跳過onLayout方法,直接開始對View進(jìn)行繪制
分為三個部分繪制,繪制刻度圓,繪制文字值,繪制旋轉(zhuǎn)小圓點(diǎn)
@Override protected void onDraw(Canvas canvas) { drawArcScale(canvas); drawTextValue(canvas); drawRotateDot(canvas); }
繪制刻度圓
先畫一個小豎線,通過canvas.rotate()方法每次旋轉(zhuǎn)3.6度(總共360度,用100/360=3.6)得到一個刻度為100的圓,然后通過progress參數(shù),得到要顯示的進(jìn)度數(shù),并把小于progress的刻度變成進(jìn)度圓的顏色
/** * 畫刻度 */ 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); // 旋轉(zhuǎn)的度數(shù) = 100 / 360 canvas.rotate(3.6f, mWidth / 2, mHeight / 2); } canvas.restore(); }
繪制中間文字
文字繪制的坐標(biāo)是以文字的左下角開始繪制的,所以需要先通過把文字裝載到一個矩形Rect,通過畫筆的getTextBounds方法取得字符串的長度和寬度,通過動態(tài)計(jì)算,來使文字居中顯示
/** * 畫內(nèi)部數(shù)值 */ 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(); }
繪制旋轉(zhuǎn)小圓點(diǎn)
這個小圓點(diǎn)就是簡單的繪制一個填充的圓形就好
/** * 畫旋轉(zhuǎn)小圓點(diǎn) */ 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的數(shù)值,并通過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();
開線程的方式不推薦使用,這是沒必要的開銷,而且線程不好控制,要實(shí)現(xiàn)讓小圓點(diǎn)在運(yùn)行過程中開始和結(jié)束時慢,運(yùn)動到中間時加快這種效果不好實(shí)現(xiàn),所以最好的方式是使用屬性動畫,需要讓小圓點(diǎn)動起來時,調(diào)用以下方法就好了
/** * 啟動小圓點(diǎn)旋轉(zhuǎn)動畫 */ 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) { // 設(shè)置小圓點(diǎn)的進(jìn)度,并通知界面重繪 mDotProgress = (Float) animation.getAnimatedValue(); invalidate(); } }); animator.start(); }
在屬性動畫中可以通過setInterpolator方法指定不同的插值器,這里要模擬小球掉下來的重力效果,所以需要使用AccelerateDecelerateInterpolator插值類,該類的效果就是在動畫開始時和結(jié)束時變慢,中間加快
(5)設(shè)置當(dāng)前進(jìn)度值
對外提供一個方法,用來更新當(dāng)前圓的進(jìn)度
/** * 設(shè)置進(jìn)度 */ public void setProgress(int progress) { this.progress = progress; invalidate(); }
通過外部調(diào)用setProgress方法就可以跟更新當(dāng)前圓的進(jìn)度了
源碼
/** * 仿華為圓形加載進(jìn)度條 * Created by zhuwentao on 2017-08-19. */ public class CircleLoadingView extends View { private Context mContext; // 刻度畫筆 private Paint mScalePaint; // 小原點(diǎn)畫筆 private Paint mDotPaint; // 文字畫筆 private Paint mTextPaint; // 當(dāng)前進(jìn)度 private int progress = 0; /** * 小圓點(diǎn)的當(dāng)前進(jìn)度 */ 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); // 小圓點(diǎn)畫筆 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); // 旋轉(zhuǎn)的度數(shù) = 100 / 360 canvas.rotate(3.6f, mWidth / 2, mHeight / 2); } canvas.restore(); } /** * 畫內(nèi)部數(shù)值 */ 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(); } /** * 畫旋轉(zhuǎn)小圓點(diǎn) */ 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(); } /** * 啟動小圓點(diǎn)旋轉(zhuǎn)動畫 */ 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) { // 設(shè)置小圓點(diǎn)的進(jìn)度,并通知界面重繪 mDotProgress = (Float) animation.getAnimatedValue(); invalidate(); } }); animator.start(); } /** * 設(shè)置進(jìn)度 */ 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); } // 設(shè)置該view的寬高 setMeasuredDimension(mWidth, mHeight); } }
總結(jié)
在的onDraw方法中需要避免頻繁的new對象,所以把一些如初始化畫筆Paint的方法放到了最前面的構(gòu)造方法中進(jìn)行。
在分多個模塊繪制時,應(yīng)該使用canvas.save()和canvas.restore()的組合,來避免不同模塊繪制時的相互干擾,在這兩個方法中繪制相當(dāng)于PS中的圖層概念,上一個圖層進(jìn)行的修改不會影響到下一個圖層的顯示效果。
在需要顯示動畫效果的地方使用屬性動畫來處理,可自定義的效果強(qiáng),在系統(tǒng)提供的插值器類不夠用的情況下,我么還可通過繼承Animation類,重寫它的applyTransformation方法來處理各種復(fù)雜的動畫效果。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android中WebView加載網(wǎng)頁設(shè)置進(jìn)度條
- Android Webview添加網(wǎng)頁加載進(jìn)度條實(shí)例詳解
- Android 進(jìn)度條 ProgressBar的實(shí)現(xiàn)代碼(隱藏、出現(xiàn)、加載進(jìn)度)
- Android自定義View實(shí)現(xiàn)加載進(jìn)度條效果
- Android開發(fā)之ProgressBar字體隨著進(jìn)度條的加載而滾動
- Android自定義View基礎(chǔ)開發(fā)之圖片加載進(jìn)度條
- Android自定義帶加載動畫效果的環(huán)狀進(jìn)度條
- Android中WebView加載網(wǎng)頁設(shè)置進(jìn)度條
- Android自定義帶進(jìn)度條WebView仿微信加載過程
- Android自定義View實(shí)現(xiàn)圓形加載進(jìn)度條
相關(guān)文章
Android Studio finish()方法的使用與解決app點(diǎn)擊“返回”(直接退出)
這篇文章主要介紹了Android Studio finish()方法的使用與解決app點(diǎn)擊“返回”(直接退出),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-04-04Android提高之SurfaceView的基本用法實(shí)例分析
這篇文章主要介紹了Android提高之SurfaceView的基本用法,非常實(shí)用的功能,需要的朋友可以參考下2014-08-08Android 使用 Scroller 實(shí)現(xiàn)平滑滾動功能的示例代碼
這篇文章主要介紹了Android 使用 Scroller 實(shí)現(xiàn)平滑滾動功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07Android基于ImageView繪制的開關(guān)按鈕效果示例
這篇文章主要介紹了Android基于ImageView繪制的開關(guān)按鈕效果,結(jié)合實(shí)例形式分析了Android使用ImageView進(jìn)行按鈕繪制的界面布局、功能實(shí)現(xiàn)及相關(guān)注意事項(xiàng),需要的朋友可以參考下2017-03-03DrawerLayout結(jié)合Tollbar實(shí)現(xiàn)菜單側(cè)滑效果
這篇文章主要為大家詳細(xì)介紹了DrawerLayout結(jié)合Tollbar實(shí)現(xiàn)菜單側(cè)滑效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-12-12Android實(shí)現(xiàn)沉浸式狀態(tài)欄功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)沉浸式狀態(tài)欄功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-10-10Android?實(shí)現(xiàn)自定義圓形進(jìn)度條的三種常用方法
這篇文章主要介紹了Android?實(shí)現(xiàn)自定義圓形進(jìn)度條的三種常用方法的相關(guān)資料,需要的朋友可以參考下2023-03-03搭建Android上的服務(wù)器 “實(shí)現(xiàn)隔空取物”的方法
本篇文章主要介紹了搭建Android上的服務(wù)器 “實(shí)現(xiàn)隔空取物”的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-01-01