Android 进阶自定义View(3)图表统计BarChartView柱状图的实现
《一》导语:
最近写了几个统计数据相关的图表,刚好放在自定义View这块的跟大家分享一下。对于图表这块,可能对于很多App的开发都用得不是很多,但是对于一些有数据分析统计需求相关的,例如P2P类型的,就比较常用了。不过我们可能也就需要用到那么一两个图表,例如:曲线图、折线图、柱状图,饼状图等等,只是对于不同的业务需求,页面设计会有所差别,我们需要实现的效果可能也会有一些差别。接下来的几篇文章我会逐一介绍以下几个常用的图表统计图的实现过程,都是一个类完成一个图表的实现,很轻量,希望对各位读者有所帮助:
-
柱状图
-
曲线图 / 折线图
-
饼状图
《二》小小的建议:
当遇到一些我们用系统的控件无法实现的需求的时候,我们就要去思考要不要自定义实现了,所以遇到这种需求,建议先不要急着去找网上写好的图表库,当然有一些写得很好的库,但是建议你百度后,只是仅限于参考学习,而不是去单纯的修修改改。
两点原因:
1、现成的UI库,往往功能太多,你不仅要筛选出合适的那个,往往还需要按照自己的设计图去修改UI,而修改别人的代码,是相当费时的一件事。
2、 修改别人写好的东西,一定程度上也能学习,但是肯定是没有自己动手学习得更多,而且自己写的,以后有变动,想咋改咋改。所以呢,还不如自己动手写一个,其实也没那么难。今天讲的是柱状统计图,先看一下我实现的效果图,为了好看点,还整了个背景图。
《三》根据下图,分析一下View绘制的思路:
1、确定坐标原点,作为绘制的参考点
2、绘制横向的X轴及上方的刻度线
3、绘制Y轴及Y轴的刻度文字值
4、绘制不同颜色的柱状条、X轴刻度值及柱状条上方的值
5、测量View需要的最大宽高。
涉及到的几个绘制图形的方法:drawLine(),drawText(),drawRect(),灰常的简单。
(1)通过上图可以确定绘制起点(startX,starty)的坐标。
//文字+刻度宽度+文字与刻度之间间距 startX = mMaxTextWidth + keduWidth+ keduTextSpace; //坐标原点 y轴起点。 isShowValueText :是否要展示柱状条上的值 startY = keduSpace * (yAxisList.size() - 1) + mMaxTextHeight + (isShowValueText ? keduTextSpace : 0);
(2)确定好绘制起点的坐标,一切都很简单了,接下来就是按照步骤,一点点实现,总的代码很简单,注释也比较清楚,这里就不赘述了,文末有完整的代码。
(3)有个小细节在这里说一下下。一般而言呢,X轴坐标,是能确定的,比如月份、年份,直接根据接口数据传过来稍微处理一下就行。但是Y轴坐标,一般是随着时间的推移变动的,当你的Y轴坐标的范围不确定的时候,这个时候你就必须处理一下Y轴的坐标数据,使其能够动态跟着实际数据的变化而变化,以免超出坐标轴范围,下面是我项目中用到的一种处理的方法,大家可以参考一下。由于我的测试的Activity是Kotlin写的,我就直接用Kotlin代码了,看一下大概思路就行。
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) var datas = listOf<Int>(40, 76, 90, 50, 187) var xList = listOf<String>("1月份", "2月份", "3月份", "4月份", "5月份") //根据数据的最大值生成上下对应的Y轴坐标范围 var ylist = mutableListOf<Int>() var maxYAxis: Int? = Collections.max(datas) if (maxYAxis!! % 2 == 0) { maxYAxis = maxYAxis + 2 } else { maxYAxis = maxYAxis + 1 } var keduSpace = (maxYAxis / datas.size) + 1 for (i in 0..datas.size) { ylist.add(0 + keduSpace * i) } barchartview.updateValueData(datas, xList, ylist) } }
(4)下面是BarChartView完整代码:
package com.example.jojo.learn.customview; import android.content.Context; import android.graphics.BlurMaskFilter; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.util.Log; import android.view.View; import com.example.jojo.learn.R; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; /** * Created by JoJo on 2018/8/2. * wechat:18510829974 * description:柱状统计图 */ public class BarChartView extends View { private Context mContext; private Paint mPaintBar; private Paint mPaintLline; private Paint mPaintText; //柱状条对应的颜色数组 private int[] colors; private int keduTextSpace = 10;//刻度与文字之间的间距 private int keduWidth = 20; //坐标轴上横向标识线宽度 private int keduSpace = 100; //每个刻度之间的间距 px private int itemSpace = 60;//柱状条之间的间距 private int itemWidth = 100;//柱状条的宽度 //刻度递增的值 private int valueSpace = 40; //绘制柱形图的坐标起点 private int startX; private int startY; private int mTextSize = 25; private int mMaxTextWidth; private int mMaxTextHeight; private Rect mXMaxTextRect; private Rect mYMaxTextRect; //是否要展示柱状条对应的值 private boolean isShowValueText = true; //数据值 private List<Integer> mData = new ArrayList<>(); private List<Integer> yAxisList = new ArrayList<>(); private List<String> xAxisList = new ArrayList<>(); public BarChartView(Context context) { this(context, null); } public BarChartView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public BarChartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.mContext = context; colors = new int[]{ContextCompat.getColor(context, R.color.color_07f2ab), ContextCompat.getColor(context, R.color.color_79d4d8), ContextCompat.getColor(context, R.color.color_4388bc), ContextCompat.getColor(context, R.color.color_07f2ab), ContextCompat.getColor(context, R.color.color_4388bc)}; init(context, false); } private void init(Context context, boolean isUpdate) { if (!isUpdate) { initData(); } //设置边缘特殊效果 BlurMaskFilter PaintBGBlur = new BlurMaskFilter( 1, BlurMaskFilter.Blur.INNER); //绘制柱状图的画笔 mPaintBar = new Paint(); mPaintBar.setStyle(Paint.Style.FILL); mPaintBar.setStrokeWidth(4); mPaintBar.setMaskFilter(PaintBGBlur); //绘制直线的画笔 mPaintLline = new Paint(); mPaintLline.setColor(ContextCompat.getColor(context, R.color.color_274782)); mPaintLline.setAntiAlias(true); mPaintLline.setStrokeWidth(2); //绘制文字的画笔 mPaintText = new Paint(); mPaintText.setTextSize(mTextSize); mPaintText.setColor(ContextCompat.getColor(context, R.color.color_a9c6d6)); mPaintText.setAntiAlias(true); mPaintText.setStrokeWidth(1); mYMaxTextRect = new Rect(); mXMaxTextRect = new Rect(); mPaintText.getTextBounds(Integer.toString(yAxisList.get(yAxisList.size() - 1)), 0, Integer.toString(yAxisList.get(yAxisList.size() - 1)).length(), mYMaxTextRect); mPaintText.getTextBounds(xAxisList.get(xAxisList.size() - 1), 0, xAxisList.get(xAxisList.size() - 1).length(), mXMaxTextRect); //绘制的刻度文字的最大值所占的宽高 mMaxTextWidth = mYMaxTextRect.width() > mXMaxTextRect.width() ? mYMaxTextRect.width() : mXMaxTextRect.width(); mMaxTextHeight = mYMaxTextRect.height() > mXMaxTextRect.height() ? mYMaxTextRect.height() : mXMaxTextRect.height(); if (yAxisList.size() >= 2) { valueSpace = yAxisList.get(1) - yAxisList.get(0); } //文字+刻度宽度+文字与刻度之间间距 startX = mMaxTextWidth + keduWidth + keduTextSpace; //坐标原点 y轴起点 startY = keduSpace * (yAxisList.size() - 1) + mMaxTextHeight + (isShowValueText ? keduTextSpace : 0); } /** * 初始化数据 */ private void initData() { int[] data = {80, 160, 30, 40, 100}; for (int i = 0; i < 5; i++) { mData.add(data[i]); yAxisList.add(0 + i * valueSpace); } String[] xAxis = {"1月", "2月", "3月", "4月", "5月"}; for (int i = 0; i < mData.size(); i++) { xAxisList.add(xAxis[i]); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); Log.e("TAG", "onMeasure()"); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (heightMode == MeasureSpec.AT_MOST) { if (keduWidth > mMaxTextHeight + keduTextSpace) { heightSize = (yAxisList.size() - 1) * keduSpace + keduWidth + mMaxTextHeight; } else { heightSize = (yAxisList.size() - 1) * keduSpace + (mMaxTextHeight + keduTextSpace) + mMaxTextHeight; } heightSize = heightSize + keduTextSpace + (isShowValueText ? keduTextSpace : 0);//x轴刻度对应的文字距离底部的padding:keduTextSpace } if (widthMode == MeasureSpec.AT_MOST) { widthSize = startX + mData.size() * itemWidth + (mData.size() + 1) * itemSpace; } Log.e("TAG", "heightSize=" + heightSize + "widthSize=" + widthSize); //保存测量结果 setMeasuredDimension(widthSize, heightSize); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Log.e("TAG", "onDraw()"); //从下往上绘制Y 轴 canvas.drawLine(startX, startY + keduWidth, startX, startY - (yAxisList.size() - 1) * keduSpace, mPaintLline); for (int i = 0; i < yAxisList.size(); i++) { //绘制Y轴的文字 Rect textRect = new Rect(); mPaintText.getTextBounds(Integer.toString(yAxisList.get(i)), 0, Integer.toString(yAxisList.get(i)).length(), textRect); canvas.drawText(Integer.toString(yAxisList.get(i)), (startX - keduWidth) - textRect.width() - keduTextSpace, startY - (i + 1) * keduSpace + keduSpace, mPaintText); //画X轴及上方横向的刻度线 canvas.drawLine(startX - keduWidth, startY - keduSpace * i, startX + mData.size() * itemWidth + itemSpace * (mData.size() + 1), startY - keduSpace * i, mPaintLline); } for (int j = 0; j < xAxisList.size(); j++) { //绘制X轴的文字 Rect rect = new Rect(); mPaintText.getTextBounds(xAxisList.get(j), 0, xAxisList.get(j).length(), rect); canvas.drawText(xAxisList.get(j), startX + itemSpace * (j + 1) + itemWidth * j + itemWidth / 2 - rect.width() / 2, startY + rect.height() + keduTextSpace, mPaintText); if (isShowValueText) { Rect rectText = new Rect(); mPaintText.getTextBounds(mData.get(j) + "", 0, (mData.get(j) + "").length(), rectText); //绘制柱状条上的值 canvas.drawText(mData.get(j) + "", startX + itemSpace * (j + 1) + itemWidth * j + itemWidth / 2 - rectText.width() / 2, (float) (startY - keduTextSpace - (mData.get(j) * (keduSpace * 1.0 / valueSpace))), mPaintText); } //绘制柱状条 mPaintBar.setColor(colors[j]); //(mData.get(j) * (keduSpace * 1.0 / valueSpace)):为每个柱状条所占的高度值px int initx = startX + itemSpace * (j + 1) + j * itemWidth; canvas.drawRect(initx, (float) (startY - (mData.get(j) * (keduSpace * 1.0 / valueSpace))), initx + itemWidth, startY, mPaintBar); } } /** * 根据真实的数据刷新界面 * * @param datas * @param xList * @param yList */ public void updateValueData(@NotNull List<Integer> datas, @NotNull List<String> xList, @NotNull List<Integer> yList) { this.mData = datas; this.xAxisList = xList; this.yAxisList = yList; init(mContext, true); invalidate(); } }
大家如果需要学习一些负责的图表统计图,可以参考以下几个强大的图表库:

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Android中的设计模式之代理模式
参考 《设计模式:可复用面向对象软件的基础 》4.7 Proxy 代理--对象结构型模式 《Android源码设计模式解析与实战》第18章 编程好帮手--代理模式 意图 为其它对象提供一种代理以控制对这个对象的访问 适用性 在需要用比较通用和复杂的对象指针代替简单的指针的时候,使用Proxy模式。 远程代理 为一个对象在不同的地址空间提供局部代表。这一点Android Binde实现跨进程通信很典型 虚代理 根据需要创建开销很大的对象。 保护代理 控制对原始对象的访问。保护代理用于对象应该有不同的访问权限的时候。 智能指引 取代了简单的指针,它在访问对象时执行一些附加操作。 对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放它,难道Java就是这个原理? 当第一次引用一个持久对象时,将它装入内存 在访问一个实际对象前,检查是否已经锁定了它,以确保其它对象不能改变它。 结构 代理模式结构 角色 Client 客户类,即使用代理类的类型 Subject 抽象主题类 抽象了代理行为 主要职责是声明真实主题与代理的共同接口关系,该类既可以是一个抽象类也可以是一个接口。 RealSu...
- 下一篇
细谈证书与Provisioning Profile
iOS程序员大多对证书和Provisioning Profile懵逼过吧,是时候整理一下思路了,把这个问题讲讲清楚。所有配置都在https://developer.apple.com,大家都可以上去摸索一下。 证书 打开钥匙串访问可以看到里面有证书和我的证书两项,其中证书包含系统安装的所有证书,我的证书则仅包含电脑上有私钥的证书。 私钥是用来签名的,通过签名可以确保程序是没有被篡改的。其中私钥放在自己电脑上,公钥则放在苹果服务器上。 模拟器运行App是不需要签名的,真机调试和上传AppStore的包都需要签名,主程序和所有的动态库都要签名。 //模拟器也调用了codesign,但是没有选择证书。 CodeSign /Users/henshao/Library/Developer/Xcode/DerivedData/CloudConsoleA
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS6,CentOS7官方镜像安装Oracle11G
- Mario游戏-低调大师作品
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS关闭SELinux安全模块
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果