通常app中可能的数据展示控件有柱状图,折线图,饼状图等,如果需要一个包含多种View控件的库,那么 MPAndroidChart 是不错的选择,如果只是需要一个简单的独立的饼状图控件,希望RingView满足你的要求。
控件介绍
效果图如下

控件功能
展示一组数据 绘制圆环,展示对应模块文本信息, 点击对应模块进行放大处理
实现过程
绘制圆环
圆环的基本绘制
圆环的绘制实际就是通过先后绘制两个半径不同的圆实现,圆就是360度的扇形,canvas.drawArc提供了这个功能:
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
@NonNull Paint paint)
需要先绘制有颜色的外圆对应的各个扇形,之后再“覆盖”绘制内圆对应的各个扇形。
绘制圆环的时候需要考虑开始角度mStartAngle和当前的旋转mRotation。这里设计了一个方法drawPieFromEnd用来在(start, end)的角度范围内绘制“被显示”的那些扇形。这里的角度是扇形数组的形成的0-360的连续角度范围。
为了绘制的简单,方法选择从最后一个扇形开始绘制,相当于从end绘制到start,这样的好处是不用去计算实际上start对应的是哪个扇形了,而根据传递的角度范围,当下一个绘制的扇形的起始角度大于start时,结束绘制:
/**
* 从尾部开始绘制圆环,只绘制endAngle到startAngle之间的,不一定绘制所有圆环。
*
* @param canvas
* @param startAngle
* @param endAngle
*/
private void drawPieFromEnd(Canvas canvas, float startAngle, float endAngle1) {
if (angles == null) return;
for (int i = angles.length - 1; i >= 0; i--) {
float itemAngle = angles[i] + 0.5f;
float sweepStart = endAngle1 - itemAngle;
mPaint.setColor((colorList.get(i)));
if (sweepStart >= startAngle) {
canvas.drawArc(rectF, sweepStart, itemAngle, true, mPaint);
} else {
itemAngle = endAngle1 - startAngle;
canvas.drawArc(rectF, startAngle, itemAngle, true, mPaint);
break;
}
endAngle1 -= itemAngle;
}
}
动画 当前控件交互过程中总共有一个动画:开始时候绘制动画 showOut
所有动画通过Animation实现,这里只是使用Animation完成动画时间和进度的控制。 重写applyTransformation方法来记录当前动画的进度progress,然后invalidate通知onDraw的执行。 开始动画执行时将当前动画模式字段int mAnimMode设置为不同的ANIM_MODE_xxx常量,然后onDraw中会根据当前的mAnimMode值,选择对应动画的绘制方法去执行。
代码结构如下:
public class PieGraphView extends View {
private static final int ANIM_MODE_NONE = 0;
private static final int ANIM_MODE_ROTATE = 1;
...
private void initAnims() {
mAnimShowOut = new Animation() {
@Override
protected void applyTransformation(final float interpolatedTime, final Transformation t) {
mShowOutProgress = interpolatedTime;
invalidate();
if (interpolatedTime >= 1.0f) {
cancel();
mAnimMode = ANIM_MODE_FINISHED;
// mCurrentItem=0;
// invalidate();
// mAnimMode = ANIM_MODE_NONE;
// 目前动画都是通过Animation完成的,而不是在onDraw中递归invalidate实现,所以为了
// 避免两个连续的动画产生“跳跃”,将下一个旋转动画放到下个UI循环中
post(new Runnable() {
@Override
public void run() {
int item = Math.max(0, angles.length - 1);
setCurrentItem(0, false);
}
});
}
}
};
mAnimShowOut.setDuration(mShowOutDuration);
}
...
}
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
pointList.clear();
mPaint.setStyle(Paint.Style.FILL);
if (rateList == null)
return;
// drawArcAndText(canvas);
switch (mAnimMode) {
case ANIM_MODE_SHOW_OUT:
animDrawProceed(canvas, mShowOutProgress);
if (isRing) {
drawInner(canvas);
}
if (isShowCenterPoint) {
drawCenterPoint(canvas);
}
// canvas.drawArc(rectF, 0, 360, true, mPaint);
break;
case ANIM_MODE_NONE:
// drawGrownPie(canvas);
break;
case ANIM_MODE_FINISHED:
//动画完成后绘制折现 文本
// drawableText(canvas);
drawArcAndText(canvas);
}
}
initAnims()方法中对动画进行初始化。执行runAnimRotate()来开启动画。onDraw方法中根据动画模式选择执行不同的绘制方法。
方法calcClickItem完成了点击事件的不同处理:点击圆环内部和外部不进行处理,点击圆环对点击模块进行放大处理。
private int calcClickItem(float x, float y) {
if (rateList == null) return -1;
final float centerX = rectF.centerX();
final float centerY = rectF.centerY();
float outerRadius = rectF.width() / 2;
float innerRadius = 80;
// 计算点击的坐标(x, y)和圆中心点形成的角度,角度从0-360,顺时针增加
int clickedDegree = GeomTool.calcAngle(x, y, centerX, centerY);
double clickRadius = GeomTool.calcDistance(x, y, centerX, centerY);
if (clickRadius < innerRadius) {
// 点击发生在小圆内部,也就是点击到标题区域
// return -1;
} else if (clickRadius > outerRadius) {
// 点击发生在大圆环外
return -2;
}
// 计算出来的clickedDegree是整个View原始的,被点击item需要考虑startAngle。
int startAngle = -90;
int angleStart = startAngle;
for (int i = 0; i < angles.length; i++) {
int itemStart = (angleStart + 360) % 360;
float end = itemStart + angles[i];
if (end >= 360f) {
if (clickedDegree >= itemStart && clickedDegree < 360) return i;
if (clickedDegree >= 0 && clickedDegree < (end - 360)) return i;
} else {
if (clickedDegree >= itemStart && clickedDegree < end) {
return i;
}
}
angleStart += angles[i];
}
return -3;
}
// 计算点击的坐标(x, y)和圆中心点形成的角度,角度从0-360,顺时针增加
int clickedDegree = GeomTool.calcAngle(x, y, centerX, centerY);
计算点击的角度 根据点击的坐标(x, y)和圆心(centerX, centerY)可以计算出点击的点相对圆心的角度。下面方法calcAngle完成此任务。
代码如下:
/**
* 计算坐标(x1, y1)和(x2, y2)形成的角度,角度从0-360,顺时针增加
* (x轴向右,y轴向下)
*/
public static int calcAngle(float x1, float y1, float x2, float y2) {
double resultDegree = 0;
double vectorX = x1 - x2; // 点到圆心的X轴向量,X轴向右,向量为(0, vectorX)
double vectorY = y2 - y1; // 点到圆心的Y轴向量,Y轴向上,向量为(0, vectorY)
// 点落在X,Y轴的情况这里就排除
if (vectorX == 0) {
// 点击的点在Y轴上,Y不会为0的
if (vectorY > 0) {
resultDegree = 90;
} else {
resultDegree = 270;
}
} else if (vectorY == 0) {
// 点击的点在X轴上,X不会为0的
if (vectorX > 0) {
resultDegree = 0;
} else {
resultDegree = 180;
}
} else {
// 根据形成的正切值算角度
double tanXY = vectorY / vectorX;
double arc = Math.atan(tanXY);
// degree是正数,相当于正切在四个象限的角度的绝对值
double degree = Math.abs(arc / Math.PI * 180);
// 将degree换算为对应x正轴开始的0-360的角度
if (vectorY < 0 && vectorX > 0) {
// 右下 0-90
resultDegree = degree;
} else if (vectorY < 0 && vectorX < 0) {
// 左下 90-180
resultDegree = 180 - degree;
} else if (vectorY > 0 && vectorX < 0) {
// 左上 180-270
resultDegree = 180 + degree;
} else {
// 右上 270-360
resultDegree = 360 - degree;
}
}
return (int) resultDegree;
}
上面的方法calcClickItem根据此角度,结合当前圆环的mStartAngle、mRotation就可以确定点击落在的扇形区域了。
计算扇形中心 绘制扇形过程中,可以得到扇形的中间角度middleAngle,而中心的半径就是圆环外半径减去一半圆环宽度,使用GeomTool.calcCirclePoint工具方法,可以根据“圆心、半径、角度”计算出扇形中心点的坐标。
代码如下:
/**
* 计算指定角度、圆心、半径时,对应圆周上的点。
* @param angle 角度,0-360度,X正轴开始,顺时针增加。
* @param radius 圆的半径
* @param cx 圆心X
* @param cy 圆心Y
* @param resultOut 计算的结果(x, y) ,方便对象的重用。
* @return resultOut, or new Point if resultOut is null.
*/
public static Point calcCirclePoint(int angle, float radius, float cx, float cy, Point resultOut) {
if (resultOut == null) resultOut = new Point();
// 将angle控制在0-360,注意这里的angle是从X正轴顺时针增加。而sin,cos等的计算是X正轴开始逆时针增加
angle = clampAngle(angle);
double radians = angle / 180f * Math.PI;
double sin = Math.sin(radians);
double cos = Math.cos(radians);
double dy = radius * sin;
double dx = radius * cos;
double x = cx + dx;
double y = cy + dy;
resultOut.set((int) x, (int) y);
return resultOut;
}
绘制描述文本和折现
private void drawArcCenterPoint(Canvas canvas, int position) {
if (rateList!=null){
preAngle=getPreAngle(position);
}
if (rateList != null) {
endAngle = getAngle(rateList.get(position));
}
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(mRes.getColor(R.color.transparent));
mPaint.setStrokeWidth(dip2px(1));
canvas.drawArc(rectFPoint, preAngle, (endAngle) / 2, true, mPaint);
dealPoint(rectFPoint, preAngle, (endAngle) / 2, pointArcCenterList);
Point point = pointArcCenterList.get(position);
mPaint.setColor(mRes.getColor(R.color.color_D8D8D8));
// canvas.drawCircle(point.x, point.y, dip2px(2), mPaint);
if (preRate / 2 + rateList.get(position) / 2 < 5) {
extendLineWidth = 17;
rate = 0.3f;
} else {
extendLineWidth = 17;
rate = 0.3f;
}
rate = 15f / ringPointRidus;
// 外延画折线
float lineXPoint1 = (point.x - dip2px(leftMargin + ringOuterRidus)) * (1 + rate);
float lineYPoint1 = (point.y - dip2px(topMargin + ringOuterRidus)) * (1 + rate);
float[] floats = new float[8];
floats[0] = point.x;
floats[1] = point.y;
floats[2] = dip2px(leftMargin + ringOuterRidus) + lineXPoint1;
floats[3] = dip2px(topMargin + ringOuterRidus) + lineYPoint1;
floats[4] = dip2px(leftMargin + ringOuterRidus) + lineXPoint1;
floats[5] = dip2px(topMargin + ringOuterRidus) + lineYPoint1;
float bgrectLeft;
float bgRectRight;
float textleftPos;
//1. 粗略计算文字宽度
float textWidth=mPaint.measureText(rateList.get(position) + "%");
if (point.x >= dip2px(leftMargin + ringOuterRidus)) {
mPaint.setTextAlign(Paint.Align.LEFT);
floats[6] = dip2px(leftMargin + ringOuterRidus) + lineXPoint1 + dip2px(extendLineWidth);
bgrectLeft = floats[6] + mPaint.getStrokeWidth();
bgRectRight = floats[6] +textWidth+ dip2px(14) + mPaint.getStrokeWidth();
textleftPos = bgrectLeft + dip2px(7);
} else {
mPaint.setTextAlign(Paint.Align.RIGHT);
floats[6] = dip2px(leftMargin + ringOuterRidus) + lineXPoint1 - dip2px(extendLineWidth);
bgRectRight = floats[6] - mPaint.getStrokeWidth();
bgrectLeft = floats[6] - textWidth- dip2px(14) - mPaint.getStrokeWidth();
textleftPos =bgRectRight - dip2px(7);
}
floats[7] = dip2px(topMargin + ringOuterRidus) + lineYPoint1;
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.parseColor("#C5C5C5"));
mPaint.setStrokeWidth(dip2px(1));
canvas.drawRoundRect(new RectF(bgrectLeft-dip2px(1), floats[7] - dip2px(showRateSize) / 2 - dip2px(5),
bgRectRight+dip2px(1), floats[7] + dip2px(showRateSize) / 2 + dip2px(5)), dip2px(2), dip2px(2), mPaint);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint.setColor(Color.parseColor("#4C4C4C"));
canvas.drawRoundRect(new RectF(bgrectLeft, floats[7] - dip2px(showRateSize) / 2 - dip2px(4),
bgRectRight, floats[7] + dip2px(showRateSize) / 2 + dip2px(4)), dip2px(2), dip2px(2), mPaint);
mPaint.setColor(Color.parseColor("#D8D8D8"));
canvas.drawLines(floats, mPaint);
mPaint.setTextSize(dip2px(showRateSize));
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mRes.getColor(R.color.white));
canvas.drawText(rateList.get(position) + "%", textleftPos, floats[7] + dip2px(showRateSize) / 3, mPaint);
preRate = rateList.get(position);
// addView(getTextView(rateList.get(position)+""));
}
参考文章:https://www.cnblogs.com/everhad/p/5809982.html 代码下载地址 https://download.csdn.net/download/muranfei/20929757
|