這是自定義View的第一篇文章,通過制作簡單的自定義View來了解自定義View的流程。
自定義View是Android學(xué)習(xí)和開發(fā)中必不可少的一部分。通過自定義View我們可以制作豐富絢麗的控件,自定義View主要有三種方式,具體如下:
- 繼承已有的View,來擴(kuò)展我們的View
- 組合多個(gè)View來實(shí)現(xiàn)一個(gè)復(fù)合的View
- 完全重寫View,來實(shí)現(xiàn)制作全新的控件
這里,我們講第三種方法來了解自定義View的流程。
自定義View主要依賴的方法
自定義VIew中,我們主要重寫onMeasure,onDraw這兩種方法來展現(xiàn)一個(gè)View。
onMeasure:主要工作是對(duì)我們要繪制的View進(jìn)行測量,因?yàn)椴贿M(jìn)行測量的話,系統(tǒng)不知道要繪制的View有多大,無法繪制出我們需要的樣子。
onDraw: 主要工作就是繪制我們需要的圖形,這個(gè)是自定義View中定制性最強(qiáng)也是最主要的工作。
下面我們先了解一下這兩個(gè)方法具體能做什么,然后我們通過一個(gè)實(shí)例來學(xué)習(xí)一下具體的用法。
onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
我們從上面的方法可以看出onMeasure 方法中有兩個(gè)參數(shù)widthMeasureSpec,heightMeasureSpec這兩個(gè)參數(shù)每一個(gè)參數(shù)中都包含了測量值的大小和測量的模式。
測量模式有一下三種:
- EXACTLY
當(dāng)我們把控layout_width或者layout_height屬性設(shè)為match_parent或者是個(gè)100dp那樣的精確值時(shí)就會(huì)用這種測量模式。 - AT_MOST
即最大值模式,當(dāng)控件的layout_width屬性或者layout_height指定為wrap_content時(shí),空間大小一般隨著控件的子控件或者內(nèi)容的變化而變化,這是控件的尺寸只要不超過父控件允許的最大尺寸就行了。 - UNSPECIFIED
這個(gè)是我們按照自身的想法,想繪制多大就繪制多大,沒有任何限制。
具體的做法一般是,我們先根據(jù)參數(shù),得到具體的測量模式與測量值,在根據(jù)測試的模式不同,計(jì)算不同的寬度和高度。最后通過setMeasuredDimension(int measuredWidth,int measuredHeight)將我們計(jì)算過的寬和高設(shè)置進(jìn)去,完成測量工作。
onDraw
onDraw(Canvas canvas)是我們展現(xiàn)自定義View的主要方法,他的參數(shù)是一個(gè)canvas也就是說一個(gè)畫布,為了繪制圖案,我們有了畫布以外,我們還需要一個(gè)畫筆Paint。這個(gè)畫筆來決定你畫圖案的顏色,線條的粗細(xì),是否抗鋸齒,圖案的風(fēng)格等等。而畫什么圖案就交由canvas對(duì)象,調(diào)用canvas.drawXXX()來實(shí)現(xiàn)想要的圖案,具體的文檔,參見Canvas官方文檔
自制圓形載入View
上面說了這么多,都是在YY,我們具體通過一個(gè)例子來走一遍自定義View的流程。效果如下:

首先我們新建一個(gè)CircleLoadingView.java文件。該類繼承View,生成構(gòu)造器,顯式調(diào)用父類的構(gòu)造器,并初始化我們的Paint對(duì)象,覆寫onMeasure,onDraw方法,實(shí)現(xiàn)自定義View.
第一步——初始化
在構(gòu)造方法中,我們調(diào)用自己寫的initView方法來初始化Paint對(duì)象
private void initView(){
paint = new Paint();
//設(shè)置畫筆的顏色
paint.setColor(circleColor);
//設(shè)置抗鋸齒,讓圖像更清晰
paint.setAntiAlias(true);
//設(shè)置畫筆的風(fēng)格,有三種屬性FILL,STROLE,FILL_AND_STROKE,我們不需要填充,所以設(shè)置為STROKE
paint.setStyle(Paint.Style.STROKE);
//設(shè)置畫筆的粗細(xì)
paint.setStrokeWidth(circleStrokewidth);
}
第二步——onMeasure
接下來,我們需要測量我們的View的大小,重寫onMeasure()方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//設(shè)置一個(gè)默認(rèn)的寬和高,AT_MOST模式需要
int result = 0;
//通過MeasureSpec.getMode與getSize方法獲取寬和高的測量方式與測量大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//保存最后的測量值,優(yōu)化代碼的話可以不用這個(gè)變量的。
int width = 0,height = 0;
//對(duì)測量模式進(jìn)行判斷,如果是EXACTLY的話則最后的測量值就是系統(tǒng)幫我們測量的結(jié)果。
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
}else{
//如果是UNSPECIFIED 則使用我們的默認(rèn)值作為最后的測量值
result = 300;
width = result;
//如果是AT_MOST 則就要用系統(tǒng)測量結(jié)果與我們默認(rèn)結(jié)果取最小值來決定最后的測量結(jié)果
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(result,widthSize);
}
}
//高度和寬度的過程是一致的。
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
}else {
result = 300;
height = result;
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(result, heightSize);
}
}
//把我們最后的寬和高設(shè)置進(jìn)去
setMeasuredDimension(width,height);
}
這就是onMeasure方法的代碼,可以看出來,這個(gè)都是可以放到其他地方復(fù)用的模板代碼。
第三步——onDraw
我們基本已經(jīng)完成了80%的工作了,接下來只需要重寫onDraw()繪制我們需要的一個(gè)弧形就可以了。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//決定我們的弧形外接的矩形的寬和高
float wdistance = (float)(wlength * 0.6);
float hdistance = (float)(hlength * 0.6);
//定義外接矩形
RectF rectF = new RectF(
wlength / 2 - wdistance / 2,
hlength / 2 - hdistance / 2,
wlength / 2 + wdistance / 2,
hlength / 2 + hdistance / 2);
//畫弧形
canvas.drawArc(
rectF,//外接矩形
270,//起始角度
(float)(240),//劃過的度數(shù)
false,//是否是扇形
paint//畫筆
);
}
看了上面的代碼,可能有些糊涂,因?yàn)槔锩娑嗔撕芏鄾]有提到的變量。首先的變量就是wlength,與hlength這個(gè)指的是我們onMeasure測量以后的寬和高,我們保存下來。在onMeasure方法中,我們加入下面代碼。
wlength = width;
hlength = height;
同時(shí)我們定義了外接矩形的寬和高就是我們View的0.6倍,上,下, 左,右各留了0.2倍的內(nèi)邊距。然后通過new Rectf()方法來生成矩形對(duì)象,4個(gè)參數(shù)分別為矩形的左邊距Y軸的距離(也就是說X軸的坐標(biāo)),上邊距X軸的距離(也就是說Y軸的坐標(biāo)),右邊距Y軸的距離(也就是說X軸的坐標(biāo)),下邊距Y軸的距離(也就是說Y軸的坐標(biāo))。具體見下圖

canvas 實(shí)際上就是這個(gè)坐標(biāo)軸。
定義好外接矩形之后,我們開始調(diào)用drawArc方法開始繪制弧形,這個(gè)方法接受5個(gè)參數(shù),第二個(gè)參數(shù)是弧起始的角度,這個(gè)接受一個(gè)整數(shù)代表角度,他是這樣確定起始的角度的。以我們的手表的3點(diǎn)開始,順時(shí)針轉(zhuǎn)過我們定義的角度,這時(shí)的位置就是我們開始的位置,比如我們現(xiàn)在定義的是270,那就是手表3點(diǎn)的位置轉(zhuǎn)過270度為我們弧形開始的位置。就是12點(diǎn)的位置。然后第三個(gè)參數(shù),代表弧形劃過的角度。也是順時(shí)針。第四個(gè)參數(shù)則代表是否用扇形,我們這里不用,也就是說只是一個(gè)兩個(gè)端點(diǎn)不連接圓心的弧,第五個(gè)是我們初始化過的paint。這樣我們的自定義的弧形就出來了。
我們就可以把我們的控件放到xml中,當(dāng)成普通的view去引用了。直接看代碼:
<com.example.byhieg.circleloadingview.CircleLoadingView
android:id="@+id/loading"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true"
/>
這里的引用View一定要用全名。我們就可以直接運(yùn)行程序。
第四步——?jiǎng)悠饋?/h4>
這個(gè)弧形其實(shí)并沒有卵用,不能動(dòng)。我們接下來讓他動(dòng)起來,這里我們就用繪圖自帶的方法postInvalidate()來實(shí)現(xiàn)重繪。要讓一個(gè)圖像動(dòng)起來最簡單的辦法就是讓他在每一個(gè)坐標(biāo)點(diǎn)就繪制一下,然后讓他在每個(gè)坐標(biāo)點(diǎn)都出現(xiàn)一次就實(shí)現(xiàn)了動(dòng)畫的效果。就像小時(shí)候有那種很多頁的漫畫書,我們快速翻閱漫畫書就感覺漫畫書中的漫畫動(dòng)起來了,道理是一樣的。
那這次我們只需要在onDraw方法中,不斷修改弧形的第二個(gè)參數(shù),讓他每次不同就可以了。具體的我們看代碼:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float wdistance = (float)(wlength * 0.6);
float hdistance = (float)(hlength * 0.6);
RectF rectF = new RectF(
wlength / 2 - wdistance / 2,
hlength / 2 - hdistance / 2,
wlength / 2 + wdistance / 2,
hlength / 2 + hdistance / 2);
canvas.drawArc(
rectF,
//每次重繪,起始的角度就會(huì)多5度。
270 + 5 * i,
(float)(240),
false,
paint
);
i++;
//調(diào)用重繪,來實(shí)現(xiàn)圖像不斷繪制
postInvalidate();
}
這樣,我們?cè)俅芜\(yùn)行程序的時(shí)候,我們的弧形就動(dòng)起來。當(dāng)我們?cè)诮佑|動(dòng)畫概念的時(shí)候,又會(huì)發(fā)現(xiàn)有很多方法實(shí)現(xiàn)弧形旋轉(zhuǎn)。
第五步——完善
前面4步,我們已經(jīng)實(shí)現(xiàn)了簡單的自定義View,并且有一個(gè)可觀的效果。但是我們還要完善一下,比如我們可以在XML中指定弧形的顏色,弧形的粗細(xì)。這個(gè)時(shí)候,我們就需要在values文件下新建一個(gè)attrs.xml。在里面制定我們的自定義屬性,然后在我們的View文件中讀取這些屬性。
我們先看一下attrs.xml中,我們的代碼:
<resources>
<declare-styleable name="CircleLoadingView">
<attr name="circleColor" format="color" />
<attr name="circleStrokewidth" format="float" />
</declare-styleable>
</resources>
這里,我們就定義了2個(gè)屬性,一個(gè)是弧形的顏色,一個(gè)是弧形的粗細(xì),格式分別是color和float。然后我們?cè)趘iew中的initView代碼中,獲取那些屬性。代碼如下:
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CircleLoadingView);
circleStrokewidth = ta.getFloat(R.styleable.CircleLoadingView_circleStrokewidth, 0);
circleColor = ta.getColor(R.styleable.CircleLoadingView_circleColor, 0);
然后我們就可以在我們的xml中引用這些屬性了。
<com.example.byhieg.circleloadingview.CircleLoadingView
android:id="@+id/loading"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true"
circleview:circleColor="@android:color/holo_blue_dark"
circleview:circleStrokewidth="15"/>
這里的circleview是要在根布局的設(shè)置中聲明的 xmlns:circleview="http://schemas.android.com/apk/res-auto"
因?yàn)?,這個(gè)主要出現(xiàn)在加載過程中,所以他需要一個(gè)方法來讓他顯示和隱藏。我們?cè)赩iew的文件中,設(shè)置一個(gè)方法叫做setViewVisable
當(dāng)為true的時(shí)候,就可以顯示,當(dāng)為false就可以隱藏。這樣,這個(gè)自定義View就比較完善了。
隱藏方法代碼如下:
public void setViewVisable(boolean choose) {
if (choose) {
this.setVisibility(View.VISIBLE);
}else{
this.setVisibility(View.GONE);
}
}
PS
這個(gè)小控件的源碼我放到網(wǎng)上了,大家可以對(duì)照參考下
CircleLoadingView的源碼