前言
Android自定義View是Android初中級(jí)開發(fā)工程師向高級(jí)工程師進(jìn)階所必須掌握的一塊內(nèi)容,其重要性不言而喻。接下來的一段時(shí)間,我會(huì)連續(xù)出幾篇跟自定義View相關(guān)的文章,從易到難,跟大家一起學(xué)習(xí)Android自定義View。本文講一個(gè)Android很簡單的View——DashBoard(儀表盤),以這個(gè)例子帶大家去學(xué)習(xí)自定義View的基本繪制,讓大家學(xué)會(huì)自定義View,并最終掌握。
注:本文的Demo在文章的最后
必須要掌握的幾個(gè)點(diǎn)
在開始我們的繪制DashBoard之前,有幾個(gè)點(diǎn)是必須要掌握的,這些是繪制的基礎(chǔ),也是前提。
Paint
自定義View的過程就是一個(gè)繪制的過程,而繪制就好像我們畫畫一樣,而畫畫就必須要會(huì)畫筆,Paint就是我們的畫筆。
- Paint 類的幾個(gè)最常用的方法。具體是:
- Paint.setStyle(Style style) 設(shè)置繪制模式
- Paint.setColor(int color) 設(shè)置顏色
- Paint.setStrokeWidth(float width) 設(shè)置線條寬度
- Paint.setTextSize(float textSize) 設(shè)置文字大小
- Paint.setAntiAlias(boolean aa) 設(shè)置抗鋸齒開關(guān)
這里重點(diǎn)講一下Paint.setStyle(Style style)方法,這個(gè)方法設(shè)置的是繪制的 Style 。Style 具體來說有三種: FILL, STROKE 和 FILL_AND_STROKE 。FILL 是填充模式,STROKE 是畫線模式(即勾邊模式),F(xiàn)ILL_AND_STROKE 是兩種模式一并使用:既畫線又填充。它的默認(rèn)值是 FILL,填充模式。只有當(dāng)Style是STROKE 和 FILL_AND_STROKE時(shí),Paint.setStrokeWidth(float width)才有意義,你全是填充的就不涉及什么線條寬度了。
canvas
Paint是畫筆,可畫畫光有畫筆還不行,還必須得有畫布,Canvas就是畫布。Canvas這個(gè)類是繪制最重要的類,沒有之一,幾乎所有繪制的方法都出自于這個(gè)類。
坐標(biāo)系
方法先不講,先講一下坐標(biāo)系,在Android 里,每個(gè)View 都有一個(gè)自己的坐標(biāo)系,彼此之間是不影響的。這個(gè)坐標(biāo)系的原點(diǎn)是 View 左上角的那個(gè)點(diǎn);水平方向是 x 軸,右正左負(fù);豎直方向是 y 軸,下正上負(fù)(注意,是下正上負(fù),不是上正下負(fù),和上學(xué)時(shí)候?qū)W的坐標(biāo)系方向不一樣也就是下面這個(gè)樣子。

這個(gè)坐標(biāo)非常重要,因?yàn)槲覀兯械睦L制都是在這個(gè)坐標(biāo)系的基礎(chǔ)上開展的,而關(guān)于坐標(biāo)系還有這么個(gè)兩個(gè)方法要特別注意:
- Canvas.rotate(float degrees)//旋轉(zhuǎn)坐標(biāo)系,正角度順時(shí)針,負(fù)角度逆時(shí)針
-
Canvas.translate(float dx, float dy)
注意:以上兩個(gè)方法的操作的對(duì)象是坐標(biāo)系,跟View本身沒有關(guān)系,之所以使用是為了讓我們更好、更方便地繪制View。
方法
Canvas最重要也最常用的方法都是drawXXX()方法,方法太多了,我不可能一一列舉,寫幾個(gè)最常用的,余下的請(qǐng)自行g(shù)oogle
-
drawCircle(float centerX, float centerY, float radius, Paint paint) 畫圓
基本看參數(shù)名字就能猜出來是啥意思了,前面講了坐標(biāo)系的概念,前兩個(gè)參數(shù)就是圓心的X、Y坐標(biāo)了,第三個(gè)是半徑大小,最后一個(gè)是畫筆。 - drawRect(float left, float top, float right, float bottom, Paint paint) 畫矩形 (參數(shù)啥意思基本都能猜出來,不多講,不行還是google)
- drawOval(float left, float top, float right, float bottom, Paint paint) 畫橢圓
- drawLine(float startX, float startY, float stopX, float stopY, Paint paint) 畫線
-
drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint) 繪制弧形或扇形
left, top, right, bottom 描述的是這個(gè)弧形所在的橢圓;startAngle 是弧形的起始角度(x 軸的正向,即正右的方向,是 0 度的位置;順時(shí)針為正角度,逆時(shí)針為負(fù)角度),sweepAngle 是弧形劃過的角度;useCenter 表示是否連接到圓心,如果不連接到圓心,就是弧形,如果連接到圓心,就是扇形。 -
drawPath(Path path, Paint paint) 畫自定義圖形
這里Path對(duì)象要講一下,Path.addXxx()——添加子圖形,例如Path.addCircle(float x, float y, float radius, Direction dir) 添加圓;Path.xxxTo() ——畫線(直線或曲線)lineTo(float x, float y) / rLineTo(float x, float y) 畫直線.
小結(jié)
以上就是我們開始繪制DashBoard之前還需要掌握的基礎(chǔ),因?yàn)槎加玫玫?。Paint是畫筆,主要就是設(shè)置畫筆相關(guān)的屬性,顏色、大小、風(fēng)格等等;canvas是畫布,坐標(biāo)系的概念必須清楚,重要的幾個(gè)方法也必須知道。
DashBoard
先上個(gè)圖

看圖其實(shí)很簡單,基本上就分為三步,第一份畫?。坏诙疆嬁潭?;第三步畫指針。
畫弧線
畫弧線之前,我簡單講一下自定義View的流程,創(chuàng)建一個(gè)DashBoard的類型繼承View,重寫構(gòu)造方法和onDraw(Canvas canvas)
public class DashBoard extends View {
public DashBoard(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initPaint();//初始化Paint
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
初始化Paint
private void initPaint(){
mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);//抗鋸齒
mPaint.setStyle(Paint.Style.STROKE);//畫線模式
mPaint.setStrokeWidth(Utils.px2dp(2));//線寬度
mPaint.setColor(Color.BLACK);
}
做好以上初始化工作,我們就開始第一步畫弧線。調(diào)用Canvas.drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)這個(gè)方法上面有介紹過,這里就不詳細(xì)講了,前四個(gè)參數(shù)很好設(shè)置,我們定義圓心在View的中心位置,即(getWidth()/2,geHeight()/2),半徑150dp,那么前四個(gè)參數(shù)就有了,第六個(gè)參數(shù)sweepAngle是劃過的度數(shù),這個(gè)我們定義為240度,第七個(gè)是否連接中心,false,我們不需要連接中心,最后一個(gè)放自己的Paint就行了,現(xiàn)在的關(guān)鍵就是第五個(gè)參數(shù),開始角度的計(jì)算,下面我出張圖幫助大家計(jì)算一下
圖中畫得應(yīng)該比較清楚了,不過多解釋,直接上代碼
private void drawArc(Canvas canvas){
rectF = new RectF(getWidth() / 2 - RADIUS, getHeight() / 2 - RADIUS,
getWidth() / 2 + RADIUS, getHeight() / 2 + RADIUS);
canvas.drawArc(rectF,90+(360-SWEEPANGLE)/2,SWEEPANGLE,false,mPaint);
}
效果圖

畫刻度
關(guān)于畫刻度,其實(shí)就是畫線嗎,那畫線的方法拿過來看一下,Canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint) ,需要線的起始點(diǎn)和結(jié)束點(diǎn)的坐標(biāo),如果我要畫21個(gè)刻度,那需要21個(gè)點(diǎn)的刻度都算一遍,我去,誰可能這么干啊。放心,我們當(dāng)然不會(huì)這么干了,下面提供兩種方式。
第一種方式
思路:坐標(biāo)系的旋轉(zhuǎn)+平移
之前在講Canvas里提過,每一個(gè)Android的View都對(duì)應(yīng)著有一個(gè)坐標(biāo)系,坐標(biāo)原點(diǎn)在View的左上角(可以看一下上面的那張圖),現(xiàn)在如果我們把坐標(biāo)原點(diǎn)平移到圓心的位置,并且再順時(shí)針旋轉(zhuǎn)30°,那么當(dāng)前的坐標(biāo)系就是下面這樣的

那么在我們平移旋轉(zhuǎn)以后,在畫右下角第一個(gè)刻度的時(shí)候,就相當(dāng)于這個(gè)刻度的起始點(diǎn)和結(jié)束點(diǎn)的縱坐標(biāo)都是0,因?yàn)槲覀儼言c(diǎn)移動(dòng)到了圓心,而結(jié)束點(diǎn)的橫坐標(biāo)就是圓的半徑(RADIUS),而起始點(diǎn)的橫坐標(biāo)就是(半徑-刻度線的長度)。下面我還是用一張圖來解釋一下

這樣第一個(gè)刻度線,我們就畫出來了,現(xiàn)在假定我們要畫21個(gè)刻度線,21刻度線對(duì)應(yīng)20個(gè)間隔,總角度是240度,每個(gè)刻度線間隔角度就是240/20即12度,所以其他的刻度線就可以讓坐標(biāo)系每次逆時(shí)針旋轉(zhuǎn)12度畫一次,用代碼表達(dá)一下會(huì)更清晰。
private void drawDegree(Canvas canvas){
canvas.translate(getWidth()/2,getHeight()/2);
canvas.rotate(30);
for (int i=0;i<20;i++){
//Utils.px2dp(10)是刻度線的長度,為10dp
canvas.drawLine(RADIUS-Utils.px2dp(10),0,RADIUS,0,mPaint);
canvas.rotate(-SWEEPANGLE/20);//逆時(shí)針選擇 負(fù)值是逆時(shí)針
}
//最后一根線
canvas.drawLine(RADIUS-Utils.px2dp(10),0,RADIUS,0,mPaint);
canvas.rotate(240-30);//旋轉(zhuǎn)回去的角度
canvas.translate(-getWidth()/2,-getHeight()/2);
}
畫完以后的效果圖

這個(gè)圖我故意沒有縮放,細(xì)心的你可能已經(jīng)發(fā)現(xiàn)了第一個(gè)和最后一個(gè)刻度明顯有點(diǎn)不自然,我再放大一下

這下很明顯了吧,感覺好像是空了一半的寬度沒有畫在弧線上。這是為什么呢? 還得再上個(gè)圖

看圖我們可以知道,我們?cè)诋嬁潭鹊臅r(shí)候,Paint就是我們的畫筆,默認(rèn)是寬度的,我們畫刻度的縱坐標(biāo)是0,但是實(shí)際畫的時(shí)候是把畫筆的中間位置放在0的坐標(biāo)上,這就導(dǎo)致了好像空了一半Paint出來,知道了什么原因其實(shí)解決起來也很簡單,就是把起始點(diǎn)和結(jié)束點(diǎn)的縱坐標(biāo)相應(yīng)的提高半個(gè)畫筆的高度,直接看代碼吧
private void drawDegree(Canvas canvas){
canvas.translate(getWidth()/2,getHeight()/2);
canvas.rotate(30);
for (int i=0;i<20;i++){
//縱坐標(biāo)下正上負(fù),向上提高,加負(fù)值,即-mPaint.getStrokeWidth()/2
canvas.drawLine(RADIUS-Utils.px2dp(10),-mPaint.getStrokeWidth()/2,RADIUS,-mPaint.getStrokeWidth()/2,mPaint);
canvas.rotate(-SWEEPANGLE/20);
}
//最后一個(gè)點(diǎn),因坐標(biāo)系已經(jīng)旋轉(zhuǎn)了240度,向上提高,加整值,即mPaint.getStrokeWidth()/2
canvas.drawLine(RADIUS-Utils.px2dp(10),mPaint.getStrokeWidth()/2,RADIUS,mPaint.getStrokeWidth()/2,mPaint);
canvas.rotate(240-30);//旋轉(zhuǎn)回去的角度
canvas.translate(-getWidth()/2,-getHeight()/2);
}
看了注釋,你會(huì)發(fā)現(xiàn)第一個(gè)點(diǎn)和最后一個(gè)點(diǎn)的提高方式不同,這也是為什么上面”相應(yīng)的“三個(gè)字我要加粗了,再看一眼修改后的效果圖
第二種方式
思路:使用PathMeasure測(cè)量弧線長度,利用PathDashPathEffect來畫刻度
簡單講一下PathMeasure和PathDashPathEffect
- PathMeasure:用來測(cè)量路徑的長度,public PathMeasure(Path path, boolean forceClosed),通過PathMeasure.getLength()
-
PathDashPathEffect:Paint.setPathEffect(PathEffect effect)給圖形的輪廓設(shè)置效果的,PathDashPathEffect是PathEffect的一個(gè)子類,它的構(gòu)造方法 PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style) 中, shape 參數(shù)是用來繪制的 Path ; advance 是兩個(gè)相鄰的 shape 段之間的間隔,不過注意,這個(gè)間隔是兩個(gè) shape 段的起點(diǎn)的間隔,而不是前一個(gè)的終點(diǎn)和后一個(gè)的起點(diǎn)的距離; phase 和 DashPathEffect 中一樣,是虛線的偏移;最后一個(gè)參數(shù) style,是用來指定拐彎改變的時(shí)候 shape 的轉(zhuǎn)換方式。style 的類型為 PathDashPathEffect.Style ,是一個(gè) enum ,具體有三個(gè)值:TRANSLATE:位移,ROTATE:旋轉(zhuǎn),MORPH:變體
知道了這兩個(gè)方法,我們就可以先用PathMeasure拿到弧線的長度,除以20獲得每個(gè)間隔的長度,然后通過Paint.setPathEffect(new PathDashPathEffect())方法來畫刻度就行了,直接上代碼
private void drawDegree2(Canvas canvas){
//刻度的路徑
dash=new Path();
//Path.Direction.CW順時(shí)針方向 同時(shí)順時(shí)針切線方向?yàn)閄軸正向
dash.addRect(0,0,Utils.px2dp(2),Utils.px2dp(10), Path.Direction.CW);
//弧線長度的路徑
Path length=new Path();
length.addArc(rectF,90+(360-SWEEPANGLE)/2,SWEEPANGLE);
//測(cè)量弧線長度
pathMeasure=new PathMeasure(length,false);
//這里(pathMeasure.getLength()-mPaint.getStrokeWidth())/20 弧線長度之所以減去Paint的寬度跟我第一種方式去掉寬度是一個(gè)意思
mPaint.setPathEffect(new PathDashPathEffect(dash,
(pathMeasure.getLength()-mPaint.getStrokeWidth())/20,0, PathDashPathEffect.Style.ROTATE));
canvas.drawArc(rectF,90+(360-SWEEPANGLE)/2,SWEEPANGLE,false,mPaint);
mPaint.setPathEffect(null);
}
這里我就不細(xì)講了,注釋還是比較清楚,效果圖跟第一種方式是一樣的就不貼圖,個(gè)人還是更加推薦第一種的畫刻度方式。
畫指針
畫指針呢,就比較簡單了,其實(shí)就是調(diào)用畫線的方法,先把坐標(biāo)系平移動(dòng)原點(diǎn)位置,設(shè)置一個(gè)當(dāng)前的角度currentAngle還有指針長度INDICATOR,唯一有一點(diǎn)難度的就是計(jì)算結(jié)束點(diǎn)的橫縱坐標(biāo),需要用到三角函數(shù)的知識(shí)
- 橫坐標(biāo):Math.cos(Math.toRadians(currentAngle))*INDICATOR
- 縱坐標(biāo):Math.sin(Math.toRadians(currentAngle))*INDICATOR
很簡單,上代碼
private void drawIndicator(Canvas canvas){
canvas.translate(getWidth()/2,getHeight()/2);
canvas.drawLine(0,0,
(float) Math.cos(Math.toRadians(currentAngle))*INDICATOR,
(float)Math.sin(Math.toRadians(currentAngle))*INDICATOR,
mPaint);
canvas.translate(getWidth()/2,getHeight()/2);
}
最后
Android自定義View是Android比較難的一塊內(nèi)容,本文主要通過繪制DashBoard來講基本的繪制,Paint和Canvas的基本用法,接下來的一段時(shí)間內(nèi),我會(huì)繼續(xù)出自定義View相關(guān)的內(nèi)容,下一篇文章會(huì)講繪制文字。
最后放上文章的demo DashBoard,覺得還不錯(cuò)的請(qǐng)給個(gè)star哈