自定義View繪製餅狀圖和環狀圖
阿新 • • 發佈:2019-01-04
最近工作中遇到一個需求,就是將不同年齡段資料以餅狀圖或者環狀圖的形式展示出來。於是利用android自定義的知識封裝一個自定義View,方便日後使用,特此記錄。
效果圖如下
1.餅狀圖
1.環狀圖
主要強調以下3部分
1.value中新增attr.xml屬性檔案
2.資料來源
3.自定義餅圖或者環形圖
1.value中新增attr.xml屬性檔案
value/attr_sector.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
< declare-styleable name="SectorView">
<!-- 圓的半徑 -->
<attr name="min_circle_radio" format="float"/>
<!-- 內圓的顏色 -->
<attr name="min_circle_color" format="color"/>
<!-- 扇形半徑 -->
<attr name="sector_radio" format="float"/>
<!-- 扇形分幾段 -->
<attr name="sector_part_num" format="integer"/>
<!--描述文字顏色-->
<attr name="sector_desc_text_color" format="integer"/>
<!--描述文字大小-->
<attr name="sector_desc_text_size" format="float"/>
</declare-styleable >
</resources>
之所以編寫attr.xml檔案,原因有二:
(1).為了更直觀的瞭解自定義View涉及到的屬性引數,方便管理
(2).在自定義View檔案中,封裝了一些通用的介面(eg.設定描述文字的字型顏色,內圓的半徑和顏色等等),在設定之前,往往會初始化一些預設值。這就用到我們的attr屬性了。
2.資料來源
public class AgeEntry {
private int totalPart;
private int childTotalIn;
private int youthTotalIn;
private int middleTotalIn;
private int oldTotalIn;
public int getChildTotalIn() {
return childTotalIn;
}
public void setChildTotalIn(int childTotalIn) {
this.childTotalIn = childTotalIn;
}
public int getYouthTotalIn() {
return youthTotalIn;
}
public void setYouthTotalIn(int youthTotalIn) {
this.youthTotalIn = youthTotalIn;
}
public int getMiddleTotalIn() {
return middleTotalIn;
}
public void setMiddleTotalIn(int middleTotalIn) {
this.middleTotalIn = middleTotalIn;
}
public int getOldTotalIn() {
return oldTotalIn;
}
public void setOldTotalIn(int oldTotalIn) {
this.oldTotalIn = oldTotalIn;
}
public int getTotalPart() {
return totalPart;
}
public void setTotalPart(int totalPart) {
this.totalPart = totalPart;
}
@Override
public String toString() {
return "AgeEntry{" +
"totalPart=" + totalPart +
", childTotalIn=" + childTotalIn +
", youthTotalIn=" + youthTotalIn +
", middleTotalIn=" + middleTotalIn +
", oldTotalIn=" + oldTotalIn +
'}';
}
}
待展示的資料,你完全可以按照自己的需求去定義。這些資料最終都需要你去換算成佔比,用於圖形的繪製。
3.自定義餅圖或者環形圖(完整檔案)
public class SectorView extends View {
private static final String TAG = "SectorView";
//圓心座標
private int mHeight, mWidth;
private int centerX, centerY;
//畫筆
private Paint mPaint;
private Paint mTextPaint;
//描述文字
private float mDescTextSize;
private int mDescTextColor;
//內圓
private float mMinCircleRadio;
private int mSectorNum;
//扇形
private int mMinCircleColor;
private float mSectorRadio;
//是否顯示裡面的圓
private boolean isShowInnerCircle = false;
//資料
private float[] mAgeLevelPercent = new float[4];
private String[] mAgeDesc = new String[4];
//age年齡段對應的顏色
private int[] mAgeColors = {
Color.parseColor("#67E5E5"),
Color.parseColor("#8BB6F6"),
Color.parseColor("#C29DFC"),
Color.parseColor("#E5E570"),
};
public SectorView(Context context) {
super(context, null);
}
public SectorView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SectorView);
//裡面圓半徑,預設200f
mMinCircleRadio = a.getFloat(R.styleable.SectorView_min_circle_radio, 120f);
//裡面圓的顏色,預設白色
mMinCircleColor = a.getColor(R.styleable.SectorView_min_circle_color, Color.parseColor("#ffffff"));
//扇形半徑,預設300f
mSectorRadio = a.getFloat(R.styleable.SectorView_sector_radio, 300f);
//扇形分幾段
mSectorNum = a.getInt(R.styleable.SectorView_sector_part_num, 4);
mDescTextSize = a.getFloat(R.styleable.SectorView_sector_desc_text_size, 40f);
mDescTextColor = a.getInt(R.styleable.SectorView_sector_desc_text_color, Color.parseColor("#000000"));
mPaint = new Paint();
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setAntiAlias(true);
mTextPaint = new Paint();
mTextPaint.setTextSize(40f);
mTextPaint.setStrokeWidth(3);
mTextPaint.setAntiAlias(true);
mTextPaint.setColor(Color.BLACK);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mHeight = MeasureSpec.getSize(heightMeasureSpec);
mWidth = MeasureSpec.getSize(widthMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
centerX = (getRight() - getLeft()) / 2;
centerY = (getBottom() - getTop()) / 2;
int min = mHeight > mWidth ? mWidth : mHeight;
if (mSectorRadio > min / 2) {
mSectorRadio = (int) ((min - getPaddingTop() - getPaddingBottom()) / 3.5);
}
canvas.save();
drawCircle(canvas);
canvas.restore();
canvas.save();
drawLineAndText(canvas);
canvas.restore();
}
/**
* 繪製線與文字
*
* @param canvas
*/
private void drawLineAndText(Canvas canvas) {
int start = 0;
canvas.translate(centerX, centerY);
mTextPaint.setStrokeWidth(4);
for (int i = 0; i < mSectorNum; i++) {
float angles = mAgeLevelPercent[i] * 360;
drawLine(canvas, start, angles, mAgeDesc[i], mAgeColors[i]);
start += angles;
}
}
/**
* 繪製線和文字
*
* @param canvas
* @param start 繪製的起始角度
* @param angles 資料塊佔用的角度(掃過的扇形弧度)
* @param text 待繪製的描述文字
* @param color
*/
private void drawLine(Canvas canvas, int start, float angles, String text, int color) {
mTextPaint.setColor(color);
//參照數學公式::b = c *Math.cos(Math.toRadians(A)),其中Math.toRadians(A)::角度轉換成弧度
float startX, startY;
startX = (float) ((mSectorRadio - 20) * Math.cos(Math.toRadians(start + angles / 2)));
startY = (float) ((mSectorRadio - 20) * Math.sin(Math.toRadians(start + angles / 2)));
//折線的終點
float stopX, stopY;
stopX = (float) ((mSectorRadio + 40) * Math.cos(Math.toRadians(start + angles / 2)));
stopY = (float) ((mSectorRadio + 40) * Math.sin(Math.toRadians(start + angles / 2)));
canvas.drawLine(startX, startY, stopX, stopY, mTextPaint);
//繪製橫線
int endX;
if (stopX > 0) {//判斷橫線是畫在左邊還是右邊
endX = (centerX - getPaddingRight() - 20);
} else {
endX = (-centerX + getPaddingLeft() + 20);
}
canvas.drawLine(stopX, stopY, endX, stopY, mTextPaint);
int dx = (int) (endX - stopX);//判斷文字繪製在左邊還是右邊
//測量文字大小
Rect rect = new Rect();
mTextPaint.getTextBounds(text, 0, text.length(), rect);
int w = rect.width();
int h = rect.height();
int offset = 20;//文字在橫線的偏移量
canvas.drawText(text, 0, text.length(), dx > 0 ? stopX + offset : stopX - w - offset, stopY + h, mTextPaint);
//測量百分比大小
String percentage = angles / 3.60 + "";
//控制百分比的位數
percentage = percentage.substring(0, percentage.length() > 4 ? 4 : percentage.length()) + "%";
mTextPaint.getTextBounds(percentage, 0, percentage.length(), rect);
w = rect.width() - 10;
//繪製百分比
canvas.drawText(percentage, 0, percentage.length(), dx > 0 ? stopX + offset : stopX - w - offset, stopY - 5, mTextPaint);
}
/**
* 繪製扇形
*
* @param canvas
*/
private void drawCircle(Canvas canvas) {
RectF rect = new RectF((float) (centerX - mSectorRadio), centerY - mSectorRadio,
centerX + mSectorRadio, centerY + mSectorRadio);
int start = 0;
for (int i = 0; i < mSectorNum; i++) {
float angles = (mAgeLevelPercent[i] * 360);
mPaint.setColor(mAgeColors[i]);//mAgeColors.length:::5
canvas.drawArc(rect, start, angles, true, mPaint);
start += angles;
}
//顯示內圓
if (isShowInnerCircle) {
mPaint.setColor(mMinCircleColor);
canvas.drawCircle(centerX, centerY, mMinCircleRadio, mPaint);
}
}
/**
* 是否顯示裡面的圓
*
* @param isShowInnerCircle
*/
public void showInnerCircle(boolean isShowInnerCircle) {
this.isShowInnerCircle = isShowInnerCircle;
invalidate();
}
/***
* 設定裡面的圓的顏色
* @param color
*/
public void setInnerCircleColor(int color) {
mMinCircleColor = color;
invalidate();
}
/***
* 設定裡面的圓的半徑
* @param radio
*/
public void setInnerCircleRadio(float radio) {
mMinCircleRadio = radio;
invalidate();
}
/***
* 設定資料塊文字描述的字型大小
* @param textSize
*/
public void setDescTextSize(float textSize) {
mDescTextSize = textSize;
mTextPaint.setTextSize(mDescTextSize);
invalidate();
}
/***
* 設定描述文字的顏色
* @param color
*/
public void setDescTextColor(int color) {//todo:目前描述文字的顏色和扇形顏色是一致的
mDescTextColor = color;
mTextPaint.setColor(color);
invalidate();
}
/***
* 設定扇形的半徑大小
* @param mSectorRadio
*/
public void setSectorRadio(int mSectorRadio) {
this.mSectorRadio = mSectorRadio;
setDescTextSize(mSectorRadio / 6);
invalidate();
}
/***
* 設定資料
* @param entry
*/
public void setData(AgeEntry entry) {
int childTotalIn = entry.getChildTotalIn();
int youthTotalIn = entry.getYouthTotalIn();
int middleTotalIn = entry.getMiddleTotalIn();
int oldTotalIn = entry.getOldTotalIn();
Log.d(TAG, " childTotalIn::" + childTotalIn + " youthTotalIn::" + youthTotalIn + " middleTotalIn::" + middleTotalIn + " oldTotalIn::" + oldTotalIn);
int total = childTotalIn + youthTotalIn + middleTotalIn + oldTotalIn;
mAgeLevelPercent[0] = (float) childTotalIn / total;
mAgeLevelPercent[1] = (float)youthTotalIn / total;
mAgeLevelPercent[2] = (float)middleTotalIn / total;
mAgeLevelPercent[3] = (float)oldTotalIn / total;
mAgeDesc[0] = "未成年";
mAgeDesc[1] = "青年";
mAgeDesc[2] = "中年";
mAgeDesc[3] = "老年";
for (int i = 0; i < 4; i++) {
Log.d(TAG, "mAgeLevelPercent[" + i + "]= " + mAgeLevelPercent[i] + "\n" +
"mAgeDesc[" + i + "]= " + mAgeDesc[i]