版權聲明:本文為部落客原創文章,轉載請标明出處。 https://blog.csdn.net/chaoyu168/article/details/50722935
前三種實作方式代碼出自:
http://stormzhang.com/openandroid/2013/11/15/android-custom-loading/
(源碼下載下傳)
http://download.csdn.net/detail/chaoyu168/9616035最近一直在學習自定義控件,搜了許多大牛們Blog裡分享的小教程,也上GitHub找了一些類似的控件進行學習。發現讀起來都不太好懂,就想寫這麼一篇東西作為學習筆記吧。
一、控件介紹:
進度條在App中非常常見,例如下載下傳進度、加載圖檔、打開文章、打開網頁等等……都需要這麼一個效果讓使用者知道我們的App正在讀取,以構造良好的互動。如果沒有這樣一個效果的話,使用者沒法知道東西有沒有下載下傳好、圖檔加載了沒有、文章打開了沒……會讓使用者很不爽。基于這樣的情景我們的UI設計師們創造了這樣一個控件。
二、這篇文章會涉及的知識點:
跟我一樣剛入門的Android菜鳥們,我推薦大家先了解一下這些知識點再往下看。這些知識點我也會推薦一些部落格給大家看看,更推薦大家看文檔裡的解釋,當然大牛們可以直接無視……
1、ClipDrawable類:能夠對一個drawable類進行剪切操作(即隻顯示某一部分的區域,另一部分隐藏),顯示多大的區域由level控制(level取值是0~10000)
【部落格:http://blog.csdn.net/lonelyroamer/article/details/8244777】、沒文檔的可以在這看【http://www.apihome.cn/api/android/ClipDrawable.html】
2、自定義View:guolin大神的深入學習View四部曲
【
Android
LayoutInflater原理分析,帶你一步步深入了解View
——http://blog.csdn.net/guolin_blog/article/details/12921889】 Android視圖繪制流程完全解析,帶你一步步深入了解View ——http://blog.csdn.net/guolin_blog/article/details/16330267】 Android視圖狀态及重繪流程分析,帶你一步步深入了解View ——http://blog.csdn.net/guolin_blog/article/details/17045157】 Android自定義View的實作方法,帶你一步步深入了解View——http://blog.csdn.net/guolin_blog/article/details/17357967】
3、沒看過我寫的:Android自定義控件——老版優酷三級菜單的話,或許需要看看這個:
【RotateAnimation詳解——http://blog.csdn.net/u012403246/article/details/41415799】
三、Android上的實作方式:
(前三種方法比較簡單,第四種方法是GitHub項目的解析,對前三種沒興趣可以直接跳到後邊……)
1、效果圖:
将進度條的變換過程分解為一幀一幀的圖檔,将這些一幀一幀的圖檔連起來構成一個動畫。常用于:手機閱讀網頁、逛社群時,加載圖檔、文章等不需要清楚知道加載進度,但是需要知道是否進行加載的情景。
這種方法實作可以通過建立一個animation-list的XML檔案,然後給系統API提供的ProgressBar的indeterminateDrawable屬性就可以了。(這個屬性應該是類似于設定一個動畫吧……)
XML:
[html]
view
plain
copy https://code.csdn.net/snippets/627396 https://code.csdn.net/snippets/627396/fork- <?xml version="1.0" encoding="utf-8"?>
- <animation-list xmlns:android="http://schemas.android.com/apk/res/android"
- android:oneshot="false" >
- <item android:duration="150" >
- <clip
- android:clipOrientation="horizontal"
- android:drawable="@drawable/loading_01"
- android:gravity="left"/>
- </item>
- android:drawable="@drawable/loading_02"
- android:drawable="@drawable/loading_03"
- android:drawable="@drawable/loading_04"
- android:drawable="@drawable/loading_05"
- android:drawable="@drawable/loading_06"
- android:drawable="@drawable/loading_07"
- android:drawable="@drawable/loading_08"
- android:drawable="@drawable/loading_09"
- android:drawable="@drawable/loading_10"
- android:drawable="@drawable/loading_11"
- android:drawable="@drawable/loading_12"
- </animation-list>
- <ProgressBar
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:indeterminateDrawable="@drawable/progressbar1"
- />
2、效果圖:
在上一篇有關自定義控件的部落格裡我們使用了一個RotateAnimation類來實作旋轉效果 (http://blog.csdn.net/u012403246/article/details/41309161),其實,我們在這裡也可以把一張圖檔,通過旋轉,達到我們要的效果。本質上和上一種方法沒多大差別。
我們隻需要建立一個rotate的XML,對其屬性進行一些簡單的設定,然後加入我們要用的圖檔就可以了。
https://code.csdn.net/snippets/627396 https://code.csdn.net/snippets/627396/fork- <rotate xmlns:android="http://schemas.android.com/apk/res/android"
- android:pivotX="50%"
- android:pivotY="50%"
- android:fromDegrees="0"
- android:toDegrees="360"
- android:interpolator="@android:anim/accelerate_decelerate_interpolator" >
- <bitmap
- android:antialias="true"
- android:filter="true"
- android:src="@drawable/loading_360"/>
- </rotate>
- android:indeterminateDrawable="@drawable/progressbar2"/>
3、效果圖:
我們可以弄兩張照片,第一張是純黑色的,然後把這張照片中心挖一個圓出來,圓區域弄成白色,挖出來的圓弄成第二張照片。我們不妨疊加顯示兩張照片,剛開始把第二張完全“遮住”,随着加載進度的增加,我們減少遮住的區域把第二張照片慢慢的顯示出來。
Android上剛好就有這麼一個ClipDrawable類,能夠實作剪裁的過程。我們來看看怎麼通過這樣的方式自定義一個進度條控件。
代碼:
[java]
https://code.csdn.net/snippets/627396 https://code.csdn.net/snippets/627396/fork- public class MyProgressBar extends FrameLayout{
- private boolean running;
- private int progress = 0;
- private static final int MAX_PROGRESS = 10000;
- private ClipDrawable clip;
- private Handler handler = new Handler(){
- @Override
- public void handleMessage(android.os.Message msg) {
- if(msg.what == 0x123)
- clip.setLevel(progress);
- }
- };
- public MyProgressBar(Context context){
- this(context,null,0);
- }
- public MyProgressBar(Context context,AttributeSet attrs){
- public MyProgressBar(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- Init(context);
- public void Init(Context context){
- View view = LayoutInflater.from(context).inflate(R.layout.view, null);
- ImageView iv = (ImageView)view.findViewById(R.id.progress_img);
- addView(view);
- clip = (ClipDrawable)iv.getDrawable();
- Thread thread = new Thread(new Runnable() {
- @Override
- public void run() {
- running = true;
- while(running){
- handler.sendEmptyMessage(0x123);
- if(progress == MAX_PROGRESS)
- progress = 0;
- progress += 100;
- try {
- Thread.sleep(18);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- });
- thread.start();
- public void stop(){
- progress = 0;
- running = false;
- }
通過代碼我們可以看到,邏輯非常簡單,關鍵就在于ClipDrawable的setLevel()方法,這個是設定剪裁效果的。
4、效果圖:
實作一個View的子類——Progress Wheel類,實作進度條效果。具體的内容我都寫在了注釋上,如果不了解自定義控件的知識,可以去閱讀guolin部落格裡自定義View四部曲的講解,講的挺好的。
https://code.csdn.net/snippets/627396 https://code.csdn.net/snippets/627396/fork- public class ProgressWheel extends View {
- //繪制View用到的各種長、寬帶大小
- private int layout_height = 0;
- private int layout_width = 0;
- private int fullRadius = 100;
- private int circleRadius = 80;
- private int barLength = 60;
- private int barWidth = 20;
- private int rimWidth = 20;
- private int textSize = 20;
- private float contourSize = 0;
- //與頁邊的間距
- private int paddingTop = 5;
- private int paddingBottom = 5;
- private int paddingLeft = 5;
- private int paddingRight = 5;
- //View要繪制的顔色
- private int barColor = 0xAA000000;
- private int contourColor = 0xAA000000;
- private int circleColor = 0x00000000;
- private int rimColor = 0xAADDDDDD;
- private int textColor = 0xFF000000;
- //繪制要用的畫筆
- private Paint barPaint = new Paint();
- private Paint circlePaint = new Paint();
- private Paint rimPaint = new Paint();
- private Paint textPaint = new Paint();
- private Paint contourPaint = new Paint();
- //繪制要用的矩形
- @SuppressWarnings("unused")
- private RectF rectBounds = new RectF();
- private RectF circleBounds = new RectF();
- private RectF circleOuterContour = new RectF();
- private RectF circleInnerContour = new RectF();
- //動畫
- //每次繪制要移動的像素數目
- private int spinSpeed = 2;
- //繪制過程的時間間隔
- private int delayMillis = 0;
- int progress = 0;
- boolean isSpinning = false;
- //其他
- private String text = "";
- private String[] splitText = {};
- /**
- * ProgressWheel的構造方法
- *
- * @param context
- * @param attrs
- */
- public ProgressWheel(Context context, AttributeSet attrs) {
- super(context, attrs);
- parseAttributes(context.obtainStyledAttributes(attrs,
- R.styleable.ProgressWheel));
- //----------------------------------
- //初始化一些元素
- /*
- * 調用這個方法時,使View繪制為方形
- * From: http://www.jayway.com/2012/12/12/creating-custom-android-views-part-4-measuring-and-how-to-force-a-view-to-be-square/
- *
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // 首先我們要調用超類的onMeasure借口
- // 原因是我們自己去實作一個方法獲得長度、寬度太麻煩了
- // 使用超類的的方法非常友善而且讓複雜的細節可控
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- // 在這裡我們不能使用getWidth()和getHeight()。
- // 因為這兩個方法隻能在View的布局完成後才能使用,而一個View的繪制過程是先繪制元素,再繪制Layout
- // 是以我們必須使用getMeasuredWidth()和getMeasuredHeight()
- int size = 0;
- int width = getMeasuredWidth();
- int height = getMeasuredHeight();
- int widthWithoutPadding = width - getPaddingLeft() - getPaddingRight();
- int heigthWithoutPadding = height - getPaddingTop() - getPaddingBottom();
- // 最後我們用一些簡單的邏輯去計算View的大小并調用setMeasuredDimension()去設定View的大小
- // 在比較View的長寬前我們不考慮間距,但當我們設定View所需要繪制的面積時,我們要考慮它
- // 不考慮間距的View(View内的實際畫面)此時就應該是方形的,但是由于間距的存在,最終View所占的面積可能不是方形的
- if (widthWithoutPadding > heigthWithoutPadding) {
- size = heigthWithoutPadding;
- } else {
- size = widthWithoutPadding;
- // 如果你重寫了onMeasure()方法,你必須調用setMeasuredDimension()方法
- // 這是你設定View大小的唯一途徑
- // 如果你不調用setMeasuredDimension()方法,父控件會抛出異常,并且程式會崩潰
- // 如果我們使用了超類的onMeasure()方法,我們就不是那麼需要setMeasuredDimension()方法
- // 然而,重寫onMeasure()方法是為了改變既有的繪制流程,是以我們必須調用setMeasuredDimension()方法以達到我們的目的
- setMeasuredDimension(size + getPaddingLeft() + getPaddingRight(), size + getPaddingTop() + getPaddingBottom());
- * 使用onSizeChanged方法代替onAttachedToWindow獲得View的面積
- * 因為這個方法會在測量了MATCH_PARENT和WRAP_CONTENT後馬上被調用
- * 使用獲得的面積設定View
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- // Share the dimensions
- layout_width = w;
- layout_height = h;
- setupBounds();
- setupPaints();
- invalidate();
- * 設定我們想要繪制的progress wheel的顔色
- private void setupPaints() {
- barPaint.setColor(barColor);
- barPaint.setAntiAlias(true);
- barPaint.setStyle(Style.STROKE);
- barPaint.setStrokeWidth(barWidth);
- rimPaint.setColor(rimColor);
- rimPaint.setAntiAlias(true);
- rimPaint.setStyle(Style.STROKE);
- rimPaint.setStrokeWidth(rimWidth);
- circlePaint.setColor(circleColor);
- circlePaint.setAntiAlias(true);
- circlePaint.setStyle(Style.FILL);
- textPaint.setColor(textColor);
- textPaint.setStyle(Style.FILL);
- textPaint.setAntiAlias(true);
- textPaint.setTextSize(textSize);
- contourPaint.setColor(contourColor);
- contourPaint.setAntiAlias(true);
- contourPaint.setStyle(Style.STROKE);
- contourPaint.setStrokeWidth(contourSize);
- * 設定元素的邊界
- private void setupBounds() {
- // 為了保持寬度和長度的一緻,我們要獲得layout_width和layout_height中較小的一個,進而繪制一個圓
- int minValue = Math.min(layout_width, layout_height);
- // 計算在繪制過程中在x,y方向的偏移量
- int xOffset = layout_width - minValue;
- int yOffset = layout_height - minValue;
- // 間距加上偏移量
- paddingTop = this.getPaddingTop() + (yOffset / 2);
- paddingBottom = this.getPaddingBottom() + (yOffset / 2);
- paddingLeft = this.getPaddingLeft() + (xOffset / 2);
- paddingRight = this.getPaddingRight() + (xOffset / 2);
- int width = getWidth(); //this.getLayoutParams().width;
- int height = getHeight(); //this.getLayoutParams().height;
- rectBounds = new RectF(paddingLeft,
- paddingTop,
- width - paddingRight,
- height - paddingBottom);
- circleBounds = new RectF(paddingLeft + barWidth,
- paddingTop + barWidth,
- width - paddingRight - barWidth,
- height - paddingBottom - barWidth);
- circleInnerContour = new RectF(circleBounds.left + (rimWidth / 2.0f) + (contourSize / 2.0f), circleBounds.top + (rimWidth / 2.0f) + (contourSize / 2.0f), circleBounds.right - (rimWidth / 2.0f) - (contourSize / 2.0f), circleBounds.bottom - (rimWidth / 2.0f) - (contourSize / 2.0f));
- circleOuterContour = new RectF(circleBounds.left - (rimWidth / 2.0f) - (contourSize / 2.0f), circleBounds.top - (rimWidth / 2.0f) - (contourSize / 2.0f), circleBounds.right + (rimWidth / 2.0f) + (contourSize / 2.0f), circleBounds.bottom + (rimWidth / 2.0f) + (contourSize / 2.0f));
- fullRadius = (width - paddingRight - barWidth) / 2;
- circleRadius = (fullRadius - barWidth) + 1;
- * 從XML中解析控件的屬性
- * @param a the attributes to parse
- private void parseAttributes(TypedArray a) {
- barWidth = (int) a.getDimension(R.styleable.ProgressWheel_barWidth,
- barWidth);
- rimWidth = (int) a.getDimension(R.styleable.ProgressWheel_rimWidth,
- rimWidth);
- spinSpeed = (int) a.getDimension(R.styleable.ProgressWheel_spinSpeed,
- spinSpeed);
- delayMillis = a.getInteger(R.styleable.ProgressWheel_delayMillis,
- delayMillis);
- if (delayMillis < 0) {
- delayMillis = 0;
- barColor = a.getColor(R.styleable.ProgressWheel_barColor, barColor);
- barLength = (int) a.getDimension(R.styleable.ProgressWheel_barLength,
- barLength);
- textSize = (int) a.getDimension(R.styleable.ProgressWheel_textSize,
- textSize);
- textColor = (int) a.getColor(R.styleable.ProgressWheel_textColor,
- textColor);
- //如果text是空的,就無視它
- if (a.hasValue(R.styleable.ProgressWheel_text)) {
- setText(a.getString(R.styleable.ProgressWheel_text));
- rimColor = (int) a.getColor(R.styleable.ProgressWheel_rimColor,
- rimColor);
- circleColor = (int) a.getColor(R.styleable.ProgressWheel_circleColor,
- circleColor);
- contourColor = a.getColor(R.styleable.ProgressWheel_contourColor, contourColor);
- contourSize = a.getDimension(R.styleable.ProgressWheel_contourSize, contourSize);
- // 使用TypedArray獲得控件屬性時必須要注意:使用結束後必須回收TypedArray的對象
- a.recycle();
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- //繪制内圓
- canvas.drawArc(circleBounds, 360, 360, false, circlePaint);
- //繪制邊界
- canvas.drawArc(circleBounds, 360, 360, false, rimPaint);
- canvas.drawArc(circleOuterContour, 360, 360, false, contourPaint);
- canvas.drawArc(circleInnerContour, 360, 360, false, contourPaint);
- //繪制條紋
- if (isSpinning) {
- canvas.drawArc(circleBounds, progress - 90, barLength, false,
- barPaint);
- canvas.drawArc(circleBounds, -90, progress, false, barPaint);
- //繪制我們想要設定的文字 (并讓它顯示在圓水準和垂直方向的中心處)
- float textHeight = textPaint.descent() - textPaint.ascent();
- float verticalTextOffset = (textHeight / 2) - textPaint.descent();
- for (String s : splitText) {
- float horizontalTextOffset = textPaint.measureText(s) / 2;
- canvas.drawText(s, this.getWidth() / 2 - horizontalTextOffset,
- this.getHeight() / 2 + verticalTextOffset, textPaint);
- scheduleRedraw();
- private void scheduleRedraw() {
- progress += spinSpeed;
- if (progress > 360) {
- progress = 0;
- postInvalidateDelayed(delayMillis);
- * 判斷wheel是否在旋轉
- */
- public boolean isSpinning() {
- if(isSpinning){
- return true;
- return false;
- * 重設進度條的值
- public void resetCount() {
- setText("0%");
- * 停止進度條的旋轉
- public void stopSpinning() {
- isSpinning = false;
- postInvalidate();
- * 讓進度條開啟旋轉模式
- public void spin() {
- isSpinning = true;
- * 讓進度條每次增加1(最大值為360)
- public void incrementProgress() {
- progress++;
- if (progress > 360)
- setText(Math.round(((float) progress / 360) * 100) + "%");
- postInvalidate();
- * 設定進度條為一個确切的數值
- public void setProgress(int i) {
- progress = i;
- //get和set方法
- * 設定progress bar的文字并不需要重新整理View
- * @param text the text to show ('\n' constitutes a new line)
- public void setText(String text) {
- this.text = text;
- splitText = this.text.split("\n");
- public int getCircleRadius() {
- return circleRadius;
- public void setCircleRadius(int circleRadius) {
- this.circleRadius = circleRadius;
- public int getBarLength() {
- return barLength;
- public void setBarLength(int barLength) {
- this.barLength = barLength;
- public int getBarWidth() {
- return barWidth;
- public void setBarWidth(int barWidth) {
- this.barWidth = barWidth;
- if ( this.barPaint != null ) {
- this.barPaint.setStrokeWidth( this.barWidth );
- public int getTextSize() {
- return textSize;
- public void setTextSize(int textSize) {
- this.textSize = textSize;
- if ( this.textPaint != null ) {
- this.textPaint.setTextSize( this.textSize );
- public int getPaddingTop() {
- return paddingTop;
- public void setPaddingTop(int paddingTop) {
- this.paddingTop = paddingTop;
- public int getPaddingBottom() {
- return paddingBottom;
- public void setPaddingBottom(int paddingBottom) {
- this.paddingBottom = paddingBottom;
- public int getPaddingLeft() {
- return paddingLeft;
- public void setPaddingLeft(int paddingLeft) {
- this.paddingLeft = paddingLeft;
- public int getPaddingRight() {
- return paddingRight;
- public void setPaddingRight(int paddingRight) {
- this.paddingRight = paddingRight;
- public int getBarColor() {
- return barColor;
- public void setBarColor(int barColor) {
- this.barColor = barColor;
- this.barPaint.setColor( this.barColor );
- public int getCircleColor() {
- return circleColor;
- public void setCircleColor(int circleColor) {
- this.circleColor = circleColor;
- if ( this.circlePaint != null ) {
- this.circlePaint.setColor( this.circleColor);
- public int getRimColor() {
- return rimColor;
- public void setRimColor(int rimColor) {
- this.rimColor = rimColor;
- if ( this.rimPaint != null ) {
- this.rimPaint.setColor( this.rimColor );
- public Shader getRimShader() {
- return rimPaint.getShader();
- public void setRimShader(Shader shader) {
- this.rimPaint.setShader(shader);
- public int getTextColor() {
- return textColor;
- public void setTextColor(int textColor) {
- this.textColor = textColor;
- this.textPaint.setColor( this.textColor );
- public int getSpinSpeed() {
- return spinSpeed;
- public void setSpinSpeed(int spinSpeed) {
- this.spinSpeed = spinSpeed;
- public int getRimWidth() {
- return rimWidth;
- public void setRimWidth(int rimWidth) {
- this.rimWidth = rimWidth;
- this.rimPaint.setStrokeWidth( this.rimWidth );
- public int getDelayMillis() {
- return delayMillis;
- public void setDelayMillis(int delayMillis) {
- this.delayMillis = delayMillis;
- public int getContourColor() {
- return contourColor;
- public void setContourColor(int contourColor) {
- this.contourColor = contourColor;
- if ( contourPaint != null ) {
- this.contourPaint.setColor( this.contourColor );
- public float getContourSize() {
- return this.contourSize;
- public void setContourSize(float contourSize) {
- this.contourSize = contourSize;
- this.contourPaint.setStrokeWidth( this.contourSize );
- }