Android自定義控件:進度條的四種實現方式
最近一直在學習自定義控件,搜了許多大牛們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】
【Android視圖繪制流程完全解析,帶你一步步深入了解View】
【Android視圖狀態及重繪流程分析,帶你一步步深入了解View】
【Android自定義View的實現方法,帶你一步步深入了解View】
3、沒看過我寫的:Android自定義控件——老版優酷三級菜單的話,或許需要看看這個:
【RotateAnimation詳解——http://blog.csdn.net/u012403246/article/details/41415799】
三、Android上的實現方式:
(前三種方法比較簡單,第四種方法是GitHub項目的解析,對前三種沒興趣可以直接跳到後邊……)
1、效果圖:
將進度條的變換過程分解為一幀一幀的圖片,將這些一幀一幀的圖片連起來構成一個動畫。常用於:手機閱讀網頁、逛社區時,加載圖片、文章等不需要清楚知道加載進度,但是需要知道是否進行加載的情景。
這種方法實現可以通過創建一個animation-list的XML文件,然後給系統API提供的ProgressBar的indeterminateDrawable屬性就可以了。(這個屬性應該是類似於設置一個動畫吧……)
2、效果圖:
在上一篇有關自定義控件的博客裡我們使用了一個RotateAnimation類來實現旋轉效果 (http://blog.csdn.net/u012403246/article/details/41309161),其實,我們在這裡也可以把一張圖片,通過旋轉,達到我們要的效果。本質上和上一種方法沒多大區別。
我們只需要創建一個rotate的XML,對其屬性進行一些簡單的設置,然後加入我們要用的圖片就可以了。
XML:
android:pivotY="50%" android:fromDegrees="0" android:toDegrees="360" android:interpolator="@android:anim/accelerate_decelerate_interpolator"> [html]
3、效果圖:
我們可以弄兩張照片,第一張是純黑色的,然後把這張照片中心挖一個圓出來,圓區域弄成白色,挖出來的圓弄成第二張照片。我們不妨疊加顯示兩張照片,剛開始把第二張完全“遮住”,隨著加載進度的增加,我們減少遮住的區域把第二張照片慢慢的顯示出來。
Android上剛好就有這麼一個ClipDrawable類,能夠實現剪裁的過程。我們來看看怎麼通過這樣的方式自定義一個進度條控件。
代碼:
publicclassMyProgressBarextendsFrameLayout{ privatebooleanrunning; privateintprogress=0; privatestaticfinalintMAX_PROGRESS=10000; privateClipDrawableclip; privateHandlerhandler=newHandler(){ @Override publicvoidhandleMessage(android.os.Messagemsg){ if(msg.what==0x123) clip.setLevel(progress); } }; publicMyProgressBar(Contextcontext){ this(context,null,0); } publicMyProgressBar(Contextcontext,AttributeSetattrs){ this(context,null,0); } publicMyProgressBar(Contextcontext,AttributeSetattrs,intdefStyle){ super(context,attrs,defStyle); Init(context); } publicvoidInit(Contextcontext){ Viewview=LayoutInflater.from(context).inflate(R.layout.view,null); ImageViewiv=(ImageView)view.findViewById(R.id.progress_img); addView(view); clip=(ClipDrawable)iv.getDrawable(); Threadthread=newThread(newRunnable(){ @Override publicvoidrun(){ running=true; while(running){ handler.sendEmptyMessage(0x123); if(progress==MAX_PROGRESS) progress=0; progress+=100; try{ Thread.sleep(18); }catch(InterruptedExceptione){ e.printStackTrace(); } } } }); thread.start(); } publicvoidstop(){ progress=0; running=false; } }
通過代碼我們可以看到,邏輯非常簡單,關鍵就在於ClipDrawable的setLevel()方法,這個是設置剪裁效果的。
4、效果圖:
實現一個View的子類——Progress Wheel類,實現進度條效果。具體的內容我都寫在了注釋上,如果不了解自定義控件的知識,可以去閱讀guolin博客裡自定義View四部曲的講解,講的挺好的。
代碼:
publicclassProgressWheelextendsView{ //繪制View用到的各種長、寬帶大小 privateintlayout_height=0; privateintlayout_width=0; privateintfullRadius=100; privateintcircleRadius=80; privateintbarLength=60; privateintbarWidth=20; privateintrimWidth=20; privateinttextSize=20; privatefloatcontourSize=0; //與頁邊的間距 privateintpaddingTop=5; privateintpaddingBottom=5; privateintpaddingLeft=5; privateintpaddingRight=5; //View要繪制的顏色 privateintbarColor=0xAA000000; privateintcontourColor=0xAA000000; privateintcircleColor=0x00000000; privateintrimColor=0xAADDDDDD; privateinttextColor=0xFF000000; //繪制要用的畫筆 privatePaintbarPaint=newPaint(); privatePaintcirclePaint=newPaint(); privatePaintrimPaint=newPaint(); privatePainttextPaint=newPaint(); privatePaintcontourPaint=newPaint(); //繪制要用的矩形 @SuppressWarnings("unused") privateRectFrectBounds=newRectF(); privateRectFcircleBounds=newRectF(); privateRectFcircleOuterContour=newRectF(); privateRectFcircleInnerContour=newRectF(); //動畫 //每次繪制要移動的像素數目 privateintspinSpeed=2; //繪制過程的時間間隔 privateintdelayMillis=0; intprogress=0; booleanisSpinning=false; //其他 privateStringtext=""; privateString[]splitText={}; /** *ProgressWheel的構造方法 * *@paramcontext *@paramattrs */ publicProgressWheel(Contextcontext,AttributeSetattrs){ 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 protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){ //首先我們要調用超類的onMeasure借口 //原因是我們自己去實現一個方法獲得長度、寬度太麻煩了 //使用超類的的方法非常方便而且讓復雜的細節可控 super.onMeasure(widthMeasureSpec,heightMeasureSpec); //在這裡我們不能使用getWidth()和getHeight()。 //因為這兩個方法只能在View的布局完成後才能使用,而一個View的繪制過程是先繪制元素,再繪制Layout //所以我們必須使用getMeasuredWidth()和getMeasuredHeight() intsize=0; intwidth=getMeasuredWidth(); intheight=getMeasuredHeight(); intwidthWithoutPadding=width-getPaddingLeft()-getPaddingRight(); intheigthWithoutPadding=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 */ @Override protectedvoidonSizeChanged(intw,inth,intoldw,intoldh){ super.onSizeChanged(w,h,oldw,oldh); //Sharethedimensions layout_width=w; layout_height=h; setupBounds(); setupPaints(); invalidate(); } /** *設置我們想要繪制的progresswheel的顏色 */ privatevoidsetupPaints(){ 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); } /** *設置元素的邊界 */ privatevoidsetupBounds(){ //為了保持寬度和長度的一致,我們要獲得layout_width和layout_height中較小的一個,從而繪制一個圓 intminValue=Math.min(layout_width,layout_height); //計算在繪制過程中在x,y方向的偏移量 intxOffset=layout_width-minValue; intyOffset=layout_height-minValue; //間距加上偏移量 paddingTop=this.getPaddingTop()+(yOffset/2); paddingBottom=this.getPaddingBottom()+(yOffset/2); paddingLeft=this.getPaddingLeft()+(xOffset/2); paddingRight=this.getPaddingRight()+(xOffset/2); intwidth=getWidth();//this.getLayoutParams().width; intheight=getHeight();//this.getLayoutParams().height; rectBounds=newRectF(paddingLeft, paddingTop, width-paddingRight, height-paddingBottom); circleBounds=newRectF(paddingLeft+barWidth, paddingTop+barWidth, width-paddingRight-barWidth, height-paddingBottom-barWidth); circleInnerContour=newRectF(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=newRectF(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中解析控件的屬性 * *@paramatheattributestoparse */ privatevoidparseAttributes(TypedArraya){ 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(); } //---------------------------------- //動畫 //---------------------------------- protectedvoidonDraw(Canvascanvas){ 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); }else{ canvas.drawArc(circleBounds,-90,progress,false,barPaint); } //繪制我們想要設置的文字(並讓它顯示在圓水平和垂直方向的中心處) floattextHeight=textPaint.descent()-textPaint.ascent(); floatverticalTextOffset=(textHeight/2)-textPaint.descent(); for(Strings:splitText){ floathorizontalTextOffset=textPaint.measureText(s)/2; canvas.drawText(s,this.getWidth()/2-horizontalTextOffset, this.getHeight()/2+verticalTextOffset,textPaint); } if(isSpinning){ scheduleRedraw(); } } privatevoidscheduleRedraw(){ progress+=spinSpeed; if(progress>360){ progress=0; } postInvalidateDelayed(delayMillis); } /** *判斷wheel是否在旋轉 */ publicbooleanisSpinning(){ if(isSpinning){ returntrue; }else{ returnfalse; } } /** *重設進度條的值 */ publicvoidresetCount(){ progress=0; setText("0%"); invalidate(); } /** *停止進度條的旋轉 */ publicvoidstopSpinning(){ isSpinning=false; progress=0; postInvalidate(); } /** *讓進度條開啟旋轉模式 */ publicvoidspin(){ isSpinning=true; postInvalidate(); } /** *讓進度條每次增加1(最大值為360) */ publicvoidincrementProgress(){ isSpinning=false; progress++; if(progress>360) progress=0; setText(Math.round(((float)progress/360)*100)+"%"); postInvalidate(); } /** *設置進度條為一個確切的數值 */ publicvoidsetProgress(inti){ isSpinning=false; progress=i; postInvalidate(); } //---------------------------------- //get和set方法 //---------------------------------- /** *設置progressbar的文字並不需要刷新View * *@paramtextthetexttoshow('\n'constitutesanewline) */ publicvoidsetText(Stringtext){ this.text=text; splitText=this.text.split("\n"); } publicintgetCircleRadius(){ returncircleRadius; } publicvoidsetCircleRadius(intcircleRadius){ this.circleRadius=circleRadius; } publicintgetBarLength(){ returnbarLength; } publicvoidsetBarLength(intbarLength){ this.barLength=barLength; } publicintgetBarWidth(){ returnbarWidth; } publicvoidsetBarWidth(intbarWidth){ this.barWidth=barWidth; if(this.barPaint!=null){ this.barPaint.setStrokeWidth(this.barWidth); } } publicintgetTextSize(){ returntextSize; } publicvoidsetTextSize(inttextSize){ this.textSize=textSize; if(this.textPaint!=null){ this.textPaint.setTextSize(this.textSize); } } publicintgetPaddingTop(){ returnpaddingTop; } publicvoidsetPaddingTop(intpaddingTop){ this.paddingTop=paddingTop; } publicintgetPaddingBottom(){ returnpaddingBottom; } publicvoidsetPaddingBottom(intpaddingBottom){ this.paddingBottom=paddingBottom; } publicintgetPaddingLeft(){ returnpaddingLeft; } publicvoidsetPaddingLeft(intpaddingLeft){ this.paddingLeft=paddingLeft; } publicintgetPaddingRight(){ returnpaddingRight; } publicvoidsetPaddingRight(intpaddingRight){ this.paddingRight=paddingRight; } publicintgetBarColor(){ returnbarColor; } publicvoidsetBarColor(intbarColor){ this.barColor=barColor; if(this.barPaint!=null){ this.barPaint.setColor(this.barColor); } } publicintgetCircleColor(){ returncircleColor; } publicvoidsetCircleColor(intcircleColor){ this.circleColor=circleColor; if(this.circlePaint!=null){ this.circlePaint.setColor(this.circleColor); } } publicintgetRimColor(){ returnrimColor; } publicvoidsetRimColor(intrimColor){ this.rimColor=rimColor; if(this.rimPaint!=null){ this.rimPaint.setColor(this.rimColor); } } publicShadergetRimShader(){ returnrimPaint.getShader(); } publicvoidsetRimShader(Shadershader){ this.rimPaint.setShader(shader); } publicintgetTextColor(){ returntextColor; } publicvoidsetTextColor(inttextColor){ this.textColor=textColor; if(this.textPaint!=null){ this.textPaint.setColor(this.textColor); } } publicintgetSpinSpeed(){ returnspinSpeed; } publicvoidsetSpinSpeed(intspinSpeed){ this.spinSpeed=spinSpeed; } publicintgetRimWidth(){ returnrimWidth; } publicvoidsetRimWidth(intrimWidth){ this.rimWidth=rimWidth; if(this.rimPaint!=null){ this.rimPaint.setStrokeWidth(this.rimWidth); } } publicintgetDelayMillis(){ returndelayMillis; } publicvoidsetDelayMillis(intdelayMillis){ this.delayMillis=delayMillis; } publicintgetContourColor(){ returncontourColor; } publicvoidsetContourColor(intcontourColor){ this.contourColor=contourColor; if(contourPaint!=null){ this.contourPaint.setColor(this.contourColor); } } publicfloatgetContourSize(){ returnthis.contourSize; } publicvoidsetContourSize(floatcontourSize){ this.contourSize=contourSize; if(contourPaint!=null){ this.contourPaint.setStrokeWidth(this.contourSize); } } }