三、具体实施
首先,说说思路一的实施过程:为了实现我思路一的做法,我先找到了能够显示并且设置数据的PieEntry,其构造方法如下几个:`
(1)
public PieEntry(float value, String label, Object data) {
super(0f, value, data);
this.label = label;
}`
(2)
public PieEntry(float value, String label) {
super(0f, value);
this.label = label;
}
其他几个我就不说了,先来说说这两个构造器的参数。value—理解为函数f(x,y)中的y值,label—理解为函数f(x,y)中的x值;其中第一个构造器的data是可以额外带的数据,在对图进行渲染的时候会得到,这里先不管。
在上面的基础上,我想写一个子类,继承PieEntry类,其构造器如下:
public PieEntry(float value, String label,boolean isVisible) {
super(0f, value);
this.label = label;
}
可以看到,这里多传了一个boolean类型的参数,这个参数的目的就在于去控制getValue()和getLabel()的返回值,PieEntry有以下方法:
public float getValue() { //获得value
return getY();
}
public String getLabel() { //获得label
return label;
}
我对这两个方法进行重写如下:
@Override
public float getValue() { //获得value
if(isVisible)
return super.getY();
else
return 0;
}
@Override
public String getLabel() { //获得label
if(isVisible)
return lable;
else
return "";
}
结果我运行后发现,该方法只对getLabel() 方法生效了,并没有对getValue()产生效果,也就是重写并没有执行。后来进去看源码才发现,原来底层进行渲染出图形的时候,获得value的值的方法就是getY(),也就是并没有调用getVaue()的方法,因此造成重写无效。后来想那就干脆重写全部PieEntry为MyPieEntry,在代码中添加值的时候也用MyPieEntry去添加,结果发现,有与底层都封装的是以PieEntry做为对象,因此,传入的参数检测非法,如果这样的话,要重写的东西就太多了,因此就没有继续研究这种方法了。(注:这种方法其实可以实现,后来意识到该重写哪了)
其次,是思路二的实现过程;上一条路没想通之后,就换了这条路,于是,我先在网上查到了相应的方法,PieChart用折线标注lable和Value,就是要设置以下两个属性:
dataSet.sPieDataSet dataSet = new PieDataSet(pieEntries,"");
etValueLinePart1Length(1f);//当值位置为外边线时,表示线的前半段长度。
dataSet.setValueLinePart2Length(0.4f);//当值位置为外边线时,表示线的后半段长度。
注释也说得很清楚了,就不再啰嗦了。
private float mValueLinePart1Length = 0.3f; //这两个是我们设置源码里面对应的变量,默认值分别为0.3和0.4
private float mValueLinePart2Length = 0.4f;
设置后,如下图(还是盗用上面的图_)
可以看到,label和value的值是上下显示的,因此还是会占据上下的空间,导致重合的可能性更大,因此现在要做的是,将lable和value的值在同一水平线上进行显示,这样,再适当的调小字体,好像关于重合的问题就能在一定程度上解决。因此经过一番折腾,具体细节我就不说可,直入主题吧,我找到了PieChartRenderer类,这就是对你添加的数据进行渲染的类,更直接的说就是,图形的显示就是实现的。因此,我继续看,终于找到了重点,源码如下:
@Override
public void drawValues(Canvas c) {
MPPointF center = mChart.getCenterCircleBox();
// get whole the radius
float radius = mChart.getRadius();
float rotationAngle = mChart.getRotationAngle();
float[] drawAngles = mChart.getDrawAngles();
float[] absoluteAngles = mChart.getAbsoluteAngles();
float phaseX = mAnimator.getPhaseX();
float phaseY = mAnimator.getPhaseY();
final float holeRadiusPercent = mChart.getHoleRadius() / 100.f;
float labelRadiusOffset = radius / 10f * 3.6f;
if (mChart.isDrawHoleEnabled()) {
labelRadiusOffset = (radius - (radius * holeRadiusPercent)) / 2f;
}
final float labelRadius = radius - labelRadiusOffset;
PieData data = mChart.getData();
List dataSets = data.getDataSets();
float yValueSum = data.getYValueSum();
boolean drawEntryLabels = mChart.isDrawEntryLabelsEnabled();
float angle;
int xIndex = 0;
c.save();
float offset = Utils.convertDpToPixel(5.f);
for (int i = 0; i < dataSets.size(); i++) {
IPieDataSet dataSet = dataSets.get(i);
final boolean drawValues = dataSet.isDrawValuesEnabled();
if (!drawValues && !drawEntryLabels)
continue;
final PieDataSet.ValuePosition xValuePosition = dataSet.getXValuePosition();
final PieDataSet.ValuePosition yValuePosition = dataSet.getYValuePosition();
// apply the text-styling defined by the DataSet
applyValueTextStyle(dataSet);
float lineHeight = Utils.calcTextHeight(mValuePaint, "Q")
+ Utils.convertDpToPixel(4f);
IValueFormatter formatter = dataSet.getValueFormatter();
int entryCount = dataSet.getEntryCount();
mValueLinePaint.setColor(dataSet.getValueLineColor());
mValueLinePaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getValueLineWidth()));
final float sliceSpace = getSliceSpace(dataSet);
for (int j = 0; j < entryCount; j++) {
PieEntry entry = dataSet.getEntryForIndex(j);
if (xIndex == 0)
angle = 0.f;
else
angle = absoluteAngles[xIndex - 1] * phaseX;
final float sliceAngle = drawAngles[xIndex];
final float sliceSpaceMiddleAngle = sliceSpace / (Utils.FDEG2RAD * labelRadius);
// offset needed to center the drawn text in the slice
final float angleOffset = (sliceAngle - sliceSpaceMiddleAngle / 2.f) / 2.f;
angle = angle + angleOffset;
final float transformedAngle = rotationAngle + angle * phaseY;
float value = mChart.isUsePercentValuesEnabled() ? entry.getY()
/ yValueSum * 100f : entry.getY();
final float sliceXBase = (float) Math.cos(transformedAngle * Utils.FDEG2RAD);
final float sliceYBase = (float) Math.sin(transformedAngle * Utils.FDEG2RAD);
final boolean drawXOutside = drawEntryLabels &&
xValuePosition == PieDataSet.ValuePosition.OUTSIDE_SLICE;
final boolean drawYOutside = drawValues &&
yValuePosition == PieDataSet.ValuePosition.OUTSIDE_SLICE;
final boolean drawXInside = drawEntryLabels &&
xValuePosition == PieDataSet.ValuePosition.INSIDE_SLICE;
final boolean drawYInside = drawValues &&
yValuePosition == PieDataSet.ValuePosition.INSIDE_SLICE;
if (drawXOutside || drawYOutside) {
final float valueLineLength1 = dataSet.getValueLinePart1Length();
final float valueLineLength2 = dataSet.getValueLinePart2Length();
final float valueLinePart1OffsetPercentage = dataSet.getValueLinePart1OffsetPercentage() / 100.f;
float pt2x, pt2y;
float labelPtx, labelPty;
float line1Radius;
if (mChart.isDrawHoleEnabled())
line1Radius = (radius - (radius * holeRadiusPercent))
* valueLinePart1OffsetPercentage
+ (radius * holeRadiusPercent);
else
line1Radius = radius * valueLinePart1OffsetPercentage;
final float polyline2Width = dataSet.isValueLineVariableLength()
? labelRadius * valueLineLength2 * (float) Math.abs(Math.sin(
transformedAngle * Utils.FDEG2RAD))
: labelRadius * valueLineLength2;
final float pt0x = line1Radius * sliceXBase + center.x;
final float pt0y = line1Radius * sliceYBase + center.y;
final float pt1x = labelRadius * (1 + valueLineLength1) * sliceXBase + center.x;
final float pt1y = labelRadius * (1 + valueLineLength1) * sliceYBase + center.y;
if (transformedAngle % 360.0 >= 90.0 && transformedAngle % 360.0 <= 270.0) {
pt2x = pt1x - polyline2Width;
pt2y = pt1y;
mValuePaint.setTextAlign(Align.RIGHT);
if(drawXOutside)
mEntryLabelsPaint.setTextAlign(Align.RIGHT);
labelPtx = pt2x - offset;
labelPty = pt2y;
} else {
pt2x = pt1x + polyline2Width;
pt2y = pt1y;
mValuePaint.setTextAlign(Align.LEFT);
if(drawXOutside)
mEntryLabelsPaint.setTextAlign(Align.LEFT);
labelPtx = pt2x + offset;
labelPty = pt2y;
}
if (dataSet.getValueLineColor() != ColorTemplate.COLOR_NONE) {
c.drawLine(pt0x, pt0y, pt1x, pt1y, mValueLinePaint);
c.drawLine(pt1x, pt1y, pt2x, pt2y, mValueLinePaint);
}
// draw everything, depending on settings
if (drawXOutside && drawYOutside) {
drawValue(c,
formatter,
value,
entry,
0,
labelPtx,
labelPty,
dataSet.getValueTextColor(j));
if (j < data.getEntryCount() && entry.getLabel() != null) {
drawEntryLabel(c, entry.getLabel(), labelPtx, labelPty + lineHeight);
}
} else if (drawXOutside) {
if (j < data.getEntryCount() && entry.getLabel() != null) {
drawEntryLabel(c, entry.getLabel(), labelPtx, labelPty + lineHeight / 2.f);
}
} else if (drawYOutside) {
drawValue(c, formatter, value, entry, 0, labelPtx, labelPty + lineHeight / 2.f, dataSet
.getValueTextColor(j));
}
}
if (drawXInside || drawYInside) {
// calculate the text position
float x = labelRadius * sliceXBase + center.x;
float y = labelRadius * sliceYBase + center.y;
mValuePaint.setTextAlign(Align.CENTER);
// draw everything, depending on settings
if (drawXInside && drawYInside) {
drawValue(c, formatter, value, entry, 0, x, y, dataSet.getValueTextColor(j));
if (j < data.getEntryCount() && entry.getLabel() != null) {
drawEntryLabel(c, entry.getLabel(), x, y + lineHeight);
}
} else if (drawXInside) {
if (j < data.getEntryCount() && entry.getLabel() != null) {
drawEntryLabel(c, entry.getLabel(), x, y + lineHeight / 2f);
}
} else if (drawYInside) {
drawValue(c, formatter, value, entry, 0, x, y + lineHeight / 2f, dataSet.getValueTextColor(j));
}
}
xIndex++;
}
}
MPPointF.recycleInstance(center);
c.restore();
}
简单地说,这个方法就是用来画出lable和value的,以及显示你设置的各种样式。继续看着一段代码:
if (drawXOutside && drawYOutside) {
drawValue(c,
formatter,
value,
entry,
0,
labelPtx,
labelPty,
dataSet.getValueTextColor(j));
if (j < data.getEntryCount() && entry.getLabel() != null) {
drawEntryLabel(c, entry.getLabel(), labelPtx, labelPty + lineHeight);
}
}
由于我们想将lable和value的值显示在外面,因此,会进入这个里面,从这的方法名我们就可以看出来,drawValue(),drawEntryLabel(),就是具体的实现。里面的详细在这里也不贴出来了,其实就是传入value和lable的值以及样式,用一个Paint对象画出来。现在呢据图来看下里面的参数,都有 labelPtx, labelPty,而且,drawEntryLable里面是labelPty + lineHeight,因此,这就好理解为什么lable与value是相对上下位置显示的了。其实这里的labelPtx, labelPty就是该控件在父控件中的位置x 为水平方向,y 为垂直方向。所以lable和value是垂直显示。
于是,我想让他们两个值同一水平上显示,就是x相差一个值,y相同就行了,于是我重写成如下:
drawValue(c,formatter,value,entry,0,labelPtx,labelPty, dataSet.getValueTextColor(j));
drawEntryLabel(c, entry.getLabel(), labelPtx - 50, labelPty);
加50的原因是考虑到value的值会占据水平的空间,因此隔开显示。显示结果如下:
看上去好像还不错,可是仔细看看有一个lable为什么压在折线上了,而且经过旋转之后,压在折线上的值会变化,经过一番研究,就说个大概吧:因为折线向左折还是向右u,取决于这个扇形图的角度,由于这个扇形图可以旋转,因此有个rotationAngle的值记录着当前的旋转角度,并且0度是水平方向的右边,折线是当前value显示区域扇形的角平分线,当这个折线的角度在0-90度的时候,向右折;在90-270度的时候向左折;在270-360度的时候又向右折,因此,可以看出,上面drawEntryLabel(c, entry.getLabel(), labelPtx - 50, labelPty)的重写肯定是不合适的,因为没有考虑到旋转以及方向,这是固定显示在左边的意思。一番摸索后,重写如下:
@Override
public void drawValues(Canvas c) {
MPPointF center = mChart.getCenterCircleBox();
// get whole the radius
float radius = mChart.getRadius();
float rotationAngle = mChart.getRotationAngle();
float[] drawAngles = mChart.getDrawAngles();
float[] absoluteAngles = mChart.getAbsoluteAngles();
float phaseX = mAnimator.getPhaseX();
float phaseY = mAnimator.getPhaseY();
final float holeRadiusPercent = mChart.getHoleRadius() / 100.f;
float labelRadiusOffset = radius / 10f * 3.6f;
if (mChart.isDrawHoleEnabled()) {
labelRadiusOffset = (radius - (radius * holeRadiusPercent)) / 2f;
}
final float labelRadius = radius - labelRadiusOffset;
PieData data = mChart.getData();
List dataSets = data.getDataSets();
float yValueSum = data.getYValueSum();
boolean drawEntryLabels = mChart.isDrawEntryLabelsEnabled();
float angle;
int xIndex = 0;
c.save();
float offset = Utils.convertDpToPixel(5.f);
for (int i = 0; i < dataSets.size(); i++) {
IPieDataSet dataSet = dataSets.get(i);
final boolean drawValues = dataSet.isDrawValuesEnabled();
if (!drawValues && !drawEntryLabels)
continue;
final PieDataSet.ValuePosition xValuePosition = dataSet.getXValuePosition();
final PieDataSet.ValuePosition yValuePosition = dataSet.getYValuePosition();
// apply the text-styling defined by the DataSet
applyValueTextStyle(dataSet);
float lineHeight = Utils.calcTextHeight(mValuePaint, "Q")
+ Utils.convertDpToPixel(4f);
IValueFormatter formatter = dataSet.getValueFormatter();
int entryCount = dataSet.getEntryCount();
mValueLinePaint.setColor(dataSet.getValueLineColor());
mValueLinePaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getValueLineWidth()));
final float sliceSpace = getSliceSpace(dataSet);
for (int j = 0; j < entryCount; j++) { //画出每一个扇形的区域
PieEntry entry = dataSet.getEntryForIndex(j); //entry即最开始说的PieEntry
if (xIndex == 0)
angle = 0.f;
else
angle = absoluteAngles[xIndex - 1] * phaseX;
final float sliceAngle = drawAngles[xIndex]; //每一个扇形区域的角度
Log.d("sliceAngle","sliceAngle-"+xIndex+":"+sliceAngle + "");
final float sliceSpaceMiddleAngle = sliceSpace / (Utils.FDEG2RAD * labelRadius);
// offset needed to center the drawn text in the slice
final float angleOffset = (sliceAngle - sliceSpaceMiddleAngle / 2.f) / 2.f;
angle = angle + angleOffset; //angle为每一块的所占比例的角度
Log.d("PieEntry",entry.getLabel() +":第"+ j+"个Entry的angle: " + angle);
final float transformedAngle = rotationAngle + angle * phaseY;
Log.d("1PieEntry_transformed",entry.getLabel() +":第"+ j+"个Entry的angle: " + rotationAngle);
float value = mChart.isUsePercentValuesEnabled() ? entry.getY()
/ yValueSum * 100f : entry.getY();
final float sliceXBase = (float) Math.cos(transformedAngle * Utils.FDEG2RAD);
final float sliceYBase = (float) Math.sin(transformedAngle * Utils.FDEG2RAD);
final boolean drawXOutside = drawEntryLabels &&
xValuePosition == PieDataSet.ValuePosition.OUTSIDE_SLICE;
final boolean drawYOutside = drawValues &&
yValuePosition == PieDataSet.ValuePosition.OUTSIDE_SLICE;
final boolean drawXInside = drawEntryLabels &&
xValuePosition == PieDataSet.ValuePosition.INSIDE_SLICE;
final boolean drawYInside = drawValues &&
yValuePosition == PieDataSet.ValuePosition.INSIDE_SLICE;
if (drawXOutside || drawYOutside) {
//TODO 不同的比例显示不同的长度
//final float valueLineLength1 = dataSet.getValueLinePart1Length();
final float valueLineLength2 = dataSet.getValueLinePart2Length();
//final float temp_valueLineLength1 = dataSet.getValueLinePart1Length()
//根据比例占比,使得折线的长度不同 ,比例越小,折线越长
final float valueLineLength1 = dataSet.getValueLinePart1Length() + 1 / entry.getValue() / entryCount;
Log.d("PieEntry_valueLength2","valueLineLength2:"+valueLineLength2);
final float valueLinePart1OffsetPercentage = dataSet.getValueLinePart1OffsetPercentage() / 100.f;
float pt2x, pt2y;
float labelPtx, labelPty;
float line1Radius;
if (mChart.isDrawHoleEnabled())
line1Radius = (radius - (radius * holeRadiusPercent))
* valueLinePart1OffsetPercentage
+ (radius * holeRadiusPercent);
else
line1Radius = radius * valueLinePart1OffsetPercentage;
final float polyline2Width = dataSet.isValueLineVariableLength()
? labelRadius * valueLineLength2 * (float) Math.abs(Math.sin(
transformedAngle * Utils.FDEG2RAD))
: labelRadius * valueLineLength2;
final float pt0x = line1Radius * sliceXBase + center.x;
final float pt0y = line1Radius * sliceYBase + center.y;
final float pt1x = labelRadius * (1 + valueLineLength1) * sliceXBase + center.x;
final float pt1y = labelRadius * (1 + valueLineLength1) * sliceYBase + center.y;
if (transformedAngle % 360.0 >= 90.0 && transformedAngle % 360.0 <= 270.0) {
pt2x = pt1x - polyline2Width;
pt2y = pt1y;
mValuePaint.setTextAlign(Paint.Align.RIGHT);
if(drawXOutside)
mEntryLabelsPaint.setTextAlign(Paint.Align.RIGHT);
labelPtx = pt2x - offset;
labelPty = pt2y;
} else {
pt2x = pt1x + polyline2Width;
pt2y = pt1y;
mValuePaint.setTextAlign(Paint.Align.LEFT);
if(drawXOutside)
mEntryLabelsPaint.setTextAlign(Paint.Align.LEFT);
labelPtx = pt2x + offset;
labelPty = pt2y;
}
if (dataSet.getValueLineColor() != ColorTemplate.COLOR_NONE) {
c.drawLine(pt0x, pt0y, pt1x, pt1y, mValueLinePaint);
c.drawLine(pt1x, pt1y, pt2x, pt2y, mValueLinePaint);
}
// draw everything, depending on settings
if (drawXOutside && drawYOutside) {
drawValue(c,
formatter,
value,
entry,
0,
labelPtx,
labelPty,
dataSet.getValueTextColor(j));
if (j < data.getEntryCount() && entry.getLabel() != null) {
Log.d("PieEntry",entry.getLabel() +":第"+ j+"个Entry的labelPtx: " + labelPtx);
Log.d("PieEntry",entry.getLabel() +":第"+ j+"个Entry的offset: " + offset);
//todo 要改
float tempAngle = 0;//当前扇形区域角平分线的角度(上面说了,水平右边的0度)
//是以又水平线为0度
//当块的角平分线的角度为:(旋转角+前面所有块的角度的和+自身角度的二分之一)对360度取余
for (int k = 0; k < (xIndex+1); k++) {
if(k == xIndex){ //是自身那块
tempAngle = tempAngle + drawAngles[k] / 2;
}else {//其他的就求和
tempAngle = tempAngle + drawAngles[k];
}
}
//得设置初始旋转角为0度,也就是以水平线的右边为0度
tempAngle = (tempAngle + rotationAngle) % 360;
Log.d("tempAngle",j +":"+tempAngle +"");
if((tempAngle > 0 && tempAngle < 90) || (tempAngle > 270 && tempAngle < 360))//文字在线的右边
drawEntryLabel(c, entry.getLabel(), labelPtx + 45, labelPty);
else //文字在线的左边
drawEntryLabel(c, entry.getLabel(), labelPtx - 45, labelPty);
//drawEntryLabel(c, entry.getLabel(), labelPtx, labelPty + lineHeight);
}
} else if (drawXOutside) {
if (j < data.getEntryCount() && entry.getLabel() != null) {
drawEntryLabel(c, entry.getLabel(), labelPtx, labelPty + lineHeight / 2.f);
}
} else if (drawYOutside) {
drawValue(c, formatter, value, entry, 0, labelPtx, labelPty + lineHeight / 2.f, dataSet
.getValueTextColor(j));
}
}
if (drawXInside || drawYInside) {
// calculate the text position
float x = labelRadius * sliceXBase + center.x;
float y = labelRadius * sliceYBase + center.y;
mValuePaint.setTextAlign(Paint.Align.CENTER);
// draw everything, depending on settings
if (drawXInside && drawYInside) {
drawValue(c, formatter, value, entry, 0, x, y, dataSet.getValueTextColor(j));
if (j < data.getEntryCount() && entry.getLabel() != null) {
drawEntryLabel(c, entry.getLabel(), x, y + lineHeight);
}
} else if (drawXInside) {
if (j < data.getEntryCount() && entry.getLabel() != null) {
drawEntryLabel(c, entry.getLabel(), x, y + lineHeight / 2f);
}
} else if (drawYInside) {
drawValue(c, formatter, value, entry, 0, x, y + lineHeight / 2f, dataSet.getValueTextColor(j));
}
}
xIndex++;
}
}
MPPointF.recycleInstance(center);
c.restore();
}
上面改动了的以及需要用的我都做了注释,主要改动有两个,一个是根据我重新创建的float tempAngle = 0;去记录当前扇形区域的角平分线的角度,然后再判断drawEntryLabel中的x参数是应该加还是减,也就是显示在value的左边还是右边;第二个改动是做了一点优化:
final float valueLineLength1 = dataSet.getValueLinePart1Length() + 1 / entry.getValue() / entryCount;
这里根据每个扇形区域的比例不一致,动态的设置了折线的长度,比例越小,折线越长,本来打算用tan值来设置,但是角度太小导致太长了,就稍稍加长了一点。对了,别忘了要设置初始的旋转角为o度,如果不设置的话,默认为270度
pieChart.setRotationAngle(0); //设置初始旋转角为0度
显示效果如下:
可以看出,lable和value的值呈水平方向显示,并且根据折线的方向显示在左边或者右边,同时,细心的还可以发现,比例越小的扇形区域的折线的长度越长。这样也就在一定程度上解决了重合的问题了,当然,如果扇形区域很小很小,还是有可能重合。当然,此时就设置需求啦,例如我们把比例小到一定程度的数据不显示。
到这里也就完成了我思路二想完成的,具体的参数大小,大家自己去设置。
再来说说思路一的当时走到死胡同后该怎么解决,为什么重写PieEntry的getValue无效,其实,细心的看我上面粘贴的源码就知道了:
float value = mChart.isUsePercentValuesEnabled() ? entry.getY() / yValueSum * 100f : entry.getY();
这就很明显了,它调用的是entry.getY(),而并不是entry.getValue(),因此重写才没有用。所以解决方法就是两种,一种是重写getY()方法,二是在PieChartRenderer类中DrawValue()方法里面,把getY()方法改成getValue(),这样就能使我们的重写有效了。具体实施你们自己看着办吧。
情况就是这么个情况,感觉收获挺多的,通过源码了解到了很多,也还是佩服能写出这些开源控件的大神们,瞻仰瞻仰!!!
有问题的,欢迎交流。