Android 进阶自定义View(5)图表统计PieChartView圆饼图的实现
今天讲图表统计中比较常用的一个,像支付宝的月账单啥的,都是用圆饼图来做数据统计的,先看一下我最终实现的效果图:
该效果实际上是两个实心圆叠加后的效果。
《一》View实现思路分析:
(1)根据占比集合数据,计算所需绘制的角度,动态设置画笔颜色,drawArc()绘制外圆弧
(2)drawCircle()绘制内圆
(3)确定每块圆饼的小白点的位置
(4)绘制白点的沿线和占比文字
《二》具体实现:
(1)绘制不同颜色的圆饼
for (int i = 0; i < mRateList.size(); i++) { mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(mColorList.get(i)); // Log.e("TAG", "startAngle=" + startAngle + "--sweepAngle=" + ((int) (mRateList.get(i) * (360)) - offset)); canvas.drawArc(rectF, startAngle, (int) (mRateList.get(i) * (360)) , true, mPaint); startAngle = startAngle + (int) (mRateList.get(i) * 360); }
(2)绘制内圆
mPaint.setColor(ContextCompat.getColor(mContext, R.color.color_081638)); canvas.drawCircle(radius + centerPointRadius + (xOffset + yOffset + textRect.width()), radius + centerPointRadius + (xOffset + yOffset + textRect.height()), radius / 1.5f, mPaint);
(3)确定每块圆饼的小白点的位置,通过每段圆饼的起始角度确定该段圆弧的中心点位置。
private void dealPoint(RectF rectF, float startAngle, float endAngle, List<Point> pointList) { Path path = new Path(); //通过Path类画一个90度(180—270)的内切圆弧路径 path.addArc(rectF, startAngle, endAngle); PathMeasure measure = new PathMeasure(path, false); // Log.e("路径的测量长度:", "" + measure.getLength()); float[] coords = new float[]{0f, 0f}; //利用PathMeasure分别测量出各个点的坐标值coords int divisor = 1; measure.getPosTan(measure.getLength() / divisor, coords, null); // Log.e("coords:", "x轴:" + coords[0] + " -- y轴:" + coords[1]); float x = coords[0]; float y = coords[1]; Point point = new Point(Math.round(x), Math.round(y)); pointList.add(point); }
(4)绘制以白点为起点的折线和占比文字。有个细节需要注意一下,绘制折线和比例文字时,每部分沿线和文字的绘制规则不一样,我是按下面的规则处理的:将圆分为四部分,每块区分显示。
//折线横向长度 private int xOffset; //折线偏Y方向长度 private int yOffset; private void dealRateText(Canvas canvas, Point point, int position, List<Point> pointList) { if (position == 0) { lastPoint = pointList.get(0); } else { lastPoint = pointList.get(position - 1); } float[] floats = new float[8]; floats[0] = point.x; floats[1] = point.y; //右半圆 if (point.x >= radius + centerPointRadius + (xOffset + yOffset + textRect.width())) { mPaint.setTextAlign(Paint.Align.LEFT); floats[6] = point.x + xOffset; if (point.y <= radius + centerPointRadius + (xOffset + yOffset + textRect.height())) { //右上角 floats[2] = point.x + yOffset; floats[3] = point.y - yOffset; floats[4] = point.x + yOffset; floats[5] = point.y - yOffset; floats[7] = point.y - yOffset; } else { //右下角 floats[2] = point.x + yOffset; floats[3] = point.y + yOffset; floats[4] = point.x + yOffset; floats[5] = point.y + yOffset; floats[7] = point.y + yOffset; } //左半圆 } else { mPaint.setTextAlign(Paint.Align.RIGHT); floats[6] = point.x - xOffset; //防止相邻的圆饼绘制的文字重叠显示 if (point.y <= radius + centerPointRadius) { //左上角 floats[2] = point.x - yOffset; floats[3] = point.y - yOffset; floats[4] = point.x - yOffset; floats[5] = point.y - yOffset; floats[7] = point.y - yOffset; } else { //左下角 floats[2] = point.x - yOffset; floats[3] = point.y + yOffset; floats[4] = point.x - yOffset; floats[5] = point.y + yOffset; floats[7] = point.y + yOffset; } } //根据每块的颜色,绘制对应颜色的折线 // mPaint.setColor(mRes.getColor(colorList.get(position))); mPaint.setColor(ContextCompat.getColor(mContext, R.color.color_b69b4f)); //画圆饼图每块边上的折线 canvas.drawLines(floats, mPaint); mPaint.setStyle(Paint.Style.STROKE); //绘制显示的文字,需要根据类型显示不同的文字 if (mRateList.size() > 0) { //Y轴:+ textRect.height() / 2 ,相对沿线居中显示 canvas.drawText(getFormatPercentRate(mRateList.get(position) * 100) + "%", floats[6], floats[7] + textRect.height() / 2, mPaint); } }
完整代码:
package com.example.jojo.learn.customview; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathMeasure; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.view.View; import com.example.jojo.learn.R; import com.example.jojo.learn.utils.DP2PX; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.List; /** * Created by JoJo on 2018/8/6. * wechat:18510829974 * description: 饼状图 */ public class PieView extends View { private Context mContext; private Paint mPaint; //每块占比的绘制的颜色 private List<Integer> mColorList = new ArrayList<>(); //圆弧占比的集合 private List<Float> mRateList = new ArrayList<>(); //是否展示文字 private boolean isShowRateText; //圆弧半径 private float radius; private int startAngle = 0; //不同色块之间是否需要空隙offset private int offset = 0; //圆弧中心点小圆点的圆心半径 private int centerPointRadius; private float showRateSize; private Rect textRect; //折线横向长度 private int xOffset; //折线偏Y方向长度 private int yOffset; private float mChangeAngle; private boolean isAnimation; private int sign = 0; public PieView(Context context) { this(context, null); } public PieView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public PieView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mContext = context; init(); initData(); } private void initData() { float[] rate = {30f, 40f, 15f, 15f}; int[] colors = {Color.RED, Color.BLUE, Color.YELLOW, Color.GRAY}; for (int i = 0; i < rate.length; i++) { mRateList.add(rate[i] / 100); mColorList.add(colors[i]); } textRect = new Rect(); if (mRateList.size() > 0) { mPaint.getTextBounds((mRateList.get(0) + "%"), 0, (mRateList.get(0) + "%").length(), textRect); } } private void init() { radius = DP2PX.dip2px(mContext, 80); centerPointRadius = DP2PX.dip2px(mContext, 2); xOffset = DP2PX.dip2px(mContext, 20); yOffset = DP2PX.dip2px(mContext, 5); showRateSize = DP2PX.dip2px(mContext, 10); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.RED); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL); mPaint.setTextSize(showRateSize); if (mRateList.size() > 0) { textRect = new Rect(); mPaint.getTextBounds((mRateList.get(0) + "%"), 0, (mRateList.get(0) + "%").length(), textRect); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); 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) { //边沿线和文字所占的长度:(xOffset + yOffset + textRect.width()) heightSize = (int) (radius * 2) + 2 * centerPointRadius + getPaddingLeft() + getPaddingRight() + (xOffset + yOffset + textRect.height()) * 2; } if (widthMode == MeasureSpec.AT_MOST) { widthSize = (int) (radius * 2) + 2 * centerPointRadius + getPaddingLeft() + getPaddingRight() + (xOffset + yOffset + textRect.width()) * 2; } //保存测量结果 setMeasuredDimension(widthSize, heightSize); } private int paintPosition; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //(1)绘制圆饼 RectF rectF = new RectF(0 + centerPointRadius + (xOffset + yOffset + textRect.width()), 0 + centerPointRadius + (xOffset + yOffset + textRect.height()), 2 * radius + centerPointRadius + (xOffset + yOffset + textRect.width()), 2 * radius + centerPointRadius + (xOffset + yOffset + textRect.height())); List<Point> mPointList = new ArrayList<>(); for (int i = 0; i < mRateList.size(); i++) { mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(mColorList.get(i)); // Log.e("TAG", "startAngle=" + startAngle + "--sweepAngle=" + ((int) (mRateList.get(i) * (360)) - offset)); canvas.drawArc(rectF, startAngle, (int) (mRateList.get(i) * (360)) - offset, true, mPaint); //(2)处理每块圆饼弧的中心点,绘制折线,显示对应的文字 if (isShowRateText) { dealPoint(rectF, startAngle, (mRateList.get(i) * 360 - offset) / 2, mPointList); Point point = mPointList.get(i); mPaint.setColor(Color.WHITE);//点的绘制的颜色 canvas.drawCircle(point.x, point.y, centerPointRadius, mPaint); dealRateText(canvas, point, i, mPointList); } startAngle = startAngle + (int) (mRateList.get(i) * 360); } //(3)绘制内部中空的圆 mPaint.setColor(ContextCompat.getColor(mContext, R.color.color_081638)); mPaint.setStyle(Paint.Style.FILL); canvas.drawCircle(radius + centerPointRadius + (xOffset + yOffset + textRect.width()), radius + centerPointRadius + (xOffset + yOffset + textRect.height()), radius / 1.5f, mPaint); } private Point lastPoint; private void dealRateText(Canvas canvas, Point point, int position, List<Point> pointList) { if (position == 0) { lastPoint = pointList.get(0); } else { lastPoint = pointList.get(position - 1); } float[] floats = new float[8]; floats[0] = point.x; floats[1] = point.y; //右半圆 if (point.x >= radius + centerPointRadius + (xOffset + yOffset + textRect.width())) { mPaint.setTextAlign(Paint.Align.LEFT); floats[6] = point.x + xOffset; //防止相邻的圆饼绘制的文字重叠显示 // if (lastPoint != null) { // int absX = Math.abs(point.x - lastPoint.x); // int absY = Math.abs(point.y - lastPoint.y); // if (absX > 0 && absX < 20 && absY > 0 && absY < 20) { // floats[6] = point.x + xOffset - textRect.width() / 2; // Log.e("TAG", "右半圆"); // } else { // floats[6] = point.x + xOffset; // } // } else { // floats[6] = point.x + xOffset; // } if (point.y <= radius + centerPointRadius + (xOffset + yOffset + textRect.height())) { //右上角 floats[2] = point.x + yOffset; floats[3] = point.y - yOffset; floats[4] = point.x + yOffset; floats[5] = point.y - yOffset; floats[7] = point.y - yOffset; } else { //右下角 floats[2] = point.x + yOffset; floats[3] = point.y + yOffset; floats[4] = point.x + yOffset; floats[5] = point.y + yOffset; floats[7] = point.y + yOffset; } //左半圆 } else { mPaint.setTextAlign(Paint.Align.RIGHT); floats[6] = point.x - xOffset; //防止相邻的圆饼绘制的文字重叠显示 // if (lastPoint != null) { // int absX = Math.abs(point.x - lastPoint.x); // int absY = Math.abs(point.y - lastPoint.y); // if (absX > 0 && absX < 20 && absY > 0 && absY < 20) { // floats[6] = point.x - xOffset - textRect.width() / 2; // Log.e("TAG", "左半圆"); // } else { // floats[6] = point.x - xOffset; // } // } else { // floats[6] = point.x - xOffset; // } if (point.y <= radius + centerPointRadius) { //左上角 floats[2] = point.x - yOffset; floats[3] = point.y - yOffset; floats[4] = point.x - yOffset; floats[5] = point.y - yOffset; floats[7] = point.y - yOffset; } else { //左下角 floats[2] = point.x - yOffset; floats[3] = point.y + yOffset; floats[4] = point.x - yOffset; floats[5] = point.y + yOffset; floats[7] = point.y + yOffset; } } //根据每块的颜色,绘制对应颜色的折线 // mPaint.setColor(mRes.getColor(colorList.get(position))); mPaint.setColor(ContextCompat.getColor(mContext, R.color.color_b69b4f)); //画圆饼图每块边上的折线 canvas.drawLines(floats, mPaint); mPaint.setStyle(Paint.Style.STROKE); //绘制显示的文字,需要根据类型显示不同的文字 if (mRateList.size() > 0) { //Y轴:+ textRect.height() / 2 ,相对沿线居中显示 canvas.drawText(getFormatPercentRate(mRateList.get(position) * 100) + "%", floats[6], floats[7] + textRect.height() / 2, mPaint); } } private void dealPoint(RectF rectF, float startAngle, float endAngle, List<Point> pointList) { Path path = new Path(); //通过Path类画一个90度(180—270)的内切圆弧路径 path.addArc(rectF, startAngle, endAngle); PathMeasure measure = new PathMeasure(path, false); // Log.e("路径的测量长度:", "" + measure.getLength()); float[] coords = new float[]{0f, 0f}; //利用PathMeasure分别测量出各个点的坐标值coords int divisor = 1; measure.getPosTan(measure.getLength() / divisor, coords, null); // Log.e("coords:", "x轴:" + coords[0] + " -- y轴:" + coords[1]); float x = coords[0]; float y = coords[1]; Point point = new Point(Math.round(x), Math.round(y)); pointList.add(point); } public void updateDate(List<Float> rateList, List<Integer> colorList, boolean isShowRateText) { this.isShowRateText = isShowRateText; this.mRateList = rateList; this.mColorList = colorList; init(); invalidate(); } /** * 获取格式化的保留两位数的数 */ public String getFormatPercentRate(float dataValue) { DecimalFormat decimalFormat = new DecimalFormat(".00");//构造方法的字符格式这里如果小数不足2位,会以0补足. return decimalFormat.format(dataValue); } }
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Android中的设计模式之原型模式
参考 《设计模式:可复用面向对象软件的基础 》3.4 prototype 原型--对象创建型模式 《Android源码设计模式解析与实战》第4章 使程序运行更高效 原型模式 意图 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象 使用场景 类初始化需要消化非常多的资源,这个资源包括数据,硬件资源等,通过原型拷贝避免这些消耗。 通过new产生一个对象需要非常繁琐的数据准备和访问权限,这时可以使用原型模式。 一个对象需要提供给其它调用者访问,而且各个调用者可能都需要修改其值时,可以考虑用原型模式拷贝多个对象共调用者使用,即保护性拷贝。 注意,通过实现cloneAble接口的原型模式在调用colone()方法构造实例是并不一定比new操作速度快,只有当通过new构造对象较为耗时或者成本较高时,通过clone()方法才能够获得效率上的提高。所以,在使用cloneAble时需要考虑构造对象的成本以及做一些效率上的测试。 结构 Client : 客户,使用者 protocolType: 抽象类或者接口,声明具有clone能力 ConcretePrototype: 具体的原型实现类。 ...
- 下一篇
推荐一款优雅的日历控件
原文链接:https://mp.weixin.qq.com/s/SmxDiWIidHS2hwVvFcz_hw 项目需要用到日历控件,这是我们的效果图。 去github上搜了一哈,搜到大神写的CalendarView,各种炫酷效果,我这种的也只需要自定义效果就可以了,话不多说,直接开撸! 这里附上github的链接地址:https://github.com/huanghaibin-dev/CalendarView, 里面的api文档说明还是很齐全的,这里就直接记录我的开发历程。 gradle 关联 implementation 'com.haibin:calendarview:3.4.0' 使用 刚开始布局中使用的话注意是 <com.haibin.calendarview.CalendarView /> 有包名路径的,如果直接是 <CalendarView /> 使用的是系统自带的日历控件。 <com.haibin.calendarview.CalendarView android:layout_width="match_parent" android:la...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS7设置SWAP分区,小内存服务器的救世主