原文地址:http://makerchen.com/2016/05/29/android-alibaba/
廢話不多說,先看下效果:

該效果一眼看上去比較簡單,但其涉及的知識(shí)點(diǎn)還是挺多的。尤其是需要讀者對(duì)android.graphics包下的API有一定的了解。
先對(duì)涉及到的知識(shí)點(diǎn)羅列如下,還不是很了解的讀者可以先自行百度做個(gè)簡單的涉獵,對(duì)后續(xù)文章的理解會(huì)有很大幫助。
- Paint、Canvas這兩個(gè)基礎(chǔ)的類必須熟悉。
- 用作渲染的Shader類及其子類,以及后文中使用的是SweepGradient梯度渲染,用作漸變圓環(huán),需要了解。
- canvas.save() & canvas.restore() 的作用與關(guān)系。
- 由Paint引申的PathEffect、PorterDuffXfermode,已經(jīng)Matrix等類要有個(gè)基本的概念。
- 圖層繪制的一些概念。
- 臟矩形技術(shù)。
如果你已經(jīng)基本了解了上面涉及到的知識(shí)點(diǎn)。
OK。那接下來我們就一步一步實(shí)現(xiàn)這個(gè)效果。
1.環(huán)形漸變
或許大家都有印象,在ApiDemos中提供過一個(gè)例子仿照PS做的取色器效果。有興趣的讀者可以具體查看ApiDemos下的ColorPickerDialog的實(shí)現(xiàn)。這里我們參考他的寫法,就可以做出一個(gè)簡單的環(huán)形漸變了。
當(dāng)然ColorPickerDialog中的核心代碼也正是使用了剛才所提及的SweepGradient類用作渲染。該類屬于Shader的子類,當(dāng)然其兄弟類還有BitmapShader位圖渲染、LinearGradient線性渲染、RadialGradient環(huán)形渲染、SweepGradient梯度渲染以及ComposeShader組合渲染。網(wǎng)上有一大堆關(guān)于他們的介紹,可以做出很多很棒的效果。此處不展開。
核心代碼如下:
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);// 漸變色環(huán)畫筆,抗鋸齒
private final int[] mColors = new int[] { 0xffff0000, 0xffffff00, 0xff00ff00,
0xff00ffff,0xff0000ff,0xffff00ff };// 漸變色環(huán)顏色
Shader s = new SweepGradient(0, 0, mColors, null);
mPaint.setShader(s);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(40);
float r = CENTER_X - mPaint.getStrokeWidth() * 0.5f;
canvas.save() ;
canvas.translate(CENTER_X, CENTER_X);// 移動(dòng)中心
canvas.rotate(150);
canvas.drawOval(new RectF(-r, -r, r, r), mPaint);// 畫出色環(huán)和中心園
canvas.restore();
效果如圖1所示:

代碼講解:
從參考效果圖上看,顏色是有紅色漸變(并非線性漸變,這里我們先按照簡單的實(shí)現(xiàn))為綠色,而且效果并非為一個(gè)整圓。為了計(jì)算方便,我們假設(shè)該圓環(huán)的角度為240度。
如圖2所示

我們已知SweepGradient是一個(gè)360度均勻分布的漸變,我們一共設(shè)置了6個(gè)漸變色:從紅色(ff0000)到紫色(ff00ff),使其均勻分布在圓環(huán)上。
而繪制圓的時(shí)候,我們先將canvas的原點(diǎn)(在android2D圖形系統(tǒng)中其坐標(biāo)系原點(diǎn)在視圖左上角)通過
canvas.translate()平移至了圓環(huán)的中心點(diǎn)。在此我們使用canvas.rotate()旋轉(zhuǎn)操作,旋轉(zhuǎn)150度,使其紅色漸變的開始位置處于圖片左下方(此處正確的理解應(yīng)該是這樣:由于我們對(duì)畫布旋轉(zhuǎn)了150度,所以我們?cè)诶L制完圓環(huán)之后,通過restore()方法又使得畫布回歸到原來位置,從而達(dá)到了將紅色漸變位于左下方的目的)。調(diào)整完canvas之后,我們通過canvas.drawOval()將圓繪制上去。最后將畫布回歸到原來的位置。此處還使用了
canvas.save()與canvas.restore()組合操作。簡單介紹一下:由于此處我們對(duì)畫布有平移、旋轉(zhuǎn)操作。為了不造成對(duì)后續(xù)繪制的影響,使其復(fù)雜度增加。我們使用save()和restore()的組合來使得畫布回歸到它原來的位置。此舉有時(shí)候會(huì)對(duì)性能產(chǎn)生一定的影響,本文只是step by step的實(shí)現(xiàn)教程,而且此效果并不會(huì)強(qiáng)依賴于性能,所以性能在此處先放一邊。文末我會(huì)注明可以優(yōu)化的點(diǎn),供大家思考、討論。在這里調(diào)用完restore()的表象就是canvas的原點(diǎn)又回到了視圖的左上角。關(guān)于具體對(duì)
canvas.save()和canvas.restore()的解釋,網(wǎng)上有一大堆。這里不詳細(xì)展開。大致可以理解為save()為保存當(dāng)前canvas狀態(tài),restore則為恢復(fù)上一次save()的狀態(tài)。
2.繪制內(nèi)圓
核心代碼如下
paintMiddleCircle.setColor(Color.GRAY);
paintInnerCircle.setColor(Color.GRAY);
paintMiddleCircle.setStrokeWidth(4);
paintInnerCircle.setStrokeWidth(4);
paintMiddleCircle.setStyle(Paint.Style.STROKE);
paintInnerCircle.setStyle(Paint.Style.STROKE);
PathEffect effects = new DashPathEffect(new float[]{5,5,5,5},1);
paintInnerCircle.setPathEffect(effects);
canvas.save() ;
canvas.translate(CENTER_X, CENTER_X);
canvas.drawCircle(0, 0, CENTER_X * 5 / 8, paintInnerCircle);
canvas.drawCircle(0, 0, CENTER_X * 3 / 4, paintMiddleCircle);
canvas.restore();
效果如圖3所示

代碼講解:
該功能比較簡單。
在此處需要了解PathEffect及其子類的作用,這里我們使用DashPathEffect繪制虛線。
細(xì)心的讀者還可以發(fā)現(xiàn),我們使用的繪制圓形的方法不一樣。前面使用的是drawOval繪制橢圓,而在此處使用的是drawCircle直接畫圓,效果都一樣。具體區(qū)別可以自己體會(huì),一個(gè)是框死了畫內(nèi)切橢圓,另一個(gè)是直接畫圓。
3.繪制輔助線
核心代碼如下
paintGap1.setColor(Color.WHITE);
paintGap2.setColor(Color.WHITE);
paintGap1.setStrokeWidth(2);
paintGap2.setStrokeWidth(4);
int a = (int) (2 * CENTER_X - mPaint.getStrokeWidth());
for ( int i=0;i<=60; i++) {
canvas.save() ;
canvas.rotate(-(-30 + 4 * i), CENTER_X, CENTER_X);
if ( i % 10 == 0 ) {
canvas.drawLine( a ,CENTER_X, 2 * CENTER_X, CENTER_X, paintGap2);
} else {
canvas.drawLine( a ,CENTER_X, 2 * CENTER_X, CENTER_X, paintGap1);
}
canvas.restore();
}
效果如圖4所示

代碼講解
在上面,我們?cè)僭O(shè)了圓弧的角度為240度。便于計(jì)算,我們將該圓弧劃分為6個(gè)區(qū),每個(gè)區(qū)占40度,每個(gè)區(qū)有10個(gè)小間隔,每個(gè)小間隔的角度就是4度。由于圓弧有30度是在水平線以下的,所以我們的循環(huán)規(guī)則是上述代碼。canvas.rotate(-(-30 + 4 * i), CENTER_X, CENTER_X);此處由于CENTER_X==CENTER_Y==r,將上述代碼修改為canvas.rotate(-(-30 + 4 * i), CENTER_X, CENTER_Y);或許更容易理解。rotate中參數(shù)>0為順時(shí)針旋轉(zhuǎn),<0為逆時(shí)針旋轉(zhuǎn)。
4.圓環(huán)變圓弧
到目前為止,我們畫的還只是個(gè)漸變圓環(huán),與效果圓弧還有些不同。下面我們將圓環(huán)處理為圓弧。
** 核心代碼如下 **
width = MeasureSpec.getSize(widthMeasureSpec);
height = (int) ( ( Math.tan(Math.PI / 6) + 1 ) * width / 2 ) ;
Path path = new Path();
path.moveTo(CENTER_X, CENTER_X);
path.lineTo(0, height);
path.lineTo(width, height);
path.lineTo(CENTER_X, CENTER_X);
path.close();
canvas.drawPath(path, paintBg);
效果圖5如下

** 代碼講解:**
首先我們需要調(diào)整視圖的高度。在這之前我們都是令
width==height,保證繪制出一個(gè)整圓。現(xiàn)在根據(jù)我們的假設(shè)圓弧度數(shù)240度,其在水平線以下為30度,即PI/6。由數(shù)學(xué)公式計(jì)算得知,其視圖高度為 height = r * tan(PI/6) + r。這還不夠,調(diào)整完視圖的高度,我們需要將一些雜線,從視圖中除去,讓其看上去更像是個(gè)圓弧。
如圖6所示未去雜線的時(shí)候

我們利用圖層互相遮罩的原理。以圓心和視圖的兩個(gè)頂點(diǎn),連接成一個(gè)三角形,可以達(dá)到掩蓋其與雜線的目的。也就是后面代碼的作用。
記住在onDraw時(shí)候的一個(gè)原則:先畫的在畫布下方,后畫的在畫布上方,后畫的會(huì)覆蓋先畫的。從而達(dá)到圖5的效果。
5.文字的繪制
** 核心代碼如下**
private static final String[] text = {"950","極好","700","優(yōu)秀","650","良好","600","中等","550","較差","350","很差","150"};
for ( int i=0;i<=12;i++) {
canvas.save();
canvas.rotate(-(-120 + 20 * i ), CENTER_X, CENTER_X);
canvas.drawText(text[i],CENTER_X - 20 ,CENTER_X * 3 / 16,paintText);
canvas.restore();
}
效果圖7如下

** 代碼講解 **
我們已知每個(gè)區(qū)為40度。從參考效果圖上可以看出每隔20度就會(huì)有一段文字。我們知道在繪制文字的時(shí)候,都是從左往右寫的。所以我們?cè)谛D(zhuǎn)畫布的時(shí)候,起始點(diǎn)需要在原來的基礎(chǔ)上再加90度,即逆時(shí)針旋轉(zhuǎn)120度,然后繪入文字。當(dāng)然這段繪制的過程需要在繪制三角形之后,否則部分文字會(huì)被三角形的遮罩遮蓋起來。
6.最后的動(dòng)效
if ( isSetReferValue ) {
float r1 = CENTER_X * 6 / 8 ;
canvas.save();
canvas.translate(CENTER_X, CENTER_X);
canvas.drawArc(new RectF(-r1, -r1, r1, r1), -210, currentRotateAngle, false, paintMiddleArc);
canvas.rotate( - 30 + currentRotateAngle );
Matrix matrix = new Matrix();
matrix.preTranslate(-r1 - bitmapWidth * 3/ 8,-bitmapHeight/2);
canvas.drawBitmap(bitmapLocation,matrix,paintBitmap);
canvas.restore();
}
public void setReferValue ( int referValue ,final RotateListener rotateListener) {
isSetReferValue = !isSetReferValue ;
if ( referValue <= 150 ) {
totalRotateAngle = 0f ;
} else if ( referValue <= 550 ) {
totalRotateAngle = ( referValue - 150 ) * 80 / 400f ;
} else if ( referValue <= 700 ) {
totalRotateAngle = ( referValue - 550 ) * 120 / 150f + 80 ;
} else if ( referValue <= 950 ) {
totalRotateAngle = ( referValue - 700 ) * 40 / 250f + 200;
} else {
totalRotateAngle = 210f ;
}
rotateAngle = totalRotateAngle / 60 ;
new Thread(new Runnable() {
@Override
public void run() {
boolean rotating = true ;
float value = 350;
while (rotating) {
try {
Thread.sleep(16);
} catch (InterruptedException e) {
e.printStackTrace();
}
currentRotateAngle += rotateAngle;
if ( currentRotateAngle >= totalRotateAngle ) {
currentRotateAngle = totalRotateAngle;
rotating = false;
}
if ( null != rotateListener) {
if ( currentRotateAngle <= 80 ) {
value = 350 + ( currentRotateAngle / 80 ) * 400 ;
} else if ( currentRotateAngle <= 200 ) {
value = 550 + ( ( currentRotateAngle - 80 )/ 120 ) * 150 ;
} else {
value = 700 + ( ( currentRotateAngle - 200 ) / 40 ) * 250 ;
}
rotateListener.rotate(currentRotateAngle,value);
}
postInvalidate();
}
}
}).start();
}
效果圖8如下

代碼講解
繪制的代碼中。首先我們要了解到繪制圓弧的方法為canvas.drawArc(),此處我們要從左下角開始繪制圓弧,所以我們的起始旋轉(zhuǎn)角度為-210度。
由于我們此處的原點(diǎn)在圓心。圖片要跟隨著已知的旋轉(zhuǎn)角度進(jìn)行旋轉(zhuǎn)。我們知道針對(duì)canvas.rotate()方法,當(dāng)旋轉(zhuǎn)角度>0的時(shí)候,是順時(shí)針旋轉(zhuǎn);<0為逆時(shí)針旋轉(zhuǎn)。由于此處我們圖片的箭頭朝向向右,為了保證圖片的朝向指向圓心。我們旋轉(zhuǎn)的規(guī)則為- 30 + currentRotateAngle,保證每一次在繪制圖形的時(shí)候,都是在(x,y)為(-r1 - bitmapWidth * 3/ 8,-bitmapHeight/2)這個(gè)位置的時(shí)候繪制。最后恢復(fù)canvas。
關(guān)于在計(jì)算totalRotateAngle、currentRotateAngle以及 value的時(shí)候,都是些簡單的算法。夾雜著很多硬編碼,耐心點(diǎn)應(yīng)該可以讀懂,不做過多解釋。
實(shí)現(xiàn)的七七八八,大致思路應(yīng)該是這樣。
一些問題
- 在上文也提到了,參考的效果圖,并非是一個(gè)平滑的漸變。仔細(xì)觀察的話,在600處有處瞬斷的跡象。
解決思路:利用上面講到過的PorterDuffXfermode,將兩段不同的環(huán)形漸變,拼接而成。到達(dá)此效果。 - 關(guān)于優(yōu)化
- onDraw()方法中,canvas.save()與canvas.restore()方法多次使用,造成不必要的性能浪費(fèi)。
- 在執(zhí)行箭頭轉(zhuǎn)動(dòng)效果的時(shí)候,不需要在canvas上每次全部都重新繪制。只需要繪制需要繪制的部分區(qū)域即可,即臟矩形。在這里也就是箭頭所滾動(dòng)范圍內(nèi)的部分區(qū)域圓環(huán)。讀者可以自行實(shí)現(xiàn)。
- 關(guān)于多線程
細(xì)心的人可以發(fā)現(xiàn)方法setReferValue(),并沒有考慮多線程的情況。此處只是demo,場景也有限。沒做特殊處理。有興趣的讀者可以自行實(shí)現(xiàn)。
后記
之前一直沒有記錄博客的習(xí)慣?,F(xiàn)在寫完兩篇,發(fā)現(xiàn)將代碼翻譯成文不是一件容易的事。代碼在周三就基本完成了,文章也是一直拖著到現(xiàn)在才整理出來發(fā)布。要將每一個(gè)知識(shí)點(diǎn),能夠簡單的表述出來,是比較難的一件事情。落筆成文同面對(duì)面與人講述,會(huì)不太一樣。以后要多加強(qiáng)這方面的練習(xí)。也希望讀者們能夠一起來嘗試記錄。遺留的問題,都不是很難,讀者可以自行嘗試的去實(shí)現(xiàn)。今天腦子有點(diǎn)疼,就寫到此了。
源代碼在此下載:http://pan.baidu.com/s/1kTKUowJ
enjoy it!
想及時(shí)了解最新信息。掃一掃,添加關(guān)注微信公眾號(hào)
