原文地址:http://makerchen.com/2016/05/29/android-alibaba/
廢話不多說,先看下效果:
該效果一眼看上去比較簡單,但其涉及的知識點還是挺多的。尤其是需要讀者對android.graphics包下的API有一定的了解。
先對涉及到的知識點羅列如下,還不是很了解的讀者可以先自行百度做個簡單的涉獵,對后續文章的理解會有很大幫助。
- Paint、Canvas這兩個基礎的類必須熟悉。
- 用作渲染的Shader類及其子類,以及后文中使用的是SweepGradient梯度渲染,用作漸變圓環,需要了解。
- canvas.save() & canvas.restore() 的作用與關系。
- 由Paint引申的PathEffect、PorterDuffXfermode,已經Matrix等類要有個基本的概念。
- 圖層繪制的一些概念。
- 臟矩形技術。
如果你已經基本了解了上面涉及到的知識點。
OK。那接下來我們就一步一步實現這個效果。
1.環形漸變
或許大家都有印象,在ApiDemos中提供過一個例子仿照PS做的取色器效果。有興趣的讀者可以具體查看ApiDemos下的ColorPickerDialog的實現。這里我們參考他的寫法,就可以做出一個簡單的環形漸變了。
當然ColorPickerDialog中的核心代碼也正是使用了剛才所提及的SweepGradient類用作渲染。該類屬于Shader的子類,當然其兄弟類還有BitmapShader位圖渲染、LinearGradient線性渲染、RadialGradient環形渲染、SweepGradient梯度渲染以及ComposeShader組合渲染。網上有一大堆關于他們的介紹,可以做出很多很棒的效果。此處不展開。
核心代碼如下:
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);// 漸變色環畫筆,抗鋸齒
private final int[] mColors = new int[] { 0xffff0000, 0xffffff00, 0xff00ff00,
0xff00ffff,0xff0000ff,0xffff00ff };// 漸變色環顏色
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);// 移動中心
canvas.rotate(150);
canvas.drawOval(new RectF(-r, -r, r, r), mPaint);// 畫出色環和中心園
canvas.restore();
效果如圖1所示:
代碼講解:
從參考效果圖上看,顏色是有紅色漸變(并非線性漸變,這里我們先按照簡單的實現)為綠色,而且效果并非為一個整圓。為了計算方便,我們假設該圓環的角度為240度。
如圖2所示
我們已知SweepGradient是一個360度均勻分布的漸變,我們一共設置了6個漸變色:從紅色(ff0000)到紫色(ff00ff),使其均勻分布在圓環上。
而繪制圓的時候,我們先將canvas的原點(在android2D圖形系統中其坐標系原點在視圖左上角)通過
canvas.translate()
平移至了圓環的中心點。在此我們使用canvas.rotate()
旋轉操作,旋轉150度,使其紅色漸變的開始位置處于圖片左下方(此處正確的理解應該是這樣:由于我們對畫布旋轉了150度,所以我們在繪制完圓環之后,通過restore()方法又使得畫布回歸到原來位置,從而達到了將紅色漸變位于左下方的目的)。調整完canvas之后,我們通過canvas.drawOval()
將圓繪制上去。最后將畫布回歸到原來的位置。此處還使用了
canvas.save()
與canvas.restore()
組合操作。簡單介紹一下:由于此處我們對畫布有平移、旋轉操作。為了不造成對后續繪制的影響,使其復雜度增加。我們使用save()和restore()的組合來使得畫布回歸到它原來的位置。此舉有時候會對性能產生一定的影響,本文只是step by step的實現教程,而且此效果并不會強依賴于性能,所以性能在此處先放一邊。文末我會注明可以優化的點,供大家思考、討論。在這里調用完restore()的表象就是canvas的原點又回到了視圖的左上角。關于具體對
canvas.save()
和canvas.restore()
的解釋,網上有一大堆。這里不詳細展開。大致可以理解為save()為保存當前canvas狀態,restore則為恢復上一次save()的狀態。
2.繪制內圓
核心代碼如下
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繪制虛線。
細心的讀者還可以發現,我們使用的繪制圓形的方法不一樣。前面使用的是drawOval繪制橢圓,而在此處使用的是drawCircle直接畫圓,效果都一樣。具體區別可以自己體會,一個是框死了畫內切橢圓,另一個是直接畫圓。
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所示
代碼講解
在上面,我們曾假設了圓弧的角度為240度。便于計算,我們將該圓弧劃分為6個區,每個區占40度,每個區有10個小間隔,每個小間隔的角度就是4度。由于圓弧有30度是在水平線以下的,所以我們的循環規則是上述代碼。canvas.rotate(-(-30 + 4 * i), CENTER_X, CENTER_X);
此處由于CENTER_X==CENTER_Y==r
,將上述代碼修改為canvas.rotate(-(-30 + 4 * i), CENTER_X, CENTER_Y);
或許更容易理解。rotate中參數>0為順時針旋轉,<0為逆時針旋轉。
4.圓環變圓弧
到目前為止,我們畫的還只是個漸變圓環,與效果圓弧還有些不同。下面我們將圓環處理為圓弧。
** 核心代碼如下 **
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如下
** 代碼講解:**
首先我們需要調整視圖的高度。在這之前我們都是令
width==height
,保證繪制出一個整圓。現在根據我們的假設圓弧度數240度,其在水平線以下為30度,即PI/6。由數學公式計算得知,其視圖高度為 height = r * tan(PI/6) + r
。這還不夠,調整完視圖的高度,我們需要將一些雜線,從視圖中除去,讓其看上去更像是個圓弧。
如圖6所示未去雜線的時候
我們利用圖層互相遮罩的原理。以圓心和視圖的兩個頂點,連接成一個三角形,可以達到掩蓋其與雜線的目的。也就是后面代碼的作用。
記住在onDraw時候的一個原則:先畫的在畫布下方,后畫的在畫布上方,后畫的會覆蓋先畫的。從而達到圖5的效果。
5.文字的繪制
** 核心代碼如下**
private static final String[] text = {"950","極好","700","優秀","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如下
** 代碼講解 **
我們已知每個區為40度。從參考效果圖上可以看出每隔20度就會有一段文字。我們知道在繪制文字的時候,都是從左往右寫的。所以我們在旋轉畫布的時候,起始點需要在原來的基礎上再加90度,即逆時針旋轉120度,然后繪入文字。當然這段繪制的過程需要在繪制三角形之后,否則部分文字會被三角形的遮罩遮蓋起來。
6.最后的動效
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()
,此處我們要從左下角開始繪制圓弧,所以我們的起始旋轉角度為-210度。
由于我們此處的原點在圓心。圖片要跟隨著已知的旋轉角度進行旋轉。我們知道針對canvas.rotate()
方法,當旋轉角度>0的時候,是順時針旋轉;<0為逆時針旋轉。由于此處我們圖片的箭頭朝向向右,為了保證圖片的朝向指向圓心。我們旋轉的規則為- 30 + currentRotateAngle
,保證每一次在繪制圖形的時候,都是在(x,y)為(-r1 - bitmapWidth * 3/ 8,-bitmapHeight/2)這個位置的時候繪制。最后恢復canvas。
關于在計算totalRotateAngle
、currentRotateAngle
以及 value
的時候,都是些簡單的算法。夾雜著很多硬編碼,耐心點應該可以讀懂,不做過多解釋。
實現的七七八八,大致思路應該是這樣。
一些問題
- 在上文也提到了,參考的效果圖,并非是一個平滑的漸變。仔細觀察的話,在600處有處瞬斷的跡象。
解決思路:利用上面講到過的PorterDuffXfermode,將兩段不同的環形漸變,拼接而成。到達此效果。 - 關于優化
- onDraw()方法中,canvas.save()與canvas.restore()方法多次使用,造成不必要的性能浪費。
- 在執行箭頭轉動效果的時候,不需要在canvas上每次全部都重新繪制。只需要繪制需要繪制的部分區域即可,即臟矩形。在這里也就是箭頭所滾動范圍內的部分區域圓環。讀者可以自行實現。
- 關于多線程
細心的人可以發現方法setReferValue()
,并沒有考慮多線程的情況。此處只是demo,場景也有限。沒做特殊處理。有興趣的讀者可以自行實現。
后記
之前一直沒有記錄博客的習慣。現在寫完兩篇,發現將代碼翻譯成文不是一件容易的事。代碼在周三就基本完成了,文章也是一直拖著到現在才整理出來發布。要將每一個知識點,能夠簡單的表述出來,是比較難的一件事情。落筆成文同面對面與人講述,會不太一樣。以后要多加強這方面的練習。也希望讀者們能夠一起來嘗試記錄。遺留的問題,都不是很難,讀者可以自行嘗試的去實現。今天腦子有點疼,就寫到此了。
源代碼在此下載:http://pan.baidu.com/s/1kTKUowJ
enjoy it!
想及時了解最新信息。掃一掃,添加關注微信公眾號