自定义view:快速实现柱状图的绘制
来源:互联网 发布:java web前端开发工具 编辑:程序博客网 时间:2024/05/05 22:24
自定义view:快速实现柱状图的绘制
标签(空格分隔): 安卓
1、前言
本文讲解如何通过canvas快速实现柱状图表的绘制,先看下最终效果图:
网上开源的图表绘制框架还是很多的,功能也非常强大,比如hellocharts,MPAndroidCharts等。
但是作为一个有追求的程序员,过多地使用第三方的东西总是觉得缺少点安全感。主要原因是现成的轮子固然好,但是仅仅作为使用者的我们对其理解不深,导致不易于扩展修改。尤其是涉及自定义view这种效果显示的,保不准哪天产品和设计就把效果给改了,然后我们只能苦逼的研究源码,这样太被动了。而自己写的东西就不一样了,全在自己掌控之下,任你需求设计随便改,我自横刀对你笑。
2、分析
整个柱状图的绘制可以分为三个步骤:
- 坐标轴绘制
- 绘制标尺
- 刻度值绘制
- 柱状绘制
3、代码实现
3.1 坐标轴的绘制
坐标轴的绘制使用的是cavans的drawLine
方法:
drawLine(float startX, float startY, float stopX, float stopY,Paint paint)
其中传入的参数分别是起始坐标、终点坐标、画笔。俩点妥妥的确认一条直线,所以我们知道俩个绘制坐标点就行。
x轴和y轴都是从原点(坐标轴的原点,而得view的,请勿混淆了)开始画,为了确认这个点,我们全局定义了一个变量mPaddingLabLine
表示坐标轴到view边界(Y轴就是到左边,x就是相对于底部)的距离,这个距离我们是用来绘制坐标轴刻度以及标题。涉及到位置计算时,为了兼容性我们去除view的内边距。这个原点的x、y坐标计算方法代码如下:
float startX = getPaddingLeft() + mPaddingLabLine; float startY = getHeight() - getPaddingBottom() - mPaddingLabLine;
x轴终点坐标很好理解,y方向不变与起始点相等,x方向宽度减去内边距即可:
float stopX = getWidth() - getPaddingRight(); float stopY = startY;
y轴终点坐标与x轴的类似,只是此时x点坐标没变,y点变成了内边距,起始点计算出来了后,线条的绘制就很简单了:
canvas.drawLine(startX, startY, stopX, stopY, mTabLinePaint); //x轴 canvas.drawLine(startX, startY, startX, getPaddingTop(), mTabLinePaint); //y轴
3.2 标尺绘制
标尺这部分也是绘制直线,在图表上绘制mLabelCount
条直线作为标尺线.
float height = startY; float ceilHeight = (mHeight - mAxisTitleSize) / mLabelCount; for (int i = 0; i < mLabelCount; i++) { canvas.save(); canvas.translate(0, -ceilHeight * i); canvas.drawLine(startX, startY, stopX, stopY, mTabLinePaint); canvas.restore(); }
我们计算根据标尺数目计算出每根标尺线之间的距离ceilHeight
,每次绘制标尺线时,只需要在Y方向平移画布就行。可以理解为绘制一与x轴重叠的直线,然后往上平移,这样逻辑上更简洁。
3.3 刻度值绘制
刻度值包括俩个,一个是坐标轴名称,比如Y轴的销售额,x轴的年份等,另一个是每一个刻度的具体值。
坐标轴名称绘制
x轴相对简单一点,直接调用cavans的drawText方法即可。这里主要分析下y轴的画法,因为y轴名称文字是竖着显示,所以绘画的时候需要把画布逆时针旋转90度,画布旋转注意中心点的位置为文字绘制的位置,但是需要稍微右移一点,防止文字左偏导致部分内容不可见:
canvas.save(); canvas.rotate(-90, getPaddingLeft(), mHeight / 3 - textHeight); canvas.drawText(axisTitle, getPaddingLeft(), mHeight / 3, mTabLinePaint); canvas.restore();
坐标轴刻度值绘制
绘制刻度值的关键代码:
canvas.drawText(axisName[i], pointFs[i].x, pointFs[i].y, mTabLinePaint);
其中axisName
表示需要绘制的坐标刻度值,pointFs
表示每个刻度值文字绘制时的坐标点数组,俩个坐标轴的计算方法不同,先看x轴:
int textHeight = getTextHeight(); int cellWidth = (mWidth - mPaddingLabLine - mColumnMarginRight) / this.mColumnName.length; final int y = mHeight - mPaddingLabLine + textHeight; for (int i = 0; i < pointFs.length; i++) { float textWidth = mTabLinePaint.measureText(axisName[i]); PointF pointF = new PointF(); pointF.x = mPaddingLabLine + mColumnMarginRight + i * cellWidth + textWidth / 2; pointF.y = y; pointFs[i] = pointF; }
根据x轴刻度值数目把x轴平分为宽度为cellWidth
的若干等分,mColumnMarginRight
的含义为,第一根柱子从原点向像右偏移的距离,防止第一根柱子紧挨Y轴。为了保证每个刻度值都在柱子的正中间,每次计算该刻度的x位置时,加上该刻度值宽度的一半。所有X轴刻度值得y坐标都是一样的,x轴加上文字高度即可textHeight
。
接下来是Y轴刻度值绘制:
float height = getHeight() - getPaddingBottom() - mPaddingLabLine; float ceilHeight = (mHeight - mAxisTitleSize) / mLabelCount; for (int i = 0; i < pointFs.length; i++) { PointF pointF = new PointF(); float textWidth = mTabLinePaint.measureText(axisName[i]); pointF.x = mPaddingLabLine - textWidth - mAxisTextDistance / 3; pointF.y = height - i * ceilHeight; pointFs[i] = pointF; } }
Y轴刻度值得计算与X轴类似,有个小细节注意一下就好了,可能刻度值文字个数不一样,按drawText方法又是从左开始画的(刻度值左对齐),这样的显示效果非常丑,要让刻度值右对齐的话就必须测量文字的宽度,然后Y轴减去这个宽度就可以,这样就紧靠Y轴,所以再减去mAxisTextDistance / 3
就可以实现Y轴刻度值右对齐并且左偏移Y轴一定距离。
getTextHeight()
测量字体的绘制高度:
//获取字符串的高度private int getTextHeight(){ Paint.FontMetricsInt fontMetrics = mTabLinePaint.getFontMetricsInt(); return fontMetrics.bottom - fontMetrics.top;}
3.4 绘制柱型
先上代码
//每一条数据占有的宽度 int cellWidth = (int) (mWidth - mPaddingLabLine - mColumnMarginRight) / mColumnData.length; cellWidth = cellWidth > mMaxSubcolumnWidth ? mMaxSubcolumnWidth : cellWidth; //限制柱状图的最大宽度,防止列数过少时柱形图过宽 //矩形区有效绘制高度 float height = getHeight() - getPaddingBottom() - mPaddingLabLine; float proportion = height / (maxValue - minValue); //高度数值比例尺 mTabLinePaint.setStyle(Paint.Style.FILL); for (int j = 0; j < mColumnData.length; j++) { int startPos = mPaddingLabLine + mColumnMarginRight + cellWidth * j; float top =height - proportion * (mColumnData[j] - minValue); RectF rectF = new RectF(startPos + cellWidth / 5, top, startPos + cellWidth * 4 / 5, height); canvas.drawRect(rectF, mTabLinePaint); }
4、完整代码
public class ColumnView extends View{ private int mWidth; private int mHeight; private final static int X_LAB = 0; //表示x轴绘制 private final static int Y_LAB = 1; //y轴的绘制 private Paint mTabLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); private int color_label_line = Color.parseColor("#DDDDDD"); //线条颜色 private int mLineStroke = 1;//线条宽度 private int mLabelCount = 10; //标尺线条数目 /** * 图标所能显示的数值范围, * 最大值比能显示的极值大20%,防止如果出现较大数值时,矩形过高而过于贴近顶部 */ private float maxValue = 100 * 1.2f; private float minValue = 20; private float[] mColumnData; //模拟数据 private String mAxisXTitle = "销售项"; //x轴含义 private String mAxisYTitle = "销售额"; //y轴含义 private String[] mColumnName; //每个矩形数据项名称,即x轴刻度值 private String[] mAxisYName; //y轴刻度值 private int mAxisTitleSize = DensityUtils.dp2px(getContext(), 12); //坐标轴标题大小 private int mAxisNameTextSize = DensityUtils.dp2px(getContext(), 10); //坐标轴刻度值文字大小 private int mAxisTextDistance = DensityUtils.dp2px(getContext(), 6); // 坐标轴标题文字与刻度值之间的距离 private int mColumnMarginRight = DensityUtils.dp2px(getContext(), 3); // 矩形离Y轴的距离 private int mMaxSubcolumnWidth = DensityUtils.dp2px(getContext(), 40); //坐标轴到view边界的留出的空间,用于绘制文字 private int mPaddingLabLine = mAxisTitleSize + mAxisNameTextSize + 2 * mAxisTextDistance; public ColumnView(Context context) { this(context, null); } public ColumnView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public ColumnView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mTabLinePaint.setColor(color_label_line); mTabLinePaint.setStrokeWidth(mLineStroke); initData(); } private void initData() { //生成数据 mColumnData = new float[8]; for (int i = 0; i < mColumnData.length; i++) { mColumnData[i] = new Random().nextInt(80) + 20; } //Y轴刻度 mColumnName = new String[8]; for (int i = 0; i < mColumnData.length; i++) { mColumnName[i] = "数据" + String.valueOf(i + 1); } //y轴刻度 mAxisYName = new String[mLabelCount]; int v = (int) ((maxValue - minValue) / mLabelCount * 100); float value = v / 100f; //转成精确小数点后俩位的小数 for (int i = 0; i < mAxisYName.length; i++) { String axisName = value * i + minValue + ""; mAxisYName[i] = axisName; } } @Override public void layout(int l, int t, int r, int b) { super.layout(l, t, r, b); int width = getWidth(); int height = getHeight(); //兼容性处理,获取view的可绘制区域的宽高 mWidth = width - getPaddingLeft() - getPaddingRight(); mHeight = height - getPaddingBottom() - getPaddingTop(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawColor(Color.WHITE); drawLabLine(canvas); drawAxis(canvas); drawSubcolumn(canvas); } //画柱状矩形 private void drawSubcolumn(Canvas canvas) { mTabLinePaint.setColor(Color.parseColor("#4476AB")); //每一条数据占有的宽度 int cellWidth = (int) (mWidth - mPaddingLabLine - mColumnMarginRight) / mColumnData.length; cellWidth = cellWidth > mMaxSubcolumnWidth ? mMaxSubcolumnWidth : cellWidth; //限制柱状图的最大宽度 //矩形区有效绘制高度 float height = getHeight() - getPaddingBottom() - mPaddingLabLine; float proportion = height / (maxValue - minValue); //高度数值比例尺 mTabLinePaint.setStyle(Paint.Style.FILL); for (int j = 0; j < mColumnData.length; j++) { int startPos = mPaddingLabLine + mColumnMarginRight + cellWidth * j; float top =height - proportion * (mColumnData[j] - minValue); RectF rectF = new RectF(startPos + cellWidth / 5, top, startPos + cellWidth * 4 / 5, height); canvas.drawRect(rectF, mTabLinePaint); } } //画刻度值 private void drawAxis(Canvas canvas) { drawAxisTitle(canvas, mAxisYTitle, Y_LAB); drawAxisTitle(canvas, mAxisXTitle, X_LAB); drawAxisName(canvas, Y_LAB, mAxisYName); drawAxisName(canvas, X_LAB, mColumnName); } // 画刻度值 private void drawAxisName(Canvas canvas, int labelType, String[] axisName) { mTabLinePaint.setTextSize(mAxisNameTextSize); //计算绘制坐标 PointF[] pointFs = calculateAxisNamePosition(labelType, axisName); for (int i = 0; i < axisName.length; i++) { canvas.drawText(axisName[i], pointFs[i].x, pointFs[i].y, mTabLinePaint); } } @NonNull private PointF[] calculateAxisNamePosition(int labelType, String[] axisName) { PointF[] pointFs = new PointF[axisName.length]; if (labelType == X_LAB) //x轴 { int textHeight = getTextHeight(); int cellWidth = (mWidth - mPaddingLabLine - mColumnMarginRight) / this.mColumnName.length; final int y = mHeight - mPaddingLabLine + textHeight; for (int i = 0; i < pointFs.length; i++) { float textWidth = mTabLinePaint.measureText(axisName[i]); PointF pointF = new PointF(); pointF.x = mPaddingLabLine + mColumnMarginRight + i * cellWidth + textWidth / 2; pointF.y = y; pointFs[i] = pointF; } } else { float height = getHeight() - getPaddingBottom() - mPaddingLabLine; float ceilHeight = (mHeight - mAxisTitleSize) / mLabelCount; for (int i = 0; i < pointFs.length; i++) { PointF pointF = new PointF(); float textWidth = mTabLinePaint.measureText(axisName[i]); pointF.x = mPaddingLabLine - textWidth - mAxisTextDistance / 3; pointF.y = height - i * ceilHeight; pointFs[i] = pointF; } } return pointFs; } //绘制坐标轴名称 private void drawAxisTitle(Canvas canvas, String axisTitle, int labelType) { mTabLinePaint.setTextSize(mAxisTitleSize); int textHeight = getTextHeight(); if (labelType == X_LAB) { //x轴 canvas.drawText(mAxisXTitle, mWidth / 2, mHeight - textHeight + mAxisTextDistance, mTabLinePaint); } else { //y轴 canvas.save(); //注意中心点的位置,需要稍微右移一点,防止文字太过于左偏 canvas.rotate(-90, getPaddingLeft(), mHeight / 3 - textHeight); canvas.drawText(axisTitle, getPaddingLeft(), mHeight / 3, mTabLinePaint); canvas.restore(); } } //获取字符串的高度 private int getTextHeight() { Paint.FontMetricsInt fontMetrics = mTabLinePaint.getFontMetricsInt(); return fontMetrics.bottom - fontMetrics.top; } //画坐标轴 private void drawLabLine(Canvas canvas) { mTabLinePaint.setColor(color_label_line); //x轴起始点(也是Y轴的起点),终点 float startX = getPaddingLeft() + mPaddingLabLine; float startY = getHeight() - getPaddingBottom() - mPaddingLabLine; float stopX = getWidth() - getPaddingRight(); float stopY = startY; canvas.drawLine(startX, startY, stopX, stopY, mTabLinePaint); //x轴 canvas.drawLine(startX, startY, startX, getPaddingTop(), mTabLinePaint); //y轴 //画标尺 float height = startY; float ceilHeight = (mHeight - mAxisTitleSize) / mLabelCount; for (int i = 0; i < mLabelCount; i++) { canvas.save(); canvas.translate(0, -ceilHeight * i); canvas.drawLine(startX, startY, stopX, stopY, mTabLinePaint); canvas.restore(); } }}
5、总结
在xml文件中的使用与普通view用法相同:
<widget.qike.com.columnview.widget.ColumnView android:id="@+id/cv_test" android:layout_width="match_parent" android:layout_height="match_parent"/>
demo下载地址
- 自定义view:快速实现柱状图的绘制
- 实现自定义View的绘制
- Android自定义View实现简单的折线图、柱状图
- [AndroidUI]自定义view(五):实现动态柱状图
- Android 自定义view实现动态柱状图。
- Android 项目(一):自定义View绘制“柱状图”
- 自定义view的绘制
- 自定义view 的绘制
- 二、关于自定义view的文章:柱状图
- 自定义view之柱状图
- 自定义View之柱状图
- 自定义view柱状图
- android自定义View实现图片的绘制、旋转、缩放
- Android 自定义View 实现方向盘控件的绘制
- 自定义view--虚线的绘制
- view类的自定义绘制
- Android自定义View的绘制
- 自定义view的绘制流程
- 使用Metrics监控应用程序的性能
- 热修复And插件化学习总结
- EditText 屏蔽删除键
- angular引入模板报错解决
- PuTTY 颜色配置
- 自定义view:快速实现柱状图的绘制
- Unity3D脚本:相机旋转代码
- CoreData按时间查询最新20条数据
- android不知不觉偷拍他人功能实现(手机关闭依然拍照)
- Kendo UI单页面程序中文文档 (赞)
- 第16周oj训练——结构体--学生信息排序(2878)
- SEAndroid安全机制简要介绍和学习计划
- DML、DDL、DCL区别
- mysql安全