Android 圖解自定義車速表
簡單講述繪製圓弧、漸變圓、時速表刻度、文字和時速指標
1.前言:下圖來自於度娘,擷取一部分來繪製,其他的內容大同小異;而動圖為所實現的效果圖。
2.需求分析:如上第一張圖,這些引數可能經常被變動,所以把這些做成自定義屬性,方便後面修改。
3.自定義屬性的定義: 在values目錄下新建attr.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--1.自定義屬性-->
<declare-styleable name="CarBoardView">
<attr name="outRingColor" format="color"/>
<attr name="innerRingColor" format="color"/>
<attr name="speedColor" format="color"/>
<attr name="indicatorColor" format="color"/>
<attr name="outRingRadius" format="float"/>
<attr name="innerRingRadius" format="float"/>
<attr name="outSpeedSize" format="float"/>
<attr name="innerSpeedSize" format="float"/>
<attr name="speedUnitSize" format="float"/>
</declare-styleable>
</resources>
如果不知道格式可參考android原始碼定義的:sdk\platforms\android-23\data\res\values
4.自定義屬性的獲取: TypedArray是通過上下文context獲取的,要注意用完要呼叫recycle()方法進行回收
public CarBoardView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
//螢幕密度,為了適配各種不同畫素的手機
mDensity = context.getResources().getDisplayMetrics().density;
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CarBoardView,
defStyleAttr, defStyleRes);
mOutRingColor = a.getColor(R.styleable.CarBoardView_outRingColor, Color.BLUE);
mInnerRingColor = a.getColor(R.styleable.CarBoardView_innerRingColor, Color.BLUE);
mSpeedColor = a.getColor(R.styleable.CarBoardView_speedColor, Color.WHITE);
mIndicatorColor = a.getColor(R.styleable.CarBoardView_indicatorColor, Color.RED);
mOutRingRadius = a.getFloat(R.styleable.CarBoardView_outRingRadius, 100) * mDensity;
mInnerRingRadius = a.getFloat(R.styleable.CarBoardView_innerRingRadius, 50) * mDensity;
mOutSpeedSize = a.getFloat(R.styleable.CarBoardView_outSpeedSize, 13) * mDensity;
mInnerSpeedSize = a.getFloat(R.styleable.CarBoardView_innerSpeedSize, 18) * mDensity;
mSpeedUnitSize = a.getFloat(R.styleable.CarBoardView_speedUnitSize, 13) * mDensity;
a.recycle();
mHeight = mWidth = (int) (mOutRingRadius*2 + 10*mDensity);//先定義寬高,可以先初始化畫筆等
initTools();
}
5.測量控制元件大小:通過重新onMeasure方法,呼叫setMeasuredDimension方法限定控制元件的寬高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mStartMarkX = (float) (mWidth/2 - mOutRingRadius*Math.sin(Math.PI*45/180) + 5*mDensity);
mStartMarkY = (float) (mWidth/2 + mOutRingRadius*Math.cos(Math.PI*45/180) + 5*mDensity);
mMarkAngle = 270 / 15f;
Log.i(TAG, "onMeasure mWidth: " + mWidth + ",mHeight: " + mHeight + " ,mMarkAngle: "+mMarkAngle);
setMeasuredDimension(mWidth, mHeight);//限定本view的寬高要一致
}
6.初始化畫筆等:初始化畫筆等工具
private void initTools() {
Log.i(TAG,"initTools");
mPaint = new Paint();
mOutRingRectF = new RectF(5*mDensity,5*mDensity,mWidth-5*mDensity,mHeight-5*mDensity);//距離邊界5*mDensity
mShader = new RadialGradient(mWidth/2,mHeight/2,mInnerRingRadius, //三個數字分別表示,圓心的X、Y軸座標以及半徑
new int[]{mInnerRingColor,0xFF53C0E7, 0xFF2062E8}, //這裡是用來設定顏色值的,在這個int陣列內可以有N組Color值
new float[]{0.6f,0.8f,1f},Shader.TileMode.MIRROR);//0.6f,0.8f,1f透明度是指從裡到外的漸變;而且注意要跟上面Color資料長度相等
mTextPaint = new Paint();
mBound = new Rect();
}
7.onDraw繪製過程:在onDraw裡不要儘量不要建立物件,因為頻繁的繪製會不斷的建立物件,然而gc會不斷的回收,會降低效能
(1)畫外圓弧:drawArc方法,參1:用來定義形狀和大小;參2:弧的起始角度(解析下圖);參3:旋轉過的角度;參4:是否閉合(即與圓中心是否相連);參5:畫筆
mPaint.setStrokeWidth(2*mDensity);
mPaint.setColor(mOutRingColor);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawArc(mOutRingRectF,120,300,false, mPaint);
(2)畫漸變內圓:mPaint.setShader(mShader)給畫筆設定圓環的漸變效果,上面有介紹;drawCircle方法,參1:圓中心x座標;參2:圓中心y座標;參3:半徑;參4:畫筆
mPaint.setStrokeWidth(7*mDensity);
mPaint.setShader(mShader);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(mWidth/2,mHeight/2,mInnerRingRadius-5*mDensity, mPaint);//這裡繪製出來一個比漸變圓略小的圓,並且覆蓋到漸變圓上
(3)畫外圓刻度:畫出第一條線,然後以圓心為旋轉點,經過n°畫出16個刻度;drawLine方法:參1:起始x座標;參2:起始y座標;參3:終點x座標;參4:終點y座標;參5:畫筆
canvas.save(); //這時候儲存的是畫布沒旋轉之前的狀態
mPaint.reset();//重置畫筆
float degreeLength = 10*mDensity;
mPaint.setColor(mOutRingColor);
mPaint.setStrokeWidth(2*mDensity);
mPaint.setAntiAlias(true);
for(int i=0;i<16;i++){
canvas.drawLine(mStartMarkX, mStartMarkY, mStartMarkX+degreeLength, mStartMarkY-degreeLength, mPaint);
canvas.rotate(mMarkAngle, mWidth/2, mHeight/2);//旋轉角度,x支點,y支點(就是環繞支點移動)
}
(4)畫外圓時速(數字):這裡得要慢慢調,比較麻煩,畫字型是從左下標開始的
canvas.restore();//還原狀態(還原上一個save的狀態),即將旋轉過的畫布重置
mTextPaint.setTypeface(Typeface.DEFAULT_BOLD);
mTextPaint.setColor(mSpeedColor);
mTextPaint.setTextSize(mOutSpeedSize);
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setAntiAlias(true);
float x;
float y;
for(int i=0;i<16;i++){
x = (float) (mWidth/2 - (mOutRingRadius-degreeLength)*Math.cos((Math.PI*45-Math.PI*mMarkAngle*i)/180));
y = (float) (mWidth/2 + (mOutRingRadius-degreeLength)*Math.sin((Math.PI*45-Math.PI*mMarkAngle*i)/180));
switch (i){
case 0:case 1:case 2:
x = x+(i+1)*mDensity*(i==0?4:(i==1?3:2));
y = y+(i+1)*mDensity*4;
break;
case 3:case 4:case 5:
x = x-i*mDensity/(i==3?-1:2);
y = y+i*mDensity*(i==5?3:4);
break;
case 6:case 7:case 8:
x = x-i*mDensity*2;
y = (float) (y+i*mDensity*(i==6?3:(i==7?2:1.5)));
break;
case 9:case 10:case 11:
x = x-i*mDensity*2;
y = (float) (y+i*mDensity/(i==9?1:(i==10?1.5:3)));
break;
case 12:case 13:case 14:
x = (float) (x-i*mDensity*(i==12?2:1.5));
y = y-i*mDensity/2;
break;
case 15:
x = x-i*mDensity;
y = y-i*mDensity/2;
break;
}
canvas.drawText(String.valueOf(30*i),x, y,mTextPaint);
}
(5)畫內圓時速:使用畫筆設定getTextBounds方法,實現根據字型長度而居中顯示
String text = String.valueOf(mSpeed);
mTextPaint.setTextSize(mInnerSpeedSize);
mTextPaint.setColor(mSpeedColor);
mTextPaint.getTextBounds(text, 0, text.length(), mBound);
float startX1 = mWidth/2 - mBound.width()/2;//控制元件寬度/2 - 文字寬度/2
float startY1 = mHeight/2 + mBound.height()/2-mInnerSpeedSize;//控制元件高度/2 + 文字高度/2,繪製文字從文字左下角開始,因此"+"
canvas.drawText(text, startX1, startY1, mTextPaint);// 繪製文字
(6)畫內圓速度單位:跟上面一樣原理
String text2 = "km·h";
mTextPaint.setTextSize(mSpeedUnitSize);
mTextPaint.setColor(mOutRingColor);
mTextPaint.getTextBounds(text2, 0, text2.length(), mBound);
float startX2 = mWidth/2 - mBound.width()/2;//控制元件寬度/2 - 文字寬度/2
float startY2 = mHeight/2 + mBound.height()/2;//控制元件高度/2 + 文字高度/2,繪製文字從文字左下角開始,因此"+"
canvas.drawText(text2,startX2, startY2,mTextPaint);
(7)畫時速指標:其實就是畫一個封閉的三角形
Path path = new Path();//這裡建立Path物件為了保證每次繪製都是新一條path(並且顯示出來只有一條)
mPaint.setStrokeWidth(2*mDensity);
mPaint.setColor(mIndicatorColor);
mPaint.setStyle(Paint.Style.FILL);
int m = 7;
float startX = (float) (mWidth/2 - mOutRingRadius*Math.cos((Math.PI*45 - Math.PI*(mSpeed-m)/450*270)/180));
float startY = (float) (mWidth/2 + mOutRingRadius*Math.sin((Math.PI*45 - Math.PI*(mSpeed-m)/450*270)/180));
float endX1 = (float) (mWidth/2 - (mInnerRingRadius-m*mDensity)*Math.cos((Math.PI*45-Math.PI*(mSpeed+m)/450*270)/180));
float endY1 = (float) (mWidth/2 + (mInnerRingRadius-m*mDensity)*Math.sin((Math.PI*45-Math.PI*(mSpeed+m)/450*270)/180));
float endX2 = (float) (mWidth/2 - (mInnerRingRadius-m*mDensity)*Math.cos((Math.PI*45-Math.PI*(mSpeed-m)/450*270)/180));
float endY2 = (float) (mWidth/2 + (mInnerRingRadius-m*mDensity)*Math.sin((Math.PI*45-Math.PI*(mSpeed-m)/450*270)/180));
path.moveTo(startX, startY);// 此點為多邊形的起點,指標的尖的一端
path.lineTo(endX1, endY1);
path.lineTo(endX2, endY2);
path.close(); // 使這些點構成封閉的多邊形
canvas.drawPath(path, mPaint);
8.設定對外介面:根據外界傳遞的速度進行重繪view,重新執行onDraw裡面的程式碼
public void setSpeed(int speed){
mSpeed = speed;
if(isMainThread())
invalidate();//在UI執行緒中呼叫,進行重繪
else
postInvalidate();//在子執行緒中呼叫,進行重繪
}
public boolean isMainThread() {
return Looper.getMainLooper() == Looper.myLooper();
}
9.進行測試:在Activity進行測試
final CarBoardView cbv = (CarBoardView) findViewById(R.id.cbv);
cbv.setSpeed(0);
new Thread(new Runnable() {
@Override
public void run() {
int i=0;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (i<=450){
cbv.setSpeed(i);
i = i+i%15 +1;
// Log.i(TAG,"====i: "+i);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
**10.後語:**android東西太多了,做的時候肯定有一些沒遇到過,我通常是會看看給類會提供什麼方法供我達到效果;比如在做圓環漸變效果時,看到有提供setShader方法,初步估計能達到效果;然後百度了一下,看到有個子類RadialGradient能很好的達到效果,那博主看沒有注意這可以支援多維陣列的,而他認為只能實現兩組,也就是兩種漸變的效果;歡迎大家過來交流,共同進步。