編輯:關於Android編程
自定義view練習 仿支付寶芝麻信用的儀表盤
對比圖:


首先是自定義一些屬性,可自己再添加,挺基礎的,上代碼
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RoundIndicatorView">
<!--最大數值-->
<attr name="maxNum" format="integer"/>
<!--圓盤起始角度-->
<attr name="startAngle" format="integer"/>
<!--圓盤掃過的角度-->
<attr name="sweepAngle" format="integer"/>
</declare-styleable>
</resources>
接著在構造方法裡初始化自定義屬性和畫筆:
private void initAttr(AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.RoundIndicatorView);
maxNum = array.getInt(R.styleable.RoundIndicatorView_maxNum,500);
startAngle = array.getInt(R.styleable.RoundIndicatorView_startAngle,160);
sweepAngle = array.getInt(R.styleable.RoundIndicatorView_sweepAngle,220);
//內外圓弧的寬度
sweepInWidth = dp2px(8);
sweepOutWidth = dp2px(3);
array.recycle();
}
private void initPaint() {
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setDither(true);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(0xffffffff);
paint_2 = new Paint(Paint.ANTI_ALIAS_FLAG);
paint_3 = new Paint(Paint.ANTI_ALIAS_FLAG);
paint_4 = new Paint(Paint.ANTI_ALIAS_FLAG);
}
接下來重寫onMeasure,也是比較簡單,對於不是確定值的直接給定300*400的大小:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int wMode = MeasureSpec.getMode(widthMeasureSpec);
int hSize = MeasureSpec.getSize(heightMeasureSpec);
int hMode = MeasureSpec.getMode(heightMeasureSpec);
if (wMode == MeasureSpec.EXACTLY ){
mWidth = wSize;
}else {
mWidth =dp2px(300);
}
if (hMode == MeasureSpec.EXACTLY ){
mHeight= hSize;
}else {
mHeight =dp2px(400);
}
setMeasuredDimension(mWidth,mHeight);
}
核心部分onDraw來了,注意圓的半徑不要在構造方法裡就去設置,那時候是得不到view的寬高值的,所以我在onDraw方法裡設置半徑,默認就view寬度的1/4吧。把原點移到view的中心去:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
radius = getMeasuredWidth()/4; //不要在構造方法裡初始化,那時還沒測量寬高
canvas.save();
canvas.translate(mWidth/2,(mWidth)/2);
drawRound(canvas); //畫內外圓弧
drawScale(canvas);//畫刻度
drawIndicator(canvas); //畫當前進度值
drawCenterText(canvas);//畫中間的文字
canvas.restore();
}
步驟清晰,按順序畫出儀表盤的四個部分,我們一個一個部分的看
drawRound():這個很簡單,內外圓弧所需的屬性都已經定義好了,畫筆是白色的,我們通過setAlpha()設置一下它的透明度,范圍是00~ff。
private void drawRound(Canvas canvas) {
canvas.save();
//內圓
paint.setAlpha(0x40);
paint.setStrokeWidth(sweepInWidth);
RectF rectf = new RectF(-radius,-radius,radius,radius);
canvas.drawArc(rectf,startAngle,sweepAngle,false,paint);
//外圓
paint.setStrokeWidth(sweepOutWidth);
int w = dp2px(10);
RectF rectf2 = new RectF(-radius-w , -radius-w , radius+w , radius+w);
canvas.drawArc(rectf2,startAngle,sweepAngle,false,paint);
canvas.restore();
}
第一部分完成,如圖

drawScale():如果你看過幾篇自定義view文章,應該都知道了靠旋轉畫布來畫刻度和文字的套路了,調用canvas.rotate就可以旋轉畫布,負數代表順時針,這裡我們打算把起始位置旋轉到原點正上方,即270度的地方,這樣畫刻度和文字的坐標就很好計算了,每畫完一次讓畫布逆時針轉一個刻度間隔,一直循環到畫完。我們觀察一下原圖,粗的刻度線一共有6條,數字的刻度是再粗刻度線下面的,每兩個粗刻度線之間有5條細刻度線,並且中間那條細刻度線下方有對應文字。我們把掃過的角度除以30,就是每個刻度的間隔了,然後通過判斷就可以畫對應刻度和文字了。
關於獲取文字的寬高,有兩種方法,一種是paint.measureText(text)測量文字寬度,返回值類型是float,但是得不到高度。另一種是Rect rect = new Rect();paint.getTextBounds(text,0,text.length(),rect); 將文字的屬性放入rect裡,不過是int值,我們畫的文字夠小的了,所以最好用第一種,除非需要高度值。
另外,我發現繪制文字時,坐標值代表的是文字的左下角,不同於一般的從左上角,所以canvas.drawText傳入的xy坐標是text的左下角坐標
private String[] text ={"較差","中等","良好","優秀","極好"};
private void drawScale(Canvas canvas) {
canvas.save();
float angle = (float)sweepAngle/30;//刻度間隔
canvas.rotate(-270+startAngle); //將起始刻度點旋轉到正上方(270)
for (int i = 0; i <= 30; i++) {
if(i%6 == 0){ //畫粗刻度和刻度值
paint.setStrokeWidth(dp2px(2));
paint.setAlpha(0x70);
canvas.drawLine(0, -radius-sweepInWidth/2,0, -radius+sweepInWidth/2+dp2px(1), paint);
drawText(canvas,i*maxNum/30+"",paint);
}else { //畫細刻度
paint.setStrokeWidth(dp2px(1));
paint.setAlpha(0x50);
canvas.drawLine(0,-radius-sweepInWidth/2,0, -radius+sweepInWidth/2, paint);
}
if(i==3 || i==9 || i==15 || i==21 || i==27){ //畫刻度區間文字
paint.setStrokeWidth(dp2px(2));
paint.setAlpha(0x50);
drawText(canvas,text[(i-3)/6], paint);
}
canvas.rotate(angle); //逆時針
}
canvas.restore();
}
private void drawText(Canvas canvas ,String text ,Paint paint) {
paint.setStyle(Paint.Style.FILL);
paint.setTextSize(sp2px(8));
float width = paint.measureText(text); //相比getTextBounds來說,這個方法獲得的類型是float,更精確些
// Rect rect = new Rect();
// paint.getTextBounds(text,0,text.length(),rect);
canvas.drawText(text,-width/2 , -radius + dp2px(15),paint);
paint.setStyle(Paint.Style.STROKE);
}
第二部分完畢,看圖

drawIndicator:這一步是畫外圓弧上的進度值,觀察原圖,發現有三個問題需要解決:表示進度的弧度值和小圓點的坐標怎麼計算,進度值的透明度漸變怎麼實現?小圓點像光源一樣邊緣模糊的效果怎麼實現?
對於坐標計算,其實也較簡單,將當前值比上最大值,得到一個比例就可以計算進度條掃過的弧度,小圓點呢繪制與進度條的尾端,角度已經有了(起始角度+掃過的角度),用三角函數就可以算了。
對於顏色漸變,可以用paint的shader渲染,它有5個子類
我們使用梯度漸變來實現,傳入坐標和一個顏色數組就可以實現對顏色的梯度漸變,這裡我們對顏色的修改當然只是修改它的透明度,我們知道32位的顏色值前8位就是表示透明度的。
對於小圓點有光源一樣的邊緣模糊效果,我用的是paint的setMaskFilter,其中有一個子類BlurMaskFilter可以實現邊緣模糊效果~( 不知道有沒有什麼別的方法實現這種效果) 具體看代碼
private int[] indicatorColor = {0xffffffff,0x00ffffff,0x99ffffff,0xffffffff};
這裡顏色數組這樣取值的原因在文章最後說明
private void drawIndicator(Canvas canvas) {
canvas.save();
paint_2.setStyle(Paint.Style.STROKE);
int sweep;
if(currentNum<=maxNum){
sweep = (int)((float)currentNum/(float)maxNum*sweepAngle);
}else {
sweep = sweepAngle;
}
paint_2.setStrokeWidth(sweepOutWidth);
Shader shader =new SweepGradient(0,0,indicatorColor,null);
paint_2.setShader(shader);
int w = dp2px(10);
RectF rectf = new RectF(-radius-w , -radius-w , radius+w , radius+w);
canvas.drawArc(rectf,startAngle,sweep,false,paint_2);
float x = (float) ((radius+dp2px(10))*Math.cos(Math.toRadians(startAngle+sweep)));
float y = (float) ((radius+dp2px(10))*Math.sin(Math.toRadians(startAngle+sweep)));
paint_3.setStyle(Paint.Style.FILL);
paint_3.setColor(0xffffffff);
paint_3.setMaskFilter(new BlurMaskFilter(dp2px(3), BlurMaskFilter.Blur.SOLID)); //需關閉硬件加速
canvas.drawCircle(x,y,dp2px(3),paint_3);
canvas.restore();
}
記得關閉硬件加速,就是加一句<activity Android:hardwareAccelerated="false" >
第三部完畢,看圖

drawCenterText:這步簡單,注意剛才說的繪制文字時從左下角開始的和兩種測量文字寬度的區別就好。
private void drawCenterText(Canvas canvas) {
canvas.save();
paint_4.setStyle(Paint.Style.FILL);
paint_4.setTextSize(radius/2);
paint_4.setColor(0xffffffff);
canvas.drawText(currentNum+"",-paint_4.measureText(currentNum+"")/2,0,paint_4);
paint_4.setTextSize(radius/4);
String content = "信用";
if(currentNum < maxNum*1/5){
content += text[0];
}else if(currentNum >= maxNum*1/5 && currentNum < maxNum*2/5){
content += text[1];
}else if(currentNum >= maxNum*2/5 && currentNum < maxNum*3/5){
content += text[2];
}else if(currentNum >= maxNum*3/5 && currentNum < maxNum*4/5){
content += text[3];
}else if(currentNum >= maxNum*4/5){
content += text[4];
}
Rect r = new Rect();
paint_4.getTextBounds(content,0,content.length(),r);
canvas.drawText(content,-r.width()/2,r.height()+20,paint_4);
canvas.restore();
}
到這裡繪制部分差不多完成了。接下來要實現的是當改變值時的動畫效果,同時改變背景顏色。
setCurrentNumAnim就是供用戶調用的。我們可以通過屬性動畫來改變當前值,注意要給當前值(currentNum)加上setter和getter,因為屬性動畫內部需要調用它們。
對於動畫的時間,簡單寫個計算公式就好,然後監聽動畫過程,在裡面實現背景顏色的改變。怎麼才能像支付寶芝麻信用那樣紅橙黃綠藍的漸變呢?我按自己思路實現了一個可以三種顏色之間漸變的效果。
大家學習屬性動畫時應該了解過插值器估值器的作用,我就是用ArgbEvaluator估值器實現顏色漸變的,調用它的evaluate方法,傳入一個0~1的比例,傳入開始和結束的顏色,就可以根據當前比例得到介於這兩個顏色之間的顏色值。
這裡我實現了紅到橙再到藍的漸變,假設最大值是500,那麼當前值x從0~250的過程中是從紅到橙,x/(500/2)就可以得到一個0~1的變化比例,當前值從250~500的過程是橙到藍,也需要一個0~1的變化過程的比例,計算方法就是(x-250)/(250) 其中250就是(500/2)得來的。按照這樣的思路當然可以實現更多顏色之間的漸變,就是想辦法在各區間裡算出一個0~1的比例值就行。注意數據類型轉換,上代碼!
public int getCurrentNum() {
return currentNum;
}
public void setCurrentNum(int currentNum) {
this.currentNum = currentNum;
invalidate();
}
public void setCurrentNumAnim(int num) {
float duration = (float)Math.abs(num-currentNum)/maxNum *1500+500; //根據進度差計算動畫時間
ObjectAnimator anim = ObjectAnimator.ofInt(this,"currentNum",num);
anim.setDuration((long) Math.min(duration,2000));
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
int color = calculateColor(value);
setBackgroundColor(color);
}
});
anim.start();
}
private int calculateColor(int value){
ArgbEvaluator evealuator = new ArgbEvaluator();
float fraction = 0;
int color = 0;
if(value <= maxNum/2){
fraction = (float)value/(maxNum/2);
color = (int) evealuator.evaluate(fraction,0xFFFF6347,0xFFFF8C00); //由紅到橙
}else {
fraction = ( (float)value-maxNum/2 ) / (maxNum/2);
color = (int) evealuator.evaluate(fraction,0xFFFF8C00,0xFF00CED1); //由橙到藍
}
return color;
}
锵锵锵~ 完畢外部調用setCurrentNumAnim就可以動畫的改變數值啦


好了,還有最後一個問題,就是前面提到的
為什麼透明度漸變的顏色數組是這樣的
private int[] indicatorColor = {0xffffffff,0x00ffffff,0x99ffffff,0xffffffff};
大概就是從不透明-->透明-->半透明-->不透明的變化
問:第一個不是多余的麼?為什麼要一開始不透明?
答:我也有點納悶,因為sweepGradient顏色漸變是從x正軸開始的,如果我顏色數組是這樣的,即從透明-->半透明-->不透明:
private int[] indicatorColor = {0x00ffffff,0x99ffffff,0xffffffff};
那麼畫個圓是長這樣的

而我們的儀表盤這裡是從160度開始,掃220度,也就是如果這樣有一部分角度(0~20度)會變透明,不是我們想要的效果。。。所以我用了這樣:
private int[] indicatorColor = {0xffffffff,0x00ffffff,0x99ffffff,0xffffffff};
這樣的數組。。畫出來是這樣的

這樣至少保證0~20度看起來也是很白的,整個進度條就實現了像從透明到不透明的效果。
其實也不是很優雅。。因為起始角度和掃過的角度是可以自定義更改的。。所以小伙伴們有什麼更好的方法麼?
源碼地址:http://xiazai.jb51.net/201701/yuanma/diy_roundindicator_jb51.rar
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持本站。
android中使用Activity實現監聽手指上下左右滑動
用Activity的onTouchEvent方法實現監聽手指上下左右滑動應用了Activity的ontouchEvent方法監聽手指點擊事件,手指滑動的時候會先按下,滑倒
Android實現單選與多選對話框的代碼
android開發中實現單選與多選對話框的代碼非常簡單,具體代碼如下所示:public void myClick(View view) { // 單選對話框 //si
Android打包利器Gradle之三板斧
當傳統的手工打包方式遇上同一應用,多渠道/多包名及多種引導頁/icon等等差異時,就變成了苦不堪言的純體力活了。 但有了Gradle這一切不再是問題了,Gradle使得這
Android之自定義View以及畫一個時鐘
概述:當Android自帶的View滿足不了開發者時,自定義View就發揮了很好的作用。建立一個自定義View,需要繼承於View類,並且實現其中的至少一個構造函數和兩個