1.寫在前面
專案中做了個儀表盤效果,需要支援在java中實時更改最大值以及當前值,效果圖如下
同時我這裡處理了下,讓它可以支援float型別的資料2.實現
首先這個自定義view沒有涉及事件分發和動畫,純粹全部都是draw,所以是個練手的好機會。對這個view分析下,可以分為如下幾部分
我們需要做的就是按部就班的全部畫出來即可。2.1 外弧
我這裡方法可能一別人不一樣,我首先就移動了畫布,將畫布的移動到view的中心,所以中心座標是(0,0),我覺得這樣子後面座標點的座標好算點。
//整個view的長 寬
private int mWidth,mHeight;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
/**
* 圓弧外切矩形的寬度,圓弧的半徑即為它的一半
*/
mRectWidth = (float) (Math.min(mWidth,mHeight)*0.9);
}
/**
* 將圓心移到控制元件中間
*/
canvas.translate(mWidth / 2, mHeight / 2);
複製程式碼
上方的Math.min(x,y)*0.9是 正方形類自定義view的常規處理,畢竟這種view的長寬設定時一般都是一樣的,但是萬一設定的不一樣,就取最小的同時在縮小點,留點padding
第一步當然畫如上圖的弧,我為了更明顯,紅色的座標系是我加上過去的。這個弧比較容易畫,比較明顯弧的起始角度是135,弧掃過的角度是270
/**
* 畫圓弧
* @param canvas
*/
private void drawArc(Canvas canvas){
mArcPaint.setStyle(Paint.Style.STROKE);
RectF rectFArc = new RectF(-mRectWidth / 2, -mRectWidth / 2, mRectWidth / 2, mRectWidth / 2);
canvas.drawArc(rectFArc,mStartAngle,mSweepAngle,false,mArcPaint);
}
複製程式碼
一般來說,畫圓弧的第一步是獲取圓弧的外切矩形,比如上的rectFArc
2.2 畫短刻度
這裡為什麼是先畫短刻度,而不是長刻度,我這裡的處理是一共分為十個大份,每個大份分為十個小份,其實長刻度是屬於短刻度的,屬於包含關係,所以我先講短刻度畫出來,然後長刻度覆蓋即可。畫短刻度的思路是,先找出短刻度兩個點的座標,然後將兩個點這條線畫出來,在通過畫布旋轉。
如上圖所示,每個短刻度都有兩個點,紅線是一個點的x,y座標,綠線是一個點的x,y座標,將這個短刻度與圓心的連線,所以這兩個點其實就是三角函式的值。設定好短刻度的長度,然後就知道這兩個點到圓心連線的長度,這樣就可以通過三角函式算出兩個點的座標。 /**
* 畫短刻度
* @param canvas
*/
private void drawGraduation2(Canvas canvas){
/**
* 先將畫布儲存,等下畫長度前拿出來,這樣 畫布相當於是最新的,不然 先畫短刻度後 在畫長刻度 畫布的角度已經變了
*/
canvas.save();
/**
* 將角度135 轉成弧度 3pi/4
*/
float startPi = (float) Math.toRadians(mStartAngle);
/**
* 刻度兩個點的座標
*/
float[] point1 = new float[2];
float[] point2 = new float[2];
point1[0] = (float) (Math.cos(startPi)*mRectWidth/2);
point1[1] = (float) (Math.sin(startPi) * mRectWidth / 2);
/**
* 半徑減去短刻度的長度,然後算出三角函式
*/
point2[0] = (float) (Math.cos(startPi)*(mRectWidth/2-shortLength));
point2[1] = (float) (Math.sin(startPi) * (mRectWidth / 2 - shortLength));
for(float i=0;i<=270;i+=avgSmallAngle){
canvas.drawLine(point2[0],point2[1],point1[0],point1[1],mArcPaint);
canvas.rotate(avgSmallAngle);
}
}
複製程式碼
2.3 畫長刻度
長刻度如上一樣,只是畫布旋轉的角度不一樣而已,直接看程式碼即可
/**
* 畫長刻度
* @param canvas
*/
private void drawGraduation(Canvas canvas){
canvas.restore();
/**
* 再次儲存 為了以後的畫布而言,拿出來時 還是最初的
*/
canvas.save();
/**
* 將角度135 轉成弧度 3pi/4
*/
float startPi = (float) Math.toRadians(mStartAngle);
/**
* 刻度兩個點的座標
*/
float[] point1 = new float[2];
float[] point2 = new float[2];
point1[0] = (float) (Math.cos(startPi)*mRectWidth/2);
point1[1] = (float) (Math.sin(startPi) * mRectWidth / 2);
point2[0] = (float) (Math.cos(startPi)*(mRectWidth/2-longLength));
point2[1] = (float) (Math.sin(startPi) * (mRectWidth / 2 - longLength));
for(float i=0;i<=270;i+=avgLargeAngle){
canvas.drawLine(point2[0],point2[1],point1[0],point1[1],mArcPaint);
canvas.rotate(avgLargeAngle);
}
}
複製程式碼
2.4 畫長刻度讀數
我們在畫長刻度時,首先要獲取長刻度的內容,這裡程式碼比較簡單,只是我在這裡將資料處理了下,所有的數都保留一位有效數字,這樣如果最大值是小數時,也可以支援,免得畫出來很難看
/**
* 長刻度文字內容陣列
*/
private String[] textContent = null;
/**
* 獲取長刻度 文字內容
*/
private void getTextContent(){
mMax = getmMax();
textContent = new String[largeSection + 1];
for(int i=0;i<textContent.length;i++){
float avgF = (mMax-mMin)/largeSection;
float result = avgF*i;
Log.d(TAG,"------result------ "+result);
/**
* 對值進行處理,保留一位小數
*/
String lastResutl = new DecimalFormat("#.0").format(result);
/**
* 注意和上面的區別,上面的是 所有的值 始終保留一位小數,下面是 如果是整數則不保留,如果不是整數,則保留一位小數
*/
//String lastResutl = new DecimalFormat("#.0").format(result);
textContent[i] = lastResutl;
}
}
複製程式碼
這讀數都是隨著圓弧畫出來的,所以這裡使用的方法是drawTextOnPath,其中需要一個path物件,我們需要做的就是給path新增一個圓弧Arc,這裡我們需要畫11個資料,同時圓弧也要畫11次,只不過圓弧每次起點不一樣,同時這裡我對角度處理了下,等下可以對比下
/**
* 畫長刻度 文字
* @param canvas
*/
private void drawText(Canvas canvas){
canvas.restore();
canvas.save();
inArcRect = new RectF(-mRectWidth / 2+longLength+longLength,-mRectWidth / 2+longLength+longLength,mRectWidth / 2-longLength-longLength,mRectWidth / 2-longLength-longLength);
mInPath = new Path();
textRect = new Rect();
mArcPaint.setTextAlign(Paint.Align.LEFT);
mArcPaint.setTextSize(sp2px(9));
mArcPaint.setStyle(Paint.Style.FILL);
for(int i=0;i<textContent.length;i++){
mArcPaint.getTextBounds(textContent[i],0,textContent[i].length(),textRect);
/**
* 處理角度 這裡暫時把文字的寬度當作弧長,然後通過弧長算出對應的角度,
* 弧長公式 l(弧長) = n(圓心角) × 派 × r(半徑) / 180
* 所以反推出 n = l*180/派×r
*/
int θ = (int) ((textRect.width()/2*180)/(Math.PI*mRectWidth / 2-longLength-longLength));
mInPath.reset();
mInPath.addArc(inArcRect, mStartAngle + i * (mSweepAngle / largeSection)-θ, mSweepAngle);
canvas.drawTextOnPath(textContent[i],mInPath,0,0,mArcPaint);
}
}
複製程式碼
不處理是這樣的
處理的方法程式碼中寫的很詳細了,就是將文字的長度近似於弧長,然後反推弧長公式算出角度,再改變起始角度即可,上面 0 前面那個 . 我已經處理了,寫部落格時沒發現。
2.5 畫表頭
畫表頭比較簡單,一般來說這種只需設定一次的,設定到xml中比較方便。所以在xml自定義一個表頭屬性即可。然後在建構函式中獲取表頭內容,如果為空則不畫,不為空 則畫出來,畫文字這裡因為畫布在中心的原因比較簡單,x座標為0,y座標取半徑的一般既可,比較簡單直接上程式碼了
/**
* 畫表頭,如果表頭不存在即不畫
* @param canvas
*/
private void drawHeadText(Canvas canvas){
if(!TextUtils.isEmpty(mHeadText)){
canvas.restore();
canvas.save();
mArcPaint.setTextSize(sp2px(14));
mArcPaint.setTextAlign(Paint.Align.CENTER);
//mArcPaint.getTextBounds(mHeadText,0,mHeadText.length(),textRect);
canvas.drawText(mHeadText,0,-mRectWidth/4,mArcPaint);
}
}
複製程式碼
2.6 畫指標
這裡我畫指標的思路是先算出指標的角度,然後找出指標的四個點座標,通過path畫出來即可,所以指標的難點是找出四個點的座標。我這裡畫了個草圖,將指標四個點排下序。
如上圖所示,外弧是最外面的弧,內弧是文字的弧,這四個點中的A點不用算了,就是(0,0),C點其實也比較簡單就是AC長度的三角函式,B 的座標其實就是AB長度的三角函式,這裡我將指標由兩部分組成,一個是ABD的等邊三角形和BCD的三角形,所以AC和AB之間的角度是30,這樣就可以利用三角函式了(AB是起始角度+30,AD是起始角度-30) /**
* 畫指標,因為這裡並不是將 指標畫出來 然後算出角度 讓它旋轉,這裡是直接算出角度 然後畫出來,所以我們畫指標 首先就是算出指標角度
*/
private void drawPointer(Canvas canvas){
canvas.restore();
canvas.save();
float [] pointC = new float[2];
float[] pointB = new float[2];
float[] pointD = new float[2];
mArcPaint.setStyle(Paint.Style.FILL);
/**
* 算出指標的弧度
*/
float angle = (float) Math.toRadians((mCurrentValue/mMax)*mSweepAngle+mStartAngle);
Log.d(TAG, " ------------angle----------- " + angle);
/**
* 指標的半徑
*/
float pointerRadius = mRectWidth/2-longLength-longLength-6;
mInPath.reset();
float d = 36;
/**
* C點座標 指標最末點的位置座標(遠離圓心)
*/
pointC[0] = (float) (Math.cos(angle)*pointerRadius);
pointC[1] = (float) (Math.sin(angle) * pointerRadius);
/**
* B點座標
*/
pointB[0] = (float) (Math.cos(angle+Math.toRadians(30))*d);
pointB[1] = (float) (Math.sin(angle + Math.toRadians(30)) * d);
/**
* D點座標
*/
pointD[0] = (float) (Math.cos(angle - Math.toRadians(30)) * d);
pointD[1] = (float) (Math.sin(angle - Math.toRadians(30)) * d);
mInPath.lineTo(pointB[0], pointB[1]);
mInPath.lineTo(pointC[0], pointC[1]);
mInPath.lineTo(pointD[0], pointD[1]);
mInPath.close();
canvas.drawPath(mInPath,mArcPaint);
}
複製程式碼
實時讀數和表頭是一樣的,就不上圖和程式碼了。 到這裡為止,剩下的就沒什麼事了,測量和寫公開方法,讓外部呼叫,這裡比較套路,就不上程式碼了,等下看原始碼即可。