当项目要做图标功能时,大家基本就是去引用开源库来使用了,其中最著名的图标库是MPAndroidChart了。但是,如果能够自己手动撸出各种图标,那是不是很酷呢?常用图表类型包括柱状图,折线图,饼状图,雷达图,股票图,这里是做的前三者图表。
演示效果:
实现步骤
-
柱状图
1.以等差高度画Y轴文字和横线
2.画X轴文字
3.依据数值画不等高度的矩形柱
4.点击各个柱子显示顶部数值
-
折线图
1.以等差高度画Y轴文字和横线
2.画X轴文字
3.根据数值绘制path
-
饼状图
1.根据数值绘制不同起始角度和弧度的圆环
2.依据弧度并利用三角函数知识绘制各版块文字,并调节位置至板块中心
3.绘制内部同心圆环
绘制圆环上文字知识点:
圆心为o(x,y),半径r,圆上各个弧度点上的坐标,求值公式为
xA = x+rsinα,yA = y-rcosα,α大小随着顶点变化而变化。
代码实现:
-
柱状图View
/**
* create by libo
* create on 2020/7/26
* description 柱状图View
*/
public class BarChartView extends View {
/** 文字Paint */
private Paint textPaint;
/** Y轴基准线Paint */
private Paint linePaint;
/** 柱型paint */
private Paint rectPaint;
/** Y轴每单元数量高度 */
private float unitHeight;
/** Y轴数据数组 */
private int[] unitHeightNum = new int[] {100, 200, 300, 400, 500};
/** 各个阶段数据数组 */
private int[] stageNum = new int[] {125, 230, 323, 253, 398, 410};
private String[] stageStr = new String[] {"Jan", "Feb", "Mar", "Apr", "May", "Jun"};
private int[] colors = new int[] {R.color.green, R.color.blue, R.color.yellow, R.color.red};
/** X轴单元宽度 */
private float unitWidth;
/** 横线左边距大小 */
private float lineLeftPadding;
/** 柱状图左右间距 */
private int rectPadding = 12;
/** 每个柱子集合 */
private ArrayList<Bar> bars = new ArrayList<>();
/** 当前显示值的位置 */
private int showValuePos = -1;
public BarChartView(Context context) {
super(context);
initPaint();
}
public BarChartView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initPaint();
}
private void initPaint() {
textPaint = new Paint();
textPaint.setColor(getResources().getColor(R.color.textcolor));
textPaint.setTextSize(40);
textPaint.setAntiAlias(true);
linePaint = new Paint();
linePaint.setColor(getResources().getColor(R.color.linecolor));
linePaint.setAntiAlias(true);
linePaint.setStyle(Paint.Style.FILL);
linePaint.setStrokeWidth(2);
rectPaint = new Paint();
rectPaint.setAntiAlias(true);
rectPaint.setStyle(Paint.Style.FILL);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawYText(canvas);
drawXText(canvas);
drawBars(canvas);
}
/**
* 绘制Y轴文字及基准线
*/
private void drawYText(Canvas canvas) {
int top = getHeight() - 80; //给底部文字留下高度
unitHeight = getHeight()/unitHeightNum.length - 20;
for (int num : unitHeightNum) {
Rect rect = new Rect();
String text = num + "万";
textPaint.getTextBounds(text, 0, text.length(), rect);
canvas.drawText(text, 0, top, textPaint); //画文字
lineLeftPadding = rect.width() + 20;
canvas.drawLine(lineLeftPadding, top, getWidth(), top, linePaint); //画横线
top -= unitHeight;
}
}
/**
* 绘制X轴文字
*/
private void drawXText(Canvas canvas) {
float left = lineLeftPadding;
unitWidth = getWidth()/stageNum.length - 20;
Bar bar;
for (int i=0;i<stageNum.length;i++) {
canvas.drawText(stageStr[i], left + unitWidth/4, getHeight()-20, textPaint); //画文字
float top = getHeight() - (float)stageNum[i]/100*unitHeight;
int color = getResources().getColor(colors[i%colors.length]);
bar = new Bar(stageNum[i], left+rectPadding, top, left+unitWidth-rectPadding, getHeight()- 80, color);
bars.add(bar);
left += unitWidth;
}
}
/**
* 绘制柱形
*/
private void drawBars(Canvas canvas) {
//画矩形,并左右设置间距
//根据该项数值获取实际的柱形高度
//Y轴每格单元高度为100数值
for (int i=0;i<bars.size();i++) {
Bar bar = bars.get(i);
rectPaint.setColor(bar.color);
canvas.drawRect(bar.left, bar.top, bars.get(i).right, bar.bootom, rectPaint);
//绘制柱形上数值
if (showValuePos == i) {
String value = String.valueOf(bar.value);
Rect rect = new Rect();
textPaint.getTextBounds(value, 0, value.length(), rect);
float textLeft = bar.left + (bar.right-bar.left-rect.width())/2; //计算使文字在柱形居中位置
canvas.drawText(value, textLeft, bar.top-20, textPaint); //绘制柱顶部数值
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
for (int i=0;i<bars.size();i++) {
if (event.getX() > bars.get(i).left && event.getX() < bars.get(i).right) {
//按下事件在当前柱形内
showValuePos = i;
invalidate();
}
}
}
return true;
}
/**
* 柱形类
*/
class Bar {
private int value;
private float left;
private float top;
private float right;
private float bootom;
private int color;
public Bar(int value, float left, float top, float right, float bootom, int color) {
this.value = value;
this.left = left;
this.top = top;
this.right = right;
this.bootom = bootom;
this.color = color;
}
}
}
-
折线图View
/**
* create by libo
* create on 2020/7/26
* description 折线图View
*/
public class LineChartView extends View {
/** 文字Paint */
private Paint textPaint;
/** 基准线Paint */
private Paint linePaint;
/** 折线paint */
private Paint charLinePaint;
/** Y轴每单元数量高度 */
private float unitHeight;
/** Y轴数据数组 */
private int[] unitHeightNum = new int[] {0, 20, 40, 60, 80, 100};
private String[] stageStr = new String[] {"Jan", "Feb", "Mar", "Apr", "May", "Jun"};
/** 横线左边距大小 */
private float lineLeftPadding;
/** X轴单元宽度 */
private float unitWidth;
/** 各个阶段数据数组 */
private int[] stageNum = new int[] {56, 40, 82, 74, 60, 92};
private Path linePath;
public LineChartView(Context context) {
super(context);
initPaint();
}
public LineChartView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initPaint();
}
private void initPaint() {
textPaint = new Paint();
textPaint.setColor(getResources().getColor(R.color.textcolor));
textPaint.setTextSize(40);
textPaint.setAntiAlias(true);
linePaint = new Paint();
linePaint.setColor(getResources().getColor(R.color.linecolor));
linePaint.setAntiAlias(true);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(2);
charLinePaint = new Paint();
charLinePaint.setColor(getResources().getColor(R.color.orange));
charLinePaint.setAntiAlias(true);
charLinePaint.setStyle(Paint.Style.FILL);
linePath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawYText(canvas);
drawXText(canvas);
drawLinePath(canvas);
}
/**
* 绘制Y轴文字及基准线
*/
private void drawYText(Canvas canvas) {
int top = getHeight() - 80; //给底部文字留下高度
unitHeight = getHeight()/unitHeightNum.length - 20;
Rect rect = new Rect();
String longText = unitHeightNum[unitHeightNum.length-1]+"万"; //以最长文字对齐
textPaint.getTextBounds(longText, 0, longText.length(), rect);
for (int num : unitHeightNum) {
canvas.drawText(num + "万", 0, top, textPaint); //画文字
lineLeftPadding = rect.width() + 20;
canvas.drawLine(lineLeftPadding, top, getWidth(), top, linePaint); //画横线
top -= unitHeight;
}
}
/**
* 绘制X轴文字
*/
private void drawXText(Canvas canvas) {
float left = lineLeftPadding;
unitWidth = getWidth()/stageNum.length - 20;
for (int i=0;i<stageNum.length;i++) {
canvas.drawText(stageStr[i], left + unitWidth/4, getHeight()-20, textPaint); //画文字
canvas.drawLine(left, getHeight()-80, left, 80, linePaint);
left += unitWidth;
}
}
/**
* 绘制折线
*/
private void drawLinePath(Canvas canvas) {
float left = lineLeftPadding;
for (int i=0;i<stageNum.length;i++) {
float topX = left + unitWidth/2;
float topY = getHeight() - (float)stageNum[i]/20*unitHeight;
if (i == 0) {
linePath.moveTo(topX, topY);
} else {
linePath.lineTo(topX, topY);
}
charLinePaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(topX, topY, 10, charLinePaint); //绘制拐点小圆
left += unitWidth;
}
charLinePaint.setStyle(Paint.Style.STROKE);
charLinePaint.setStrokeWidth(4);
canvas.drawPath(linePath, charLinePaint);
}
}
-
饼状图View
/**
* create by libo
* create on 2020/7/26
* description 饼状图
*/
public class PieChartView extends View {
private Paint piePaint;
private Paint innerPiePaint;
/** 圆环宽度 */
private int ringWidth;
private int[] colors = new int[] {R.color.green, R.color.blue, R.color.yellow, R.color.red, R.color.orange};
private int[] values = new int[] {21, 12, 30, 23, 14};
private String[] titles = new String[] {"医院", "学校", "酒店", "商场", "商业建筑"};
private Paint textPaint;
private Paint centerTextPaint;
private String title = "设施占比";
public PieChartView(Context context) {
super(context);
init();
}
public PieChartView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
piePaint = new Paint();
piePaint.setAntiAlias(true);
piePaint.setStyle(Paint.Style.STROKE);
piePaint.setAlpha(190);
innerPiePaint = new Paint();
innerPiePaint.setAntiAlias(true);
innerPiePaint.setStyle(Paint.Style.STROKE);
textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setColor(getResources().getColor(R.color.white));
textPaint.setTextSize(45);
centerTextPaint = new Paint();
centerTextPaint.setAntiAlias(true);
centerTextPaint.setColor(getResources().getColor(R.color.colorPrimary));
centerTextPaint.setTextSize(55);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawRing(canvas);
drawText(canvas);
drawCenterText(canvas);
}
/**
* 画分块圆环
*/
private void drawRing(Canvas canvas) {
ringWidth = getMeasuredWidth()/4;
piePaint.setStrokeWidth(ringWidth);
innerPiePaint.setStrokeWidth(60);
RectF rectF = new RectF(ringWidth/2, ringWidth/2, getWidth()-ringWidth/2, getHeight()-ringWidth/2);
RectF innerRectF = new RectF(ringWidth, ringWidth, getWidth()-ringWidth, getHeight()-ringWidth);
int startAngle = -90; //12点钟方向起始
for (int i=0;i<values.length;i++) {
float sweepAngle = (float) values[i]/100*360;
piePaint.setColor(getResources().getColor(colors[i]));
innerPiePaint.setColor(getResources().getColor(colors[i]));
canvas.drawArc(rectF, startAngle, sweepAngle, false, piePaint);
innerPiePaint.setAlpha(120);
canvas.drawArc(innerRectF, startAngle, sweepAngle, false, innerPiePaint);
startAngle += sweepAngle;
}
}
/**
* 画每分块文字
*/
private void drawText(Canvas canvas) {
int startAngle = 0; //12点钟方向起始
int radius = getWidth()/2-ringWidth/2;
for (int i=0;i<titles.length;i++) {
float sweepAngle = (float) values[i]/100*360;
double angle = Math.toRadians(startAngle + sweepAngle/2);
float x = (float) (getWidth()/2 + radius*Math.sin(angle));
float y = (float) (getHeight()/2 - radius*Math.cos(angle));
//横坐标需要全部左移文字宽度的一半
Rect rect = new Rect();
textPaint.getTextBounds(titles[i], 0, titles[i].length(), rect);
canvas.drawText(titles[i], x-rect.width()/2, y, textPaint);
startAngle += sweepAngle;
}
}
/**
* 中心文字
*/
private void drawCenterText(Canvas canvas) {
Rect rect = new Rect();
centerTextPaint.getTextBounds(title, 0, title.length(), rect);
canvas.drawText(title, getWidth()/2-rect.width()/2, getHeight()/2+rect.height()/2, centerTextPaint);
}
}