前言:繼上次寫了自定義圓形進度條後,今天給大家帶來自定義扇形餅狀圖。先上效果圖:
是不是很炫?看上去還有點立體感。下面帶大家一起來瞧一瞧吧。
一、定義成員變量,重寫構造方法
看着這個效果圖,我們可以想象下接下來暫時會需要用到以下屬性:
/**
* 存放事物的品種與其對應的數量
*/
private Map kindsMap = new LinkedHashMap<String, Integer>();
/**
* 存放顏色
*/
private ArrayList<Integer> colors = new ArrayList<>();
private Paint mPaint;//餅狀畫筆
private Paint mTextPaint; // 文字畫筆
private static final int DEFAULT_RADIUS = 200;
private int mRadius = DEFAULT_RADIUS; //外圓的半徑
private String centerTitle; //中間標題
然後重寫父類的構造方法,初始化畫筆:
public PieChatView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mTextPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
mTextPaint.setColor(Color.BLACK);
mTextPaint.setAntiAlias(true);
mTextPaint.setStyle(Paint.Style.STROKE);
mTextPaint.setTextAlign(Paint.Align.CENTER);
}
public PieChatView(Context context) {
this(context, null, 0);
}
public PieChatView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
一般都是一個參數和兩個參數的全部調用第三個參數的。我三個參數的構造方法中沒有去從xml文件中去獲取屬性了。我完全就用代碼實現了。也可以將一些屬性放在attrs.xml文件中,然後去獲取。自行選擇吧。
二、onMeasure()
這個方法,只要你以前寫過自定義View,基本上就是一樣的套路:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wideSize = MeasureSpec.getSize(widthMeasureSpec);
int wideMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int width, height;
if (wideMode == MeasureSpec.EXACTLY) { //精確值 或matchParent
width = wideSize;
} else {
width = mRadius * 2 + getPaddingLeft() + getPaddingRight();
if (wideMode == MeasureSpec.AT_MOST) {
width = Math.min(width, wideSize);
}
}
if (heightMode == MeasureSpec.EXACTLY) { //精確值 或matchParent
height = heightSize;
} else {
height = mRadius * 2 + getPaddingTop() + getPaddingBottom();
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(height, heightSize);
}
}
setMeasuredDimension(width, height);
mRadius = (int) (Math.min(width - getPaddingLeft() - getPaddingRight(),
height - getPaddingTop() - getPaddingBottom()) * 1.0f / 2);
}
就是獲得系統測量好的寬高,和模式後分三種模式去討論。最終通過setMeasuredimension()確定寬高的值。寬高確定好了,那就可以確定下整個餅狀圖的半徑 mRadius了。取寬高中的較小的那個。
三、onDraw();
我們需要畫一個一個的扇形,還有將扇形從零到360的動畫效果,還有扇形中的文字,中間的文字,還有實現立體感的效果。
1.畫扇形。
畫一個扇形還是蠻容易的:通過畫布調用drawArc()方法話一個60度的扇形:
mPaint.setStyle(Paint.Style.FILL);
mTextPaint.setColor(Color.RED);
RectF oval = new RectF(-mRadius, -mRadius, mRadius, mRadius);
mCanvas.drawArc(oval, 0, 60, true, mPaint);
前面設置下畫筆的顏色,style爲實心,就這幾行代碼的事情。第一個參數:是一個正方形,表明在這個區域內畫扇形。第二個參數:從哪個角度開始畫,第三個參數:就是畫的角度度數,而不是畫到哪個角度去。第四個參數:是否以正方形中心爲圓心。我這樣一說,你可能就會有點懵逼了。你自己改爲false,看效果,就理解我說的了。效果如下:
目前畫了一個扇形,如此多的扇形需要用戶去設置數據,來確定每個扇形的角度.
public ArrayList<Integer> getColors() {
return colors;
}
public void setColors(ArrayList<Integer> colors) {
this.colors = colors;
}
public void setDataMap(LinkedHashMap<String, Integer> map) {
this.kindsMap = map;
}
public String getCenterTitle() {
return centerTitle;
}
public void setCenterTitle(String centerTitle) {
this.centerTitle = centerTitle;
}
然後通過遍歷,一 一畫出這些扇形:
@Override
protected void onDraw(Canvas mCanvas) {
super.onDraw(mCanvas);
mCanvas.translate((getWidth() + getPaddingLeft() - getPaddingRight()) / 2, (getHeight() + getPaddingTop() - getPaddingBottom()) / 2);
paintPie(mCanvas);
}
private void paintPie(final Canvas mCanvas) {
if (kindsMap != null) {
Set<Map.Entry<String, Integer>> entrySet = kindsMap.entrySet();
Iterator<Map.Entry<String, Integer>> iterator = entrySet.iterator();
int i = 0;
float currentAngle = 0.0f;
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
int num = entry.getValue();
float needDrawAngle = num * 1.0f / sum * 360;
mPaint.setColor(colors.get(i));
mCanvas.drawArc(oval, currentAngle, needDrawAngle - 1, true, mPaint);
currentAngle = currentAngle + needDrawAngle;
i++;
}
}
}
記得,要先將畫筆移到畫布中心,currentAngle:當前的角度值,needDrawAngle :需要畫多少角度。sum:數據的總個數,爲每一個數據相加的和,計算百分比。效果如圖:
2、動畫效果實現
咋一看離我們的預期效果還差好遠。只有畫完以後的樣子,動畫都沒有。別急別急,動畫馬上就來:
private void initAnimator() {
ValueAnimator anim = ValueAnimator.ofFloat(0, 360);
anim.setDuration(10000);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
animatedValue = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
anim.start();
}
設置一個動畫,在10秒內,隨着時間的推移,animatedValue從0到360度發生變化,每一次監聽到animatedValue的值得改變,就去刷新View,執行ondraw();這個時候我們來修改下前面的paintPie();
if (Math.min(needDrawAngle-1, animatedValue - currentAngle) >= 0) {
mPaint.setColor(colors.get(i));
mCanvas.drawArc(oval, currentAngle, Math.min(needDrawAngle - 1, animatedValue - currentAngle), true, mPaint);
}
先看效果:
我在這個地方遇到了一個坑----ondraw()方法多次執行時,每一次執行完畢後會抹去上一次畫過的圖形.也就是說,我沒用動畫的時候,ondraw()方法就調用了一次,調用了while循環後,把所有的扇形畫出來了。用了動畫之後,animatedValue的值一改變,就會執行一次while循環。每次while循環都會重頭開始畫,我們只要保證每次while循環所畫的角度正好是animatedValue,就ok了。所以,在needDrawAngle-1小於animatedValue - currentAngle時,就把該部分扇形畫出來。在needDrawAngle-1大於animatedValue - currentAngle時,那就只畫animatedValue - currentAngle的角度。
之所以減一,是因爲扇形之間留一條白白的縫隙。
3.添加文字,中間標題,實現立體感
當某個扇形角度,不夠文字的寬度時,我們就會將文字畫在圓的外面。所以這個我們得添加一個屬性minAngle,角度最小值,添加getter和setter方法,讓使用者去設置。當needDrawAngle小於這個角度值時,就畫在外面。大於這個值就畫在扇形中央。
//畫文字
private void drawText(Canvas mCanvas, float textAngle, String kinds, float needDrawAngle) {
Rect rect = new Rect();
mTextPaint.setTextSize(sp2px(15));
mTextPaint.getTextBounds(kinds, 0, kinds.length(), rect);
if (textAngle >= 0 && textAngle <= 90) { //畫布座標系第一象限(數學座標系第四象限)
if (needDrawAngle < minAngle) { //如果小於某個度數,就把文字畫在餅狀圖外面
mCanvas.drawText(kinds, (float) (mRadius * 1.2 * Math.cos(Math.toRadians(textAngle))), (float) (mRadius * 1.2 * Math.sin(Math.toRadians(textAngle)))+rect.height()/2, mTextPaint);
} else {
mCanvas.drawText(kinds, (float) (mRadius * 0.75 * Math.cos(Math.toRadians(textAngle))), (float) (mRadius * 0.75 * Math.sin(Math.toRadians(textAngle)))+rect.height()/2, mTextPaint);
}
} else if (textAngle > 90 && textAngle <= 180) { //畫布座標系第二象限(數學座標系第三象限)
if (needDrawAngle < minAngle) {
mCanvas.drawText(kinds, (float) (-mRadius * 1.2 * Math.cos(Math.toRadians(180 - textAngle))), (float) (mRadius * 1.2 * Math.sin(Math.toRadians(180 - textAngle)))+rect.height()/2, mTextPaint);
} else {
mCanvas.drawText(kinds, (float) (-mRadius * 0.75 * Math.cos(Math.toRadians(180 - textAngle))), (float) (mRadius * 0.75 * Math.sin(Math.toRadians(180 - textAngle)))+rect.height()/2, mTextPaint);
}
} else if (textAngle > 180 && textAngle <= 270) { //畫布座標系第三象限(數學座標系第二象限)
if (needDrawAngle < minAngle) {
mCanvas.drawText(kinds, (float) (-mRadius * 1.2 * Math.cos(Math.toRadians(textAngle - 180))), (float) (-mRadius * 1.2 * Math.sin(Math.toRadians(textAngle - 180)))+rect.height()/2, mTextPaint);
} else {
mCanvas.drawText(kinds, (float) (-mRadius * 0.75 * Math.cos(Math.toRadians(textAngle - 180))), (float) (-mRadius * 0.75 * Math.sin(Math.toRadians(textAngle - 180)))+rect.height()/2, mTextPaint);
}
} else { //畫布座標系第四象限(數學座標系第一象限)
if (needDrawAngle < minAngle) {
mCanvas.drawText(kinds, (float) (mRadius * 1.2 * Math.cos(Math.toRadians(360 - textAngle))), (float) (-mRadius * 1.2 * Math.sin(Math.toRadians(360 - textAngle)))+rect.height()/2, mTextPaint);
} else {
mCanvas.drawText(kinds, (float) (mRadius * 0.75 * Math.cos(Math.toRadians(360 - textAngle))), (float) (-mRadius * 0.75 * Math.sin(Math.toRadians(360 - textAngle)))+rect.height()/2, mTextPaint);
}
}
}
首先,畫文字,我們要知道文字的中心點座標。所以怎麼來求呢?看圖:
通過這幅圖,大家應該知道了吧。唯一一點要說的就是畫圖的座標系和數學中的座標系不一樣。畫的時候還要分四個象限的情況去討論。有的人說,爲什麼不是在1/2r處,而是在0.75r處呢?因爲我們中心還要畫title的呀,對吧。畫在1/2處,不就覆蓋了嘛!
然後在中心畫文字咯。文字很容易。先畫個白色背景的圓半徑爲外圓半徑的一半,這樣就覆蓋了扇形中間的一部分。在內圓上再畫字。就OK了。
那立體效果怎麼實現呢?
也很簡單,可以仔細觀察,那部分好像是透明的。隱約能看見前面畫的扇形。所以我們在畫內園之前先畫個比內園稍微大一點的透明圓。設置畫筆的透明度就搞定。上代碼:
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
String kinds = entry.getKey();
int num = entry.getValue();
float needDrawAngle = num * 1.0f / sum * 360;
String drawAngle = dff.format(needDrawAngle / 360 * 100);
kinds = kinds + "," + drawAngle + "%";
float textAngle = needDrawAngle / 2 + currentAngle;
if (Math.min(needDrawAngle, animatedValue - currentAngle) >= 0) {
mPaint.setColor(colors.get(i));
mCanvas.drawArc(oval, currentAngle, Math.min(needDrawAngle - 1, animatedValue - currentAngle), true, mPaint);
mPaint.setColor(Color.WHITE);
mPaint.setAlpha(10);
mCanvas.drawCircle(0, 0, mRadius / 2 + dp2px(10), mPaint);
mPaint.setAlpha(255);
mCanvas.drawCircle(0, 0, mRadius / 2, mPaint);
drawCenterText(mCanvas, centerTitle, 0, 0, mTextPaint);
drawText(mCanvas, textAngle, kinds, needDrawAngle);
}
currentAngle = currentAngle + needDrawAngle;
i++;
}
該畫的畫完了。接下來就是一些優化和修改了。
四、優化
1.添加dp,sp,px轉化工具
/**
* dp 2 px
*
* @param dpVal
*/
protected int dp2px(int dpVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dpVal, getResources().getDisplayMetrics());
}
/**
* sp 2 px
*
* @param spVal
* @return
*/
protected int sp2px(int spVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
spVal, getResources().getDisplayMetrics());
}
因爲圖中有很多地方接觸到文字,如果設置文字的時候用的是px,那麼在不同的機器上,顯示的文字大小將會差距很大。最好也提供一些方法讓使用者去設置這些字體的大小。
2.讓使用者去開啓動畫
因爲數據是使用者設置上去的,什麼時候開啓動畫,還得提供一個公開方法:
public void startDraw() {
if (kindsMap != null && colors != null && centerTitle != null) {
initAnimator();
}
}
3.設置數據優化
可以知道,我用了兩個容器來裝數據。有沒有感覺到浪費資源?其實我們完全可以只用一個ArrayList即可。對吧。將所有數據定義爲一個實體的bean類。說到這,又遇到一個坑,我最開始使用hashMap去存數據的,優越hashMap存放數據是無序的,所以每次畫出來的扇形結構不一樣。你也許又會問我,怎麼顏色總是飄忽不定啊?因爲我是用了個for循環隨機生成的顏色,哈哈。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
PieChatView pieChatView = (PieChatView) findViewById(R.id.pie);
kindsMap.put("蘋果", 10);
kindsMap.put("梨子", 30);
kindsMap.put("香蕉", 10);
kindsMap.put("葡萄", 30);
kindsMap.put("哈密瓜", 10);
kindsMap.put("獼猴桃",30);
kindsMap.put("草莓", 10);
kindsMap.put("橙子", 30);
kindsMap.put("火龍果", 10);
kindsMap.put("椰子", 20);
for (int i = 1; i <= 40; i++){
int r= (new Random().nextInt(100)+10)*i;
int g= (new Random().nextInt(100)+10)*3*i;
int b= (new Random().nextInt(100)+10)*2*i;
int color = Color.rgb(r,g,b);
if(Math.abs(r-g)>10&&Math.abs(r-b)>10&&Math.abs(b-g)>10){
colors.add(color);
}
}
pieChatView.setCenterTitle("水果大拼盤");
pieChatView.setDataMap(kindsMap);
pieChatView.setColors(colors);
pieChatView.setMinAngle(50);
pieChatView.startDraw();
到這裏,文章就結束了。最後附上代碼的github地址:
https://github.com/Demidong/ClockView
我寫的博客還不是很多,歡迎大家參與討論,留言,指正我的不足。