馬上要校招了,有點(diǎn)兒慌,寫個(gè)自定義控件壓壓驚
效果圖
本來寫了一大串近段時(shí)間的感慨,還是覺得廢話少說比較不容易被噴,直接上效果圖

就是這個(gè)樣子,下面記錄一下我的編寫經(jīng)歷,擼起袖子就是干
如果不想看實(shí)現(xiàn)原理,可以直接拿去用,在我的github上有使用方法,如果能給個(gè)star的話,只能說明...你很有前途
github源碼
前期準(zhǔn)備
說實(shí)在的,作為菜鳥一個(gè),每次一聽到自定義view依然是覺得寫出來的人NB的不行,而自己依然懵逼的不行

不過看了幾篇很好的寫view的文章,對自己啟發(fā)比較大,也推薦給大家有時(shí)間可以去看看,不要謝我
Android自定義View的官方套路
我奶奶都能懂的UI繪制流程(上)!
我奶奶都能懂的UI繪制流程(下)!
另外還有一篇,我的這個(gè)view也是在他的基礎(chǔ)上編寫并做了部分修改的,感謝大神
自定義View練習(xí)
自定義屬性
這里的自定義屬性主要就是到時(shí)候在xml文件中引入控件時(shí)要用到的,在values目錄下新建attrs.xml文件,自定義屬性如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CirclePercentBar">
<attr name="arcColor" format="color"/>
<attr name="arcWidth" format="dimension"/>
<attr name="centerTextColor" format="color"/>
<attr name="centerTextSize" format="dimension"/>
<attr name="circleRadius" format="dimension"/>
<attr name="arcStartColor" format="color"/>
<attr name="arcEndColor" format="color"/>
</declare-styleable>
</resources>
這里面都是比較簡單的,具體的是什么意思在這就不啰嗦了,如果有不知道的可以google一下就能知道了
自定義View類
自定義一個(gè)CirclePercentBar類繼承自View
public class CirclePercentBar extends View{
public CirclePercentBar(Context context) {
this(context, null);
}
public CirclePercentBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CirclePercentBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
添加了3個(gè)構(gòu)造函數(shù),這個(gè)地方需要注意一下(對于初學(xué)者有坑):
如果不需要在xml文件中使用控件,就實(shí)現(xiàn)第一個(gè)構(gòu)造函數(shù)即可;
如果需要在xml中定義,需要傳入屬性值的時(shí)候,系統(tǒng)將調(diào)用第二個(gè)構(gòu)造函數(shù);
第三個(gè)系統(tǒng)不會(huì)自動(dòng)去調(diào)用,而是用戶自己需要的時(shí)候去主動(dòng)調(diào)用。
針對這個(gè)坑按照習(xí)慣(我看都這樣寫的,所以就叫習(xí)慣了...哈哈哈)做了相應(yīng)的修改,就如上面的代碼所示,第一個(gè)構(gòu)造函數(shù)去調(diào)用第二個(gè),第二個(gè)調(diào)用第三個(gè),然后把我們初始化的操作都寫在第三個(gè)構(gòu)造函數(shù)中,別問為什么,我也不知道...以后知道了再來補(bǔ)充,當(dāng)然就規(guī)矩點(diǎn)兒寫在第二個(gè)里面也應(yīng)該是不會(huì)錯(cuò)的。
獲取屬性值
既然自定義了屬性值,而且用戶在xml文件中也做了相應(yīng)的屬性設(shè)置,自然要拿到代碼中來使用這些屬性值了,獲取的過程也很簡單,都是套路
TypedArray typedArray=context.obtainStyledAttributes(attrs
, R.styleable.CirclePercentBar, defStyleAttr,0);
mArcColor = typedArray.getColor(R.styleable.CirclePercentBar_arcColor
,0xff0000);
mArcWidth = typedArray.getDimensionPixelSize(R.styleable.CirclePercentBar_arcWidth
, DisplayUtil.dp2px(context, 20));
mCenterTextColor = typedArray.getColor(R.styleable.CirclePercentBar_centerTextColor
, 0x0000ff);
mCenterTextSize = typedArray.getDimensionPixelSize(R.styleable.CirclePercentBar_centerTextSize
, DisplayUtil.dp2px(context, 20));
mCircleRadius = typedArray.getDimensionPixelSize(R.styleable.CirclePercentBar_circleRadius
, DisplayUtil.dp2px(context, 100));
arcStartColor = typedArray.getColor(R.styleable.CirclePercentBar_arcStartColor
, ContextCompat.getColor(mContext, R.color.colorStart));
arcEndColor = typedArray.getColor(R.styleable.CirclePercentBar_arcEndColor
, ContextCompat.getColor(mContext, R.color.colorEnd));
typedArray.recycle();
利用TypeArray對象來獲取屬性值,根據(jù)自定義屬性的類型,定義的是顏色屬性,就用它的getcolor方法,獲取的是尺寸屬性,就用getDimensionPixelSize屬性,這里尺寸方法需要注意第二個(gè)默認(rèn)值參數(shù)需要把dp轉(zhuǎn)為px
初始化畫筆
因?yàn)閐raw方法會(huì)被調(diào)用很多次,肯定不能在畫的時(shí)候才初始化畫筆,這樣會(huì)很消耗內(nèi)存,影響性能,所以選擇在構(gòu)造函數(shù)中初始化。
private void initPaint() {
startCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
startCirclePaint.setStyle(Paint.Style.FILL);
//startCirclePaint.setStrokeWidth(mArcWidth);
startCirclePaint.setColor(arcStartColor);
arcCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
arcCirclePaint.setStyle(Paint.Style.STROKE);
arcCirclePaint.setStrokeWidth(mArcWidth);
arcCirclePaint.setColor(ContextCompat.getColor(mContext,R.color.colorCirclebg));
arcCirclePaint.setStrokeCap(Paint.Cap.ROUND);
arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
arcPaint.setStyle(Paint.Style.STROKE);
arcPaint.setStrokeWidth(mArcWidth);
arcPaint.setColor(mArcColor);
arcPaint.setStrokeCap(Paint.Cap.ROUND);
centerTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
centerTextPaint.setStyle(Paint.Style.STROKE);
centerTextPaint.setColor(mCenterTextColor);
centerTextPaint.setTextSize(mCenterTextSize);
//圓弧的外接矩形
arcRectF = new RectF();
//文字的邊界矩形
textBoundRect = new Rect();
}
這里初始化了4個(gè)畫筆,第一個(gè)畫筆是用來畫圓環(huán)頂部的一個(gè)圓的,這個(gè)到后面再說為什么我選擇畫了一個(gè)圓;第二個(gè)畫筆是用來畫未填充狀態(tài)的背景圓環(huán)的;第三個(gè)畫筆用來畫百分比占據(jù)的彩色弧形環(huán)的,也就是效果圖中動(dòng)起來的圓弧部分,類型采用了描邊,圓角;第四個(gè)畫筆用來寫中間的百分比文字。
另外還在這里初始化了一個(gè)定位圓弧的外界矩形和定位文字的邊界矩形
重寫onMeasure
到這構(gòu)造函數(shù)需要做的初始化工作就基本做好了,接下來的工作就是自定義控件必要的重寫onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measureView(widthMeasureSpec), measureView(heightMeasureSpec));
}
private int measureView(int measureSpec) {
int result;
int specMode=MeasureSpec.getMode(measureSpec);
int specSize=MeasureSpec.getSize(measureSpec);
if(specMode==MeasureSpec.EXACTLY){
result=specSize;
}else{
result=mCircleRadius*2;
if(specMode==MeasureSpec.AT_MOST){
result=Math.min(result,specSize);
}
}
return result;
}
關(guān)于為什么要重寫onMeasure方法,還有測量都是些什么鬼,我相信很多初學(xué)者都會(huì)感到持續(xù)懵b,如果你想詳細(xì)了解下可以看看這篇文章
為什么重寫onMeasure()以及怎么重寫
不想細(xì)看的可以看看我下面簡單的解釋
測量就是為了告訴系統(tǒng)我這個(gè)控件該畫多大,如果是給了確定的值比如設(shè)置的是Match_parent或者特定的dp值就很簡單了,即按照measureSpec給出的大小返回就行,如果設(shè)置的是wrap_content,系統(tǒng)本身是不知道你的控件內(nèi)部元素到底有多大的,所以就需要計(jì)算出一個(gè)最小值告訴給系統(tǒng)
如上述代碼所示,如果判斷得到設(shè)置的模式是MeasureSpec.EXACTLY,就把MeasureSpec中的尺寸值返回就行,如果判斷得到設(shè)置的模式是MeasureSpec.AT_MOST,也就是代碼中設(shè)置的 wrap_content,就比較圓環(huán)的直徑和MeasureSpec中給出的尺寸值,取最小的一個(gè)返回,最后調(diào)用setMeasuredDimension方法,傳入處理后的長寬值。
重寫onDraw方法
這里就到了核心的地方了,畫我們要展示的view了,先上代碼:
canvas.rotate(-90, getWidth()/ 2, getHeight()/ 2);
arcRectF.set(getWidth()/2-mCircleRadius+mArcWidth/2,getHeight()/2-mCircleRadius+mArcWidth/2
,getWidth()/2+mCircleRadius-mArcWidth/2,getHeight()/2+mCircleRadius-mArcWidth/2);
canvas.drawArc(arcRectF, 0,360,false,arcCirclePaint);
arcPaint.setShader(new SweepGradient(getWidth()/2,getHeight()/2,arcStartColor,arcEndColor));
canvas.drawArc(arcRectF, 0,360* mCurData /100,false,arcPaint);
canvas.rotate(90, getWidth()/ 2, getHeight()/ 2);
canvas.drawCircle(getWidth()/2,getHeight()/2-mCircleRadius+mArcWidth/2,mArcWidth/2,startCirclePaint);
String data= String.valueOf(mCurData) +"%";
centerTextPaint.getTextBounds(data,0,data.length(),textBoundRect);
canvas.drawText(data,getWidth()/2-textBoundRect.width()/2,getHeight()/2+textBoundRect.height()/2,centerTextPaint);
1、首先將畫布繞中心點(diǎn)逆時(shí)針旋轉(zhuǎn)了90度,做這個(gè)是因?yàn)樵诤竺娈嫕u變色的圓弧時(shí),drawArc和SweepGradient這兩個(gè)類的起始點(diǎn)0度不是在我們習(xí)慣的圓環(huán)最上面那個(gè)點(diǎn),而是從圓環(huán)最右邊那個(gè)點(diǎn)開始,所以逆時(shí)針旋轉(zhuǎn)90度就能讓它從最上面的點(diǎn)開始

2、接下來首先要畫未填充狀態(tài)的圓環(huán),這個(gè)很簡單,用drawArc方法從0度到360度就好了,這個(gè)地方的坑點(diǎn)在于調(diào)用之前給圓環(huán)定位的外接矩形,這個(gè)地方稍微有點(diǎn)兒繞,看圖說話

如圖所示,紅色的矩形就是該圓環(huán)的外接矩形,而圓環(huán)的邊是有寬度的,所以這樣就有一問題了
需要發(fā)揮一點(diǎn)兒想象力,想一下假設(shè)我們設(shè)置的該控件的大小是wrap或者特定的大小,反正就是當(dāng)控件的長寬小于2r+width這個(gè)長度,而這個(gè)時(shí)候我們一般會(huì)把外接矩形長寬直接設(shè)置為2r,覺得正好,然而,當(dāng)運(yùn)行出來的時(shí)候就傻眼了,圓環(huán)上下左右四個(gè)地方缺少了一塊兒,因?yàn)閳A環(huán)的邊是有寬度的,半徑相當(dāng)于多了width/2的寬度
所以為了避免這種情況,我們在設(shè)置外接矩形的時(shí)候,提前把我們要畫的圓環(huán)半徑縮小width/2就好了,所以就有了代碼中設(shè)置外接矩形時(shí)參數(shù)里包括width/2的設(shè)置
3、緊接著要畫的就是百分比填充的彩色圓環(huán),外接矩形就可以用上面設(shè)置好的,唯一的不同就是畫筆需要設(shè)置一個(gè)漸變色的渲染效果,利用setShader方法
4、然后在圓環(huán)的頂部起始位置又畫了一個(gè)實(shí)心圓。
因?yàn)楫媹A弧的畫筆是圓頭類型的,在起始地方0度偏左還會(huì)有一個(gè)半圓,但是我們又采用了漸變色渲染,所以圓頭部分就變成了結(jié)束的顏色值,就是這樣

這效果簡直不能忍,我也嘗試了補(bǔ)償起始角度來改變這種情況,但效果都不理想,所以最后找了一個(gè)最暴力的解決方式,用一個(gè)圓直接覆蓋起始部分就好了...
5、最后就是添加中間的百分比文字就,這里沒啥說的,定位到文字起始的位置,用drawText就好
暴露設(shè)置百分比的方法
這么一個(gè)控件,自然要暴露給用戶一個(gè)動(dòng)態(tài)設(shè)置百分比的方法
public void setPercentData(float data, TimeInterpolator interpolator){
ValueAnimator valueAnimator=ValueAnimator.ofFloat(mCurData,data);
valueAnimator.setDuration((long) (Math.abs(mCurData-data)*30));
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float value= (float) valueAnimator.getAnimatedValue();
mCurData=(float)(Math.round(value*10))/10;
invalidate();
}
});
valueAnimator.setInterpolator(interpolator);
valueAnimator.start();
}
這個(gè)地方用到了ValueAnimator,然后監(jiān)聽valueAnimaer數(shù)值的更新,在回調(diào)中設(shè)置相應(yīng)百分比參數(shù),調(diào)用invalidate,重繪view,這樣就達(dá)到了動(dòng)畫改變的效果了
總結(jié)
到這,這個(gè)自定義的圓環(huán)百分比控件就完成了,回想一下確實(shí)也挺簡單的
感覺就是和大神們說的一樣,自定義View看起來很高端,我們學(xué)習(xí)的時(shí)候如果就從基礎(chǔ)的各種measure之類的基礎(chǔ)理論看起的話,很枯燥,也很抽象,不如就拿一個(gè)自定義view上手寫,先寫簡單的,領(lǐng)悟其中的原理,不懂得地方再去查相應(yīng)的知識點(diǎn),這樣學(xué)起來效率會(huì)高很多,然后循序漸進(jìn),相信慢慢就會(huì)有明顯的進(jìn)步的
菜鳥一個(gè),如果有什么不對的地方,希望能指正提意見...
2017.7.28 12:05
三教 806 實(shí)驗(yàn)室