项目开发中时不时会碰到柱状图、折线图、饼状图等效果,这些效果肯定是需要自定义控件通过绘制或者摆放来实现,当然了,也有一些很不错的第三方库,比如MPAndroid、hellocharts等,里面就实现了柱状图、折线图、饼状图等各种效果,甚至还有k线图效果;这里自定义柱状图的目的是为了熟悉Android的自定义view、Canvas绘制等知识,提升自己的Android开发水平等;先来看下大致实现的一个效果:
通过看效果,大致需要绘制实现下面这些东西:
1、标题的绘制
2、横轴、纵轴的绘制
3、横轴/纵轴刻度 箭头 文字的绘制,纵轴还有测度的绘制
4、柱状图的绘制
而对于自定义view来说,首先继承自view,初始化参数和自定义属性,测量,绘制...大致就是一个这样的流程;老规矩还是先看初始这一步;
public class HistogramView extends View {
//图表标题
private String graphTitle = "";
//标题字体的大小
private int graphTitleSize = 18;
//标题的字体颜色
private int graphTitleColor = Color.RED;
//x轴名称
private String xAxisName = "";
//y轴名称
private String yAxisName = "";
//坐标轴字体颜色
private int axisTextSize = 12;
//坐标轴字体颜色
private int axisTextColor = Color.BLACK;
//x y坐标线条的颜色
private int axisLineColor = Color.BLACK;
//x,y坐标线的宽度
private int axisLineWidth = 2;
private Paint mPaint;
private int screenWith, screenHeight;
//视图的宽度
private int width;
//视图的高度
private int height;
//起点x坐标值
private int originalX;
//起点y坐标值
private int originalY;
//y轴等份划分
private int axisDivideSizeY;
//标题距离x轴的距离
private int titleMarginXaxis = 60;
//x y轴刻度的高度
private int xAxisScaleHeight = 5;
//刻度的最大值
private Integer maxValue;
//y轴空留部分高度
private int yMarign = 30;
//柱状图数据
private List columnList;
//柱状图颜色
private List columnColors;
public HistogramView(Context context) {
this(context, null);
}
public HistogramView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HistogramView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取屏幕的宽高
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics metrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(metrics);
screenWith = metrics.widthPixels;
screenHeight = metrics.heightPixels;
initAttrs(context, attrs);
initPaint();
}
/**
* //获取自定义属性
*/
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.HistogramView);
graphTitle = array.getString(R.styleable.HistogramView_graphTitle);
xAxisName = array.getString(R.styleable.HistogramView_xAxisName);
yAxisName = array.getString(R.styleable.HistogramView_yAxisName);
axisTextSize = array.getDimensionPixelSize(R.styleable.HistogramView_axisTextSize, sp2px(axisTextSize));
axisTextColor = array.getColor(R.styleable.HistogramView_axisTextColor, axisTextColor);
axisLineColor = array.getColor(R.styleable.HistogramView_axisLineColor, axisLineColor);
graphTitleSize = array.getDimensionPixelSize(R.styleable.HistogramView_graphTitleSize, sp2px(graphTitleSize));
graphTitleColor = array.getColor(R.styleable.HistogramView_graphTitleColor, graphTitleColor);
axisLineWidth = (int) array.getDimension(R.styleable.HistogramView_axisLineWidth, dip2px(axisLineWidth));
array.recycle();
}
/**
* 初始化paint
*/
private void initPaint() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
}
private int sp2px(int sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
}
private int dip2px(int dip) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, getResources().getDisplayMetrics());
}
}
就是一些常量、成员变量的定义和赋值,初始化自定义属性和画笔,接下来还是测量,那就看看onMeasure方法;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int w = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST) {
w = screenWith;
}
int h = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode == MeasureSpec.AT_MOST) {
h = screenHeight;
}
setMeasuredDimension(w, h);
if (width == 0 || height == 0) {
//x轴的起点位置
originalX = dip2px(30);
//视图的宽度 空间的宽度减去左边和右边的位置
width = getMeasuredWidth() - originalX * 2;
//y轴的起点位置 空间高度的2/3
originalY = getMeasuredHeight() * 2 / 3;
//图表显示的高度为空间高度的一半
height = getMeasuredHeight() / 2;
}
}
onMeasure方法有时候会多次调用,所以当试图的width和height赋值后,就没有必要再去计算一些值了,对于Android屏幕来说,它的原点x、y和生活的坐标轴x,y有点一样,左上角顶点是它的原点x,y;x轴往右边走还是一样的增大,y轴往下走就不一样了,往上走是增大,往上走是减小的;
A点是屏幕的原点,B点是自定义柱状图的原始点,就需要对B点原始点进行定义,再根据B点原始点来计算柱状图显示的宽度和高度;originalX也就是B的x点,往右移动了30,originalY也就是B的y为屏幕高度的2/3,这个可以根据自己的需要进行设定,原点知道了,就可以计算试图宽度width了,就是getMeasuredWidth() - originalX * 2就可以了,测量ok了,剩下就只有绘制了,先易后难,先绘制柱状图的标题;
/**
* 绘制标题
*
* @param canvas
*/
private void drawTitle(Canvas canvas) {
if (!TextUtils.isEmpty(graphTitle)) {
//绘制标题
mPaint.setTextSize(graphTitleSize);
mPaint.setColor(graphTitleColor);
//设置文字粗体
mPaint.setFakeBoldText(true);
//获取文字的宽度
float measureText = mPaint.measureText(graphTitle);
canvas.drawText(
graphTitle,
getWidth() / 2 - measureText / 2,
originalY + dip2px(titleMarginXaxis),
mPaint
);
}
}
标题有才会进行绘制,一开始也就是paint的设置,绘制文字调用drawText就可以进行绘制了,不过要先确定文字的x,y的起始位置;
y的话就在originalY的基础上往下移动一定距离就可以,看效果,标题是屏幕居中显示,那就用屏幕宽度/2-文字宽度/2就可以得到x的位置了;
x轴和y轴的绘制放一起进行绘制,x轴变动的x的终点,y轴变动的也只是y轴的终点;
/**
* 绘制x轴
*
* @param canvas
*/
protected void drawXAxis(Canvas canvas) {
mPaint.setColor(axisLineColor);
mPaint.setStrokeWidth(axisLineWidth);
canvas.drawLine(originalX, originalY, originalX + width, originalY, mPaint);
}
/**
* 绘制y轴
*
* @param canvas
*/
protected void drawYAxis(Canvas canvas) {
mPaint.setColor(axisLineColor);
mPaint.setStrokeWidth(axisLineWidth);
canvas.drawLine(originalX, originalY, originalX, originalY - height, mPaint);
}
接下来是x刻度值,y轴刻度和刻度值的绘制;
/**
* 绘制x轴刻度值
*
* @param canvas
*/
protected void drawXAxisScaleValue(Canvas canvas) {
int xTxtMargin = dip2px(15);
mPaint.setColor(axisTextColor);
mPaint.setTextSize(axisTextSize);
mPaint.setFakeBoldText(true);
float cellWidth = width / (columnList.size() + 2);
for (int i = 0; i < columnList.size() + 1; i++) {
if (i == 0) {
continue;
}
String txt = i + "";
//测量文字的宽度
float txtWidth = mPaint.measureText(txt);
canvas.drawText(txt, cellWidth * i + originalX + (cellWidth / 2 - txtWidth / 2),
originalY + xTxtMargin,
mPaint);
}
}
首先要计算每一份显示的宽度,第一和最后一个位置要多空置各一个宽度,就要在柱状图数据集合size上+2;就是width / (columnList.size() + 2),然后调用drawText进行绘制;
/**
* 绘制y轴刻度
*
* @param canvas
*/
protected void drawYAxisScale(Canvas canvas) {
mPaint.setColor(axisLineColor);
float cellHeight = (height - dip2px(yMarign)) / axisDivideSizeY;
for (int i = 0; i < axisDivideSizeY; i++) {
canvas.drawLine(originalX,
originalY - cellHeight * (i + 1),
originalX + 10,
originalY - cellHeight * (i + 1),
mPaint);
}
}
y轴刻度的高度是根据调用是传入的axisDivideSizeY来计算的,要看y上面显示多少分,计算出每份的高度cellHeight后,调用drawLine进行绘制;
/**
* 绘制y轴刻度值
*
* @param canvas
*/
protected void drawYAxisScaleValue(Canvas canvas) {
try {
mPaint.setColor(axisTextColor);
mPaint.setTextSize(axisTextSize);
int cellHeight = (height - dip2px(yMarign)) / axisDivideSizeY;
float cellValue = maxValue / (axisDivideSizeY + 0f);
//这里只处理的大于1时的绘制 小于等于1的绘制没有处理
int ceil = (int) Math.ceil(cellValue);
// DecimalFormat df2 = new DecimalFormat("###.00");
// String format = df2.format(ceil);
// float result = Float.parseFloat(format);
for (int i = 0; i < axisDivideSizeY + 1; i++) {
if (i == 0) {
continue;
}
String s = ceil * i + "";
float v = mPaint.measureText(s);
canvas.drawText(s,
originalX - v - 10,
originalY - cellHeight * i + 10,
mPaint);
}
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
每份的高度和刻度一样也是通过axisDivideSizeY来计算出cellHeight,每份显示的value也就是刻度值,通过柱状图数据集合中的最大值/axisDivideSizeY y轴显示的份数,最大值的话是用过调用setColumnInfo方法设置参数时获取的;
/**
* 调用该方法进行图表的设置
* @param columnList 柱状图的数据
* @param columnColors 颜色
* @param axisDivideSizeY y轴显示的等份数
*/
public void setColumnInfo(List columnList, List columnColors, int axisDivideSizeY) {
this.columnList = columnList;
this.columnColors = columnColors;
this.axisDivideSizeY = axisDivideSizeY;
//获取刻度的最大值
maxValue = Collections.max(columnList);
Log.e("TAG", "maxValue-->" + maxValue);
invalidate();
}
计算出每份的刻度值,遍历循环就可以计算出对应的刻度值,调用drawText就可以进行绘制了;x、y轴,标题,x、y轴的刻度和刻度值都绘制好了,就剩下x、y的箭头,柱状图了;
/**
* 绘制x轴箭头
*
* @param canvas
*/
private void drawXAxisArrow(Canvas canvas) {
mPaint.setColor(axisTextColor);
Path xPath = new Path();
xPath.moveTo(originalX + width + 30, originalY);
xPath.lineTo(originalX + width, originalY + 10);
xPath.lineTo(originalX + width, originalY - 10);
xPath.close();
canvas.drawPath(xPath, mPaint);
//绘制x轴名称
if (!TextUtils.isEmpty(xAxisName)) {
canvas.drawText(xAxisName, originalX + width, originalY + 50, mPaint);
}
}
/**
* 绘制y轴箭头
*
* @param canvas
*/
private void drawYAxisArrow(Canvas canvas) {
mPaint.setColor(axisTextColor);
Path yPath = new Path();
yPath.moveTo(originalX, originalY - height - 30);
yPath.lineTo(originalX - 10, originalY - height);
yPath.lineTo(originalX + 10, originalY - height);
yPath.close();
canvas.drawPath(yPath, mPaint);
//绘制y轴名称
if (!TextUtils.isEmpty(yAxisName)) {
canvas.drawText(yAxisName, originalX - 50, originalY - height - 35, mPaint);
}
}
x、y轴的箭头、文字绘制差不多,不过要绘制三角形箭头,canvas并没有提供绘制三角形的api,需要利用path路径来绘制,最后看看柱状图的绘制;
/**
* 绘制柱状图
*
* @param canvas
*/
protected void drawColumn(Canvas canvas) {
if (columnList != null && columnColors != null) {
float cellWidth = width / (columnList.size() + 2);
//根据最大值和高度计算比例
float scale = (height - dip2px(yMarign)) / maxValue;
for (int i = 0; i < columnList.size(); i++) {
mPaint.setColor(columnColors.get(i));
float leftTopY = originalY - columnList.get(i) * scale;
canvas.drawRect(originalX + cellWidth * (i + 1),
leftTopY,
originalX + cellWidth * (i + 2),
originalY - axisLineWidth / 2,
mPaint);
}
}
}
x轴每份的宽度和x轴刻度值的计算一样的,根据柱状图显示的高度/maxValue,计算出每份的高度,调用drawRect绘制矩形,绘制时需要注意矩形矩形的起始x、y点,终点x、y点,x轴的话,其实上一个的终点就是下一个的x起始点,因为第一个是空置的,所以x的起始点就是originalX + cellWidth * (i + 1) x原点+对应index位置的每份宽度;y轴的话,终点是一致的,都是原点-x轴宽度/2(originalY - axisLineWidth / 2),起始点就是y轴原点-index对应的value*scale;这样就确定了每个矩形的起始x、y点,终点x、y点绘制出来就ok了;使用的话通过setColumnInfo传入对应的参数就可以了。
public class MainActivity extends AppCompatActivity {
private HistogramView histogramView;
private List values;
private List colors;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
histogramView = findViewById(R.id.histogram_view);
values = new ArrayList<>();
colors = new ArrayList<>();
values.add(16);
values.add(25);
values.add(44);
values.add(11);
values.add(22);
values.add(17);
values.add(35);
colors.add(Color.BLUE);
colors.add(Color.BLACK);
colors.add(Color.GREEN);
colors.add(Color.GRAY);
colors.add(Color.RED);
colors.add(Color.YELLOW);
colors.add(Color.LTGRAY);
histogramView.setColumnInfo(values, colors, 7);
}
}
源码