上週接到產品經理的需求,要求做一個餅圖,以作爲會員的數據統計,原型如下圖:
原型看起來不算複雜,但是作爲多年開發經驗的我,還是有點懶的,於是乎就去GitHub上找相關項目,找到了幾個項目,但做的餅圖跟我們產品經理做的原型圖還是有一定的差別的。按理說改吧改吧就好了,只是去年我幫一個朋友做了一個折線圖(純自己自定義控件),該項目demo已經上傳到GitHub,地址https://github.com/huangxuanheng/brokenLine,覺得很多點和比例都需要計算,如果想要修改一點,就得弄清楚裏面的點線座標以及計算比例關係,很麻煩。與其去修改別人做好的餅圖,再弄懂一些計算關係,還不如自己寫一個自定義控件的餅圖,這樣所有的關係都可以清晰明瞭的弄清楚,並且以後想改動什麼地方,都會非常的方便
好的,開始玩轉自定義餅圖控件
先來分析原型圖
1.首先,這個餅圖是由兩個不同半徑的同心圓組成,我們可以定義一個座標軸,以cx,cy爲兩同心圓的圓心,radius爲大圓半徑,sRadius爲小圓半徑,繪製兩個同心圓
2.數據部分的體現,是大圓-小圓的部分,而數據塊,即數據所佔百分比部分,是以爲cx,cy圓心,從一個起點startAngle角度開始,以sweepAngle爲角速度旋轉的扇形塊,大圓扇形塊-小圓扇形塊=數據塊
3.數據塊對應數據顯示,是以對應直線,以斜率k=1或者k=-1,與大圓相交爲轉折點,延伸一段距離d後,取一個點,再以斜率k=0的直線相交,往圓心相反方向延伸,根據數據文本的長度來取該線段的長度l
作圖如下:
圖畫的有點難看,能看就好
有了這些,就可以開始繪製了
步驟:
1.定義並初始化畫筆
2.根據同心圓和相關圓半徑等數據,計算出扇形的繪製位置以及開始繪製角度startAngle和角速度sweepAngle,繪製扇形
3.通過扇形的startAngle和繪製折線角速度sweepAngle,計算出扇形的大圓中間點,繪製線段d、l,繪製文字說明
4.繪製同心圓
1.定義並初始化畫筆
public class PieChartView extends View {
Paint bCiclePaint; //繪製大圓的畫筆
Paint sCiclePaint; //繪製小圓的畫筆
Paint ringPaint; //繪製環的畫筆
Paint brokenLinePaint; //繪製折線的畫筆
Paint textPaint; //繪製文字的畫筆
float radius; //大圓半徑
int cx; //圓心x
int cy; //圓心y
float total; //總量,需要分割成爲餅圖的所有數據總數
boolean isStartAngle; //是否指定開始繪製第一個餅圖塊的角度
float startAngle; //開始繪製第一個餅圖塊的角度
int maxAngle=360; //圓的最大角度
public PieChartView(Context context) {
this(context,null);
}
public PieChartView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public PieChartView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
}
private void initPaint() {
bCiclePaint =new Paint();
bCiclePaint.setAntiAlias(true);
bCiclePaint.setStyle(Paint.Style.STROKE);//設置空心
bCiclePaint.setColor(Color.GRAY);
sCiclePaint=new Paint();
sCiclePaint.setStyle(Paint.Style.FILL);//設置空心
sCiclePaint.setAntiAlias(true);
sCiclePaint.setColor(Color.WHITE);
ringPaint=new Paint();
ringPaint.setAntiAlias(true);
ringPaint.setStyle(Paint.Style.FILL);
brokenLinePaint=new Paint();
brokenLinePaint.setAntiAlias(true);
brokenLinePaint.setColor(Color.GRAY);
textPaint=new Paint();
textPaint.setAntiAlias(true);
textPaint.setColor(Color.GRAY);
textPaint.setTextSize(UIUtils.getDimens(R.dimen.sp13));
}
}
//bean
public class PieChart {
private int id;
private String name; //文字說明
private float proport; //文字說明所佔比例
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getProport() {
return proport;
}
public void setProport(float proport) {
this.proport = proport;
}
@Override
public String toString() {
return "PieChart{" +
"id=" + id +
", name='" + name + '\'' +
", proport=" + proport +
'}';
}
//子類可以通過重寫該方法,返回顯示在餅圖的數量說明文字
public String getProportStr(){
return ((int)proport)+"人";
}
}
2.根據同心圓和相關圓半徑等數據,計算出扇形的繪製位置以及開始繪製角度startAngle和角速度sweepAngle,繪製扇形
3.通過扇形的startAngle和繪製折線角速度sweepAngle,計算出扇形的大圓中間點,繪製線段d、l,繪製文字說明
Map<Integer,PieChart>pieChartMap=new HashMap<>();
private void init() {
radius=getWidth()/5;
cx =getWidth()/2;
// cy =getPaddingTop()+getWidth()/3;
cy =getPaddingTop()+getHeight()/3;
LogUtil.i(this,"cx="+cx+",cy="+cy);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
init();
drawArc(canvas);
}
//繪製扇形區域
private void drawArc(Canvas canvas) {
if(pieChartMap.isEmpty()){
return;
}
RectF oval1=new RectF(cx-radius,cy-radius,cx+radius,cy+radius);
startAngle = getStartAngle();
float ss=0; //無用,數據用來打印測試的
for(Map.Entry<Integer,PieChart>entry:pieChartMap.entrySet()){
PieChart value = entry.getValue();
float sweepAngles = value.getProport() * maxAngle / total;
ss+=sweepAngles;
canvas.drawArc(oval1, startAngle, sweepAngles, true, getRingPaint());//小弧形
LogUtil.i(this,"startAngle="+startAngle+",sweepAngles="+sweepAngles);
//繪製折線,寫數據
drawBrokenLine(canvas,startAngle,sweepAngles,maxAngle,value);
startAngle+=sweepAngles;
}
LogUtil.i(this,"ss="+ss);
}
private void drawBrokenLine(Canvas canvas, float startAngle, float sweepAngles,float maxAngle,PieChart entry) {
int k=1; //與圓相交直線的斜率
float d=100; //與圓相交的折線長度
float l; //線段l的長度,與d相交的折線長度,上下顯示文字
//扇形開始角度+角速度的二分之一,即是畫折線的起點處
float Q= (float) ((startAngle+sweepAngles/2)*Math.PI*2/maxAngle);
float sin= (float) Math.sin(Q);
float cos= (float) Math.cos(Q);
if(sin>0&&cos>0){
//第一象限
k=1;
}else if(sin>0&&cos<0){
//第二象限
k=-1;
}else if(sin<0&&cos<0){
//第三象限
k=1;
}else if(sin<0&&cos>0){
//第四象限
k=-1;
}
float x1= radius*cos+cx;
float y1=radius*sin+cy;
LogUtil.i(this,"cos="+cos+"sin="+sin+",radius*cos="+(radius*cos)+",radius*sin="+(radius*sin)+",x1="+x1+",y1="+y1);
//y=kx+b
float b=y1-k*x1;
//兩點間距離公式計算出一個點
//根號b的平方-4ac
float a= (float) (1+Math.pow(k, 2));
//相當於一元二次方法中的b
float c= k*b - x1 - k*y1;
float sqrt = (float) Math.sqrt(Math.pow(c, 2) - a*(Math.pow(x1, 2) + Math.pow(b, 2) + Math.pow(y1, 2) - 2 * b * y1 - Math.pow(d, 2)));
float x= (-c+sqrt)/a;
float lx=0; //線段l的端點
float ly; //線段l的端點
String name = entry.getName();
String proport = entry.getProportStr();
Rect rectName=new Rect();
//獲取文字的長度
textPaint.getTextBounds(name,0,name.length(),rectName);
Rect rectProport=new Rect();
textPaint.getTextBounds(proport,0,proport.length(),rectProport);
int widthName = rectName.width();
int widthProport = rectProport.width();
l=Math.max(widthName,widthProport);
if(sin>0&&cos>0){
//第一象限
if(x<x1){
//在圓內
x=(-c-sqrt)/a;
}
lx=x+l;
}else if(sin>0&&cos<0){
//第二象限
if(x>x1){
//在圓內
x=(-c-sqrt)/a;
}
lx=x-l;
}else if(sin<0&&cos<0){
//第三象限
if(x>x1){
//在圓內
x=(-c-sqrt)/a;
}
lx=x-l;
}else if(sin<0&&cos>0){
//第四象限
if(x<x1){
//在圓內
x=(-c-sqrt)/a;
}
lx=x+l;
}
float y=k*x+b;
ly=y;
LogUtil.i(this,"x="+x+",y="+y);
drawBrokenLine(canvas, x1, y1, x, y, lx, ly);
drawText(canvas, l, x, y, lx, ly, name, proport, widthName, widthProport,rectProport.height());
}
/**
* 繪製折線,與圓相交,拐出來顯示文字說明
* @param canvas
* @param x1 折線的端點x,與圓相交
* @param y1 折線的端點y,與圓相交
* @param x 折線d的端點x,與線段l相交的點
* @param y 折線d的端點y,與線段l相交的點
* @param lx 折線的端點x
* @param ly 折線的端點y
*/
private void drawBrokenLine(Canvas canvas, float x1, float y1, float x, float y, float lx, float ly) {
//繪製折線與圓相交
canvas.drawLine(x1,y1,x,y,brokenLinePaint);
//繪製折線l與d相交
canvas.drawLine(lx,ly,x,y,brokenLinePaint);
}
/**
* 繪製比例說明文字
* @param canvas
* @param l 線段l文字長度,或者說是線段l的長度
* @param x 折線的拐點x
* @param y 折線的拐點y
* @param lx 折線的端點x
* @param ly 折線的端點y
* @param name 說明文字
* @param proport 說明文字的比例,顯示在線段l下方
* @param widthName name的長度
* @param widthProport proport的長度
* @param dy 繪製文字的偏移量,距離折線的長度
*/
private void drawText(Canvas canvas, float l, float x, float y,
float lx, float ly, String name, String proport,
int widthName, int widthProport,int dy) {
// int dy=10; //繪製文字的偏移量,距離折線的長度
float tranlateName=(l-widthName)/2.0f;
float tranlateProport=(l-widthProport)/2.0f;
if(x<cx){
canvas.drawText(name,lx+tranlateName,ly-dy/2,textPaint);
canvas.drawText(proport,lx+tranlateProport,ly+dy,textPaint);
}else {
canvas.drawText(name,x+tranlateName,y-dy/2,textPaint);
canvas.drawText(proport,x+tranlateProport,y+dy,textPaint);
}
}
private Paint getRingPaint(){
Paint ringPaint=new Paint();
ringPaint.setAntiAlias(true);
ringPaint.setStyle(Paint.Style.FILL);
ringPaint.setColor(getRandomColor());
return ringPaint;
}
private int getRandomColor(){
Random random=new Random();
int r = 10+random.nextInt(200);
int g = 10+random.nextInt(200);
int b = 10+random.nextInt(200);
int rgb = Color.rgb(r,g,b);
return rgb;
}
4.繪製同心圓
//繪製圓
private void drawCircle(Canvas canvas) {
float sRadius=radius-radius/4;
LogUtil.i(this,"sRadius="+sRadius+",radius="+radius);
//1.繪製大圓
canvas.drawCircle(cx, cy,radius, bCiclePaint);
//2.繪製小圓
canvas.drawCircle(cx, cy,sRadius, sCiclePaint);
}
到這裏,基本的餅圖繪製就完成了
佈局代碼:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main"
android:layout_width="match_parent" android:layout_height="match_parent"
>
<com.ishow.huiyuantest.widget.PieChartView
android:id="@+id/pv"
android:layout_below="@id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</RelativeLayout>
activity代碼:
public class MainActivity extends AppCompatActivity {
@Bind(R.id.pv)
PieChartView pv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UIUtils.registerApplication(getApplication());
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
initPieChart();
}
private void initPieChart() {
List<PieChart> pieChartList=new ArrayList<>();
PieChart pc=new PieChart();
pc.setId(1);
pc.setName("銀卡會員");
pc.setProport(35);
pieChartList.add(pc);
pc=new PieChart();
pc.setId(2);
pc.setName("金卡會員");
pc.setProport(10);
pieChartList.add(pc);
pc=new PieChart();
pc.setId(3);
pc.setName("銅卡會員");
pc.setProport(95);
pieChartList.add(pc);
pc=new PieChart();
pc.setId(4);
pc.setName("普通會員");
pc.setProport(300);
pieChartList.add(pc);
pv.addPieCharData(pieChartList);
}
}
看一看運行效果:
效果還是不錯的
其實很多看起來複雜的控件,其實也並不是非常的複雜,只要我們把一些數學關係理清楚了,一切都變得簡單起來