前言
Android自定義View是Android初中級(jí)開(kāi)發(fā)工程師向高級(jí)工程師進(jìn)階所必須掌握的一塊內(nèi)容捉兴,其重要性不言而喻。接下來(lái)的一段時(shí)間锭吨,我會(huì)連續(xù)出幾篇跟自定義View相關(guān)的文章,從易到難,跟大家一起學(xué)習(xí)Android自定義View讶踪。本文講一個(gè)Android很簡(jiǎn)單的View——DashBoard(儀表盤(pán)),以這個(gè)例子帶大家去學(xué)習(xí)自定義View的基本繪制泊交,讓大家學(xué)會(huì)自定義View乳讥,并最終掌握。
注:本文的Demo在文章的最后
必須要掌握的幾個(gè)點(diǎn)
在開(kāi)始我們的繪制DashBoard之前廓俭,有幾個(gè)點(diǎn)是必須要掌握的云石,這些是繪制的基礎(chǔ),也是前提研乒。
Paint
自定義View的過(guò)程就是一個(gè)繪制的過(guò)程汹忠,而繪制就好像我們畫(huà)畫(huà)一樣,而畫(huà)畫(huà)就必須要會(huì)畫(huà)筆雹熬,Paint就是我們的畫(huà)筆错维。
- Paint 類(lèi)的幾個(gè)最常用的方法。具體是:
- Paint.setStyle(Style style) 設(shè)置繪制模式
- Paint.setColor(int color) 設(shè)置顏色
- Paint.setStrokeWidth(float width) 設(shè)置線(xiàn)條寬度
- Paint.setTextSize(float textSize) 設(shè)置文字大小
- Paint.setAntiAlias(boolean aa) 設(shè)置抗鋸齒開(kāi)關(guān)
這里重點(diǎn)講一下Paint.setStyle(Style style)方法橄唬,這個(gè)方法設(shè)置的是繪制的 Style 赋焕。Style 具體來(lái)說(shuō)有三種: FILL, STROKE 和 FILL_AND_STROKE 。FILL 是填充模式仰楚,STROKE 是畫(huà)線(xiàn)模式(即勾邊模式)隆判,F(xiàn)ILL_AND_STROKE 是兩種模式一并使用:既畫(huà)線(xiàn)又填充犬庇。它的默認(rèn)值是 FILL,填充模式侨嘀。只有當(dāng)Style是STROKE 和 FILL_AND_STROKE時(shí)臭挽,Paint.setStrokeWidth(float width)才有意義,你全是填充的就不涉及什么線(xiàn)條寬度了咬腕。
canvas
Paint是畫(huà)筆欢峰,可畫(huà)畫(huà)光有畫(huà)筆還不行,還必須得有畫(huà)布涨共,Canvas就是畫(huà)布纽帖。Canvas這個(gè)類(lèi)是繪制最重要的類(lèi),沒(méi)有之一举反,幾乎所有繪制的方法都出自于這個(gè)類(lèi)懊直。
坐標(biāo)系
方法先不講,先講一下坐標(biāo)系火鼻,在A(yíng)ndroid 里室囊,每個(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ǔ)上開(kāi)展的,而關(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本身沒(méi)有關(guān)系刨摩,之所以使用是為了讓我們更好寺晌、更方便地繪制View。
方法
Canvas最重要也最常用的方法都是drawXXX()方法澡刹,方法太多了呻征,我不可能一一列舉,寫(xiě)幾個(gè)最常用的罢浇,余下的請(qǐng)自行g(shù)oogle
-
drawCircle(float centerX, float centerY, float radius, Paint paint) 畫(huà)圓
基本看參數(shù)名字就能猜出來(lái)是啥意思了陆赋,前面講了坐標(biāo)系的概念沐祷,前兩個(gè)參數(shù)就是圓心的X、Y坐標(biāo)了攒岛,第三個(gè)是半徑大小赖临,最后一個(gè)是畫(huà)筆。 - drawRect(float left, float top, float right, float bottom, Paint paint) 畫(huà)矩形 (參數(shù)啥意思基本都能猜出來(lái)灾锯,不多講兢榨,不行還是google)
- drawOval(float left, float top, float right, float bottom, Paint paint) 畫(huà)橢圓
- drawLine(float startX, float startY, float stopX, float stopY, Paint paint) 畫(huà)線(xiàn)
-
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 是弧形劃過(guò)的角度;useCenter 表示是否連接到圓心君旦,如果不連接到圓心澎办,就是弧形,如果連接到圓心金砍,就是扇形局蚀。 -
drawPath(Path path, Paint paint) 畫(huà)自定義圖形
這里Path對(duì)象要講一下,Path.addXxx()——添加子圖形,例如Path.addCircle(float x, float y, float radius, Direction dir) 添加圓;Path.xxxTo() ——畫(huà)線(xiàn)(直線(xiàn)或曲線(xiàn))lineTo(float x, float y) / rLineTo(float x, float y) 畫(huà)直線(xiàn).
小結(jié)
以上就是我們開(kāi)始繪制DashBoard之前還需要掌握的基礎(chǔ)恕稠,因?yàn)槎加玫玫嚼派稹aint是畫(huà)筆,主要就是設(shè)置畫(huà)筆相關(guān)的屬性鹅巍,顏色千扶、大小、風(fēng)格等等骆捧;canvas是畫(huà)布澎羞,坐標(biāo)系的概念必須清楚,重要的幾個(gè)方法也必須知道敛苇。
DashBoard
先上個(gè)圖
看圖其實(shí)很簡(jiǎn)單妆绞,基本上就分為三步,第一份畫(huà)环闩省括饶;第二步畫(huà)刻度;第三步畫(huà)指針来涨。
畫(huà)弧線(xiàn)
畫(huà)弧線(xiàn)之前图焰,我簡(jiǎn)單講一下自定義View的流程,創(chuàng)建一個(gè)DashBoard的類(lèi)型繼承View蹦掐,重寫(xiě)構(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);//畫(huà)線(xiàn)模式
mPaint.setStrokeWidth(Utils.px2dp(2));//線(xiàn)寬度
mPaint.setColor(Color.BLACK);
}
做好以上初始化工作楞泼,我們就開(kāi)始第一步畫(huà)弧線(xiàn)驰徊。調(diào)用Canvas.drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)這個(gè)方法上面有介紹過(guò),這里就不詳細(xì)講了堕阔,前四個(gè)參數(shù)很好設(shè)置棍厂,我們定義圓心在View的中心位置,即(getWidth()/2,geHeight()/2),半徑150dp超陆,那么前四個(gè)參數(shù)就有了牺弹,第六個(gè)參數(shù)sweepAngle是劃過(guò)的度數(shù),這個(gè)我們定義為240度时呀,第七個(gè)是否連接中心张漂,false,我們不需要連接中心谨娜,最后一個(gè)放自己的Paint就行了航攒,現(xiàn)在的關(guān)鍵就是第五個(gè)參數(shù),開(kāi)始角度的計(jì)算趴梢,下面我出張圖幫助大家計(jì)算一下圖中畫(huà)得應(yīng)該比較清楚了漠畜,不過(guò)多解釋?zhuān)苯由洗a
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);
}
效果圖
畫(huà)刻度
關(guān)于畫(huà)刻度,其實(shí)就是畫(huà)線(xiàn)嗎坞靶,那畫(huà)線(xiàn)的方法拿過(guò)來(lái)看一下憔狞,Canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint) ,需要線(xiàn)的起始點(diǎn)和結(jié)束點(diǎn)的坐標(biāo),如果我要畫(huà)21個(gè)刻度彰阴,那需要21個(gè)點(diǎn)的刻度都算一遍瘾敢,我去,誰(shuí)可能這么干啊尿这。放心簇抵,我們當(dāng)然不會(huì)這么干了,下面提供兩種方式射众。
第一種方式
思路:坐標(biāo)系的旋轉(zhuǎn)+平移
之前在講Canvas里提過(guò)碟摆,每一個(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)以后雏逾,在畫(huà)右下角第一個(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)就是(半徑-刻度線(xiàn)的長(zhǎng)度)厢洞。下面我還是用一張圖來(lái)解釋一下
這樣第一個(gè)刻度線(xiàn)仇让,我們就畫(huà)出來(lái)了典奉,現(xiàn)在假定我們要畫(huà)21個(gè)刻度線(xiàn),21刻度線(xiàn)對(duì)應(yīng)20個(gè)間隔丧叽,總角度是240度卫玖,每個(gè)刻度線(xiàn)間隔角度就是240/20即12度,所以其他的刻度線(xiàn)就可以讓坐標(biāo)系每次逆時(shí)針旋轉(zhuǎn)12度畫(huà)一次踊淳,用代碼表達(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)是刻度線(xiàn)的長(zhǎng)度,為10dp
canvas.drawLine(RADIUS-Utils.px2dp(10),0,RADIUS,0,mPaint);
canvas.rotate(-SWEEPANGLE/20);//逆時(shí)針選擇 負(fù)值是逆時(shí)針
}
//最后一根線(xiàn)
canvas.drawLine(RADIUS-Utils.px2dp(10),0,RADIUS,0,mPaint);
canvas.rotate(240-30);//旋轉(zhuǎn)回去的角度
canvas.translate(-getWidth()/2,-getHeight()/2);
}
畫(huà)完以后的效果圖
這個(gè)圖我故意沒(méi)有縮放迂尝,細(xì)心的你可能已經(jīng)發(fā)現(xiàn)了第一個(gè)和最后一個(gè)刻度明顯有點(diǎn)不自然脱茉,我再放大一下
這下很明顯了吧,感覺(jué)好像是空了一半的寬度沒(méi)有畫(huà)在弧線(xiàn)上垄开。這是為什么呢琴许? 還得再上個(gè)圖
看圖我們可以知道,我們?cè)诋?huà)刻度的時(shí)候溉躲,Paint就是我們的畫(huà)筆榜田,默認(rèn)是寬度的,我們畫(huà)刻度的縱坐標(biāo)是0签财,但是實(shí)際畫(huà)的時(shí)候是把畫(huà)筆的中間位置放在0的坐標(biāo)上串慰,這就導(dǎo)致了好像空了一半Paint出來(lái),知道了什么原因其實(shí)解決起來(lái)也很簡(jiǎn)單唱蒸,就是把起始點(diǎn)和結(jié)束點(diǎn)的縱坐標(biāo)相應(yīng)的提高半個(gè)畫(huà)筆的高度邦鲫,直接看代碼吧
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);
}
看了注釋?zhuān)銜?huì)發(fā)現(xiàn)第一個(gè)點(diǎn)和最后一個(gè)點(diǎn)的提高方式不同氓拼,這也是為什么上面”相應(yīng)的“三個(gè)字我要加粗了你画,再看一眼修改后的效果圖第二種方式
思路:使用PathMeasure測(cè)量弧線(xiàn)長(zhǎng)度,利用PathDashPathEffect來(lái)畫(huà)刻度
簡(jiǎn)單講一下PathMeasure和PathDashPathEffect
- PathMeasure:用來(lái)測(cè)量路徑的長(zhǎng)度桃漾,public PathMeasure(Path path, boolean forceClosed)坏匪,通過(guò)PathMeasure.getLength()
-
PathDashPathEffect:Paint.setPathEffect(PathEffect effect)給圖形的輪廓設(shè)置效果的,PathDashPathEffect是PathEffect的一個(gè)子類(lèi)撬统,它的構(gòu)造方法 PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style) 中适滓, shape 參數(shù)是用來(lái)繪制的 Path ; advance 是兩個(gè)相鄰的 shape 段之間的間隔恋追,不過(guò)注意凭迹,這個(gè)間隔是兩個(gè) shape 段的起點(diǎn)的間隔罚屋,而不是前一個(gè)的終點(diǎn)和后一個(gè)的起點(diǎn)的距離; phase 和 DashPathEffect 中一樣嗅绸,是虛線(xiàn)的偏移脾猛;最后一個(gè)參數(shù) style,是用來(lái)指定拐彎改變的時(shí)候 shape 的轉(zhuǎn)換方式鱼鸠。style 的類(lèi)型為 PathDashPathEffect.Style 尖滚,是一個(gè) enum ,具體有三個(gè)值:TRANSLATE:位移瞧柔,ROTATE:旋轉(zhuǎn)漆弄,MORPH:變體
知道了這兩個(gè)方法,我們就可以先用PathMeasure拿到弧線(xiàn)的長(zhǎng)度造锅,除以20獲得每個(gè)間隔的長(zhǎng)度撼唾,然后通過(guò)Paint.setPathEffect(new PathDashPathEffect())方法來(lái)畫(huà)刻度就行了,直接上代碼
private void drawDegree2(Canvas canvas){
//刻度的路徑
dash=new Path();
//Path.Direction.CW順時(shí)針?lè)较?同時(shí)順時(shí)針切線(xiàn)方向?yàn)閄軸正向
dash.addRect(0,0,Utils.px2dp(2),Utils.px2dp(10), Path.Direction.CW);
//弧線(xiàn)長(zhǎng)度的路徑
Path length=new Path();
length.addArc(rectF,90+(360-SWEEPANGLE)/2,SWEEPANGLE);
//測(cè)量弧線(xiàn)長(zhǎng)度
pathMeasure=new PathMeasure(length,false);
//這里(pathMeasure.getLength()-mPaint.getStrokeWidth())/20 弧線(xiàn)長(zhǎng)度之所以減去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è)人還是更加推薦第一種的畫(huà)刻度方式糙箍。
畫(huà)指針
畫(huà)指針呢渤愁,就比較簡(jiǎn)單了,其實(shí)就是調(diào)用畫(huà)線(xiàn)的方法深夯,先把坐標(biāo)系平移動(dòng)原點(diǎn)位置抖格,設(shè)置一個(gè)當(dāng)前的角度currentAngle還有指針長(zhǎng)度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
很簡(jiǎn)單雹拄,上代碼
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)容,本文主要通過(guò)繪制DashBoard來(lái)講基本的繪制掌呜,Paint和Canvas的基本用法滓玖,接下來(lái)的一段時(shí)間內(nèi),我會(huì)繼續(xù)出自定義View相關(guān)的內(nèi)容质蕉,下一篇文章會(huì)講繪制文字势篡。
最后放上文章的demo DashBoard,覺(jué)得還不錯(cuò)的請(qǐng)給個(gè)star哈