近期要做一些图表类的需求,一開始就去github上看了看,发现开源的图表框架还是蛮多的。可是非常少有全然符合我的需求的。另外就是使用起来比較麻烦。所以就决定自己来造轮子了~~~

今天要介绍的就是Android图标系列中点阵图(姑且这么叫着吧╮(╯▽╰)╭)的画法。

效果图例如以下:

需求:

1. 给出几个点 画出坐标轴(用虚线)

2. 画出相应的点 在点的上方标出数值

3. 下方要显示个数值表示的意义

4. 重点!!

!动态计算坐标轴,多余的坐标不能显示。

前三条好理解。第四条啥意思呢~

(比方说,我们数据是{10.1,12.5, 20.2, 15.1} 那么我们的坐标轴起点就应该是从10開始画,而不是0!)例如以下图所看到的

接下来我们就来一步步实现这些需求吧~

1.定义数据模型

**
* Created by JK on 2016/1/18.
* 点阵折线图的数据模型
*/
public class DotChartItem {
public int color;//颜色
public float value;//值
public String title;//标题 public DotChartItem(int color, float value, String title) {
this.color = color;
this.value = value;
this.title = title; } }

2.自己定义View —— JKLineChart

首先是初始化部分

/**
* Created by JK on 16/1/18.
* 用毛的开源项目自己写控件之点阵图
* 这是一个点阵图
*/
public class JKLineChart extends View { private static final String TAG = "JKLineChart";
private Context mContext;
private Paint mCoordPaint;//坐标线的Paint
private Paint mIndicPaint; //点图的Paint
private Paint mHintTitlePaint; //提示块的Paint
private int mWidth;
private int mHeight;
private int mCoordColor = Color.GRAY; //坐标线的color
private int mPadding = 20;
private int mGapHeight = 50;//坐标轴之间的间隔
private float mTextX = 0f; //提示模块中。第一个方块的X坐标(和坐标上的字分离)
private float[] mMinAndMaxPointY = new float[2]; //最低点和最高点的Y坐标(參考点 用来计算方块的坐标)
private List<LineChartItem> mItemList; //数据集合 private List<LineChartItem> mTypeSet;//下方提示模块的数据集合
private float[] values; enum IndicatorType { //点阵图 图形类型
CIRCLE, //圆
RECTANGLE //方块
} private IndicatorType mIndicatorType = IndicatorType.RECTANGLE;
private int mGap = 5; //坐标轴之间的间隔
private int mIndicWidth = 5; //小方块的宽度 或者小圆圈的直径
//以下2个值用来做动画
private int mInitWidth = 0;
private int mSpeed = 1; private boolean isShowHintBlock = true; //是否显示底下的提示模块
private int mHintTitleHeight = 30; //提示模块高度
private final double EP = 0.000000001; public JKLineChart(Context context) {
this(context, null);
} public JKLineChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
} public JKLineChart(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.JKLineChart, 0, 0);
try {
isShowHintBlock = typedArray.getBoolean(R.styleable.JKLineChart_showHintBlock, true);
//坐标轴之间的间隔 默觉得5
mGap = typedArray.getInt(R.styleable.JKLineChart_coordGap, mGap);
//点是用圆圈还是用方块来画
int isRound = typedArray.getInt(R.styleable.JKLineChart_indicatorType, 1);
Log.d(TAG, "ROUND :" + isRound);
if (isRound == 0) {
mIndicatorType = IndicatorType.CIRCLE;
} else {
mIndicatorType = IndicatorType.RECTANGLE;
} } finally {
typedArray.recycle();
} init(context);
}

上面的操作主要是定义一些变量。并从构造函数中获取我们的自己定义属性

 private void init(Context context) {
this.mContext = context;
mItemList = new ArrayList<LineChartItem>();
//mPadding = dip2px(context,mPadding);
mIndicWidth = dip2px(context, mIndicWidth);
mHintTitleHeight = dip2px(context, 40);
mGapHeight = dip2px(context,mGapHeight);
initPaint();
}
//初始化画笔
private void initPaint() {
mIndicPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCoordPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mHintTitlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCoordPaint.setColor(mCoordColor);
mCoordPaint.setTextSize(40);
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
this.mWidth = width;
this.mHeight = height;
if (isShowHintBlock) {
mHeight = mHeight - mHintTitleHeight;
}
Log.d(TAG, "onSizeChanged"); }

mHeight 代表的是点阵图的高度(不包含下方的提示部分)。假设要显示下方的提示部分,就要减去提示部分的高度。

以下就是最重点。最基本的onDraw方法喽

private void animatedDraw(Canvas canvas) {
if (mItemList == null || mItemList.size() == 0)
return; drawCoordText(canvas);
drawPoint(canvas);
if (isShowHintBlock) {
drawHintTitle(canvas);
} }

分三步走,

第一 drawCoordText画坐标线

第二 drawPoint画点

第三 drawHintTitle画下方提示部分

/**
* 画坐标线
* @param canvas
*/
private void drawCoordText(Canvas canvas) {
//获取坐标上下限的值
float[] array = getMinAndMaxCoord();
//坐标轴总数量
float totalCount = (array[1] -array[0])/ mGap +1;
for (float i = array[1], count = 0; i >= array[0]; i = i - mGap, count++) {
String text = (int) i + "";
Rect textRect = new Rect();
mCoordPaint.getTextBounds(text, 0, text.length(), textRect);
float y = mPadding*3 + mGapHeight*count;
if (count == 0) {
mMinAndMaxPointY[1] = y; //最高点的坐标
} else if (count == totalCount - 1) {
mMinAndMaxPointY[0] = y; //最低点的坐标
}
canvas.drawText(text, mPadding, y, mCoordPaint);
//canvas.drawLine(mPadding,y,mWidth,y,mCoordPaint);
//画虚线坐标线
PathEffect effects = new DashPathEffect(new float[]{5, 10}, 1);
Path path = new Path();
path.moveTo(mPadding * 2f + textRect.width(), y - textRect.height() / 2);
if (mTextX >= -EP && mTextX <= EP) {
mTextX = mPadding * 2f + textRect.width();
}
path.lineTo(mWidth - mPadding, y - textRect.height() / 2);
mCoordPaint.setStyle(Paint.Style.STROKE);
mCoordPaint.setPathEffect(effects);
canvas.drawPath(path, mCoordPaint); }
}

这里有两个辅助函数非常重要!

。!堪称画坐标线的核心!

前面提到过,我们的坐标轴是依据传入的数值动态计算的,而不是直接从0開始画的。

 private float[] getMaxAndMin(float[] array) {
float min, max;
min = max = array[0];
float[] result = new float[2];
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
if (Float.compare(array[i], max) > 0) // 推断最大值
max = array[i];
if (Float.compare(array[i], min) < 0) // 推断最小值
min = array[i];
}
result[0] = min;
result[1] = max;
return result;
}

上面这个函数用来获取一个数组中的最大最小值,不管数组是否排序,这个比較简单。

/**
* 动态计算最小坐标值和最大坐标值x
* ps:{4.9f,6f,7f,8f,9.1f} 坐标最小值为 = 0; 坐标最大值 = 10;
*
* @return
*/
private float[] getMinAndMaxCoord() {
float[] array = getMaxAndMin(values);
float[] result = new float[2];
float min = array[0];
float max = array[1];
result[0] = (Math.round((min - 0.5f)) / mGap * mGap); //坐标值最小值
result[1] = ((int) (floor((max + mGap - 0.5f)) / mGap) * mGap); //坐标值最大值
return result;
}

上面这个函数用来计算坐标轴的最小值和最大值,

比方数组是{4.9f,6f,7f,8f,9.1f} 坐标最小值为 = 0; 坐标最大值 = 10;

数组是{5.1f,6f,7f,8f,10.1f} 坐标最小值为 = 5; 坐标最大值 = 15;

floor是我们重写了地板除法

 /**
* 重写floor除法
*
* @param value
* @return
*/
private float floor(float value) {
float result = 0f;
float tail = value % 1;
int integer = (int) (value - tail);
if (Float.compare(tail, 0.5f) > 0) { return integer + 1;
} else if (Float.compare(tail, 0.5f) <= 0) { return integer;
}
return result;
}

第二步: 画点 /**
* 画小方块
*
* @param canvas
*/
private void drawPoint(Canvas canvas) {
float totalWidth = mWidth - 2 * mPadding - mTextX; //全部方块所能占领的总面积
int size = values.length;
for (int i = 0; i <size; i++) {
float value = values[i];
int color = mItemList.get(i).color;
mIndicPaint.setColor(color);
mIndicPaint.setTextSize(40);
String text = value + "";
Rect textRect = new Rect();
mIndicPaint.getTextBounds(text, 0, text.length(), textRect);
if (mIndicatorType == IndicatorType.RECTANGLE) {
float top = getPointYByValue(value);
float left = mPadding + mTextX + totalWidth / size * i + (totalWidth / size - mInitWidth) / 2;
float right = left + mInitWidth;
float bottom = top + mInitWidth;
RectF indicRect = new RectF(left, top, right, bottom);
canvas.drawText(text, left, top - textRect.height(), mIndicPaint);
canvas.drawRect(indicRect, mIndicPaint);
}
}
if (mInitWidth <= mIndicWidth) {
mInitWidth += mSpeed;
invalidate();
}
}



mTextX是右側方块区域的起始坐标。用来与坐标数字分离,totalWidth是全部点所能占领的宽度,依据点的数量将totalWidth等分,然后居中显示在各自的区域里。

这里我们仅仅实现了点为方块的样式。点为圆圈的样式各位看官能够自己尝试去实现以下。

getPointYByValue函数比較重要。是依据点的值来计算坐标的。



 /**
* 依据值来计算方块的y坐标
*
* @param value
* @return
*/
private float getPointYByValue(float value) {
float[] array = getMinAndMaxCoord();
float diffY = mMinAndMaxPointY[0] - mMinAndMaxPointY[1]; //坐标值之间的差值
float diffValue = array[1] - array[0];
float y = mMinAndMaxPointY[1] + diffY/diffValue* (array[1]-value)-mIndicWidth*3/2;
return y;
}

最后 就是画底部的提示区域了

 public void getHintTitleList() {
mTypeSet = new ArrayList<LineChartItem>(mItemList);
for (int i = 0; i < mTypeSet.size() - 1; i++) {
for (int j = mTypeSet.size() - 1; j > i; j--) {
if (mTypeSet.get(j).title.equals(mTypeSet.get(i).title)) {
mTypeSet.remove(j);
}
}
}
Collections.sort(mTypeSet, new Comparator<LineChartItem>() {
@Override
public int compare(LineChartItem lhs, LineChartItem rhs) {
return -(Float.compare(lhs.value, rhs.value));
}
});
} private void drawHintTitle(Canvas canvas) {
getHintTitleList();
int totalWidth = mWidth - mPadding * 2;
int width = totalWidth / mTypeSet.size();
float startY = mMinAndMaxPointY[0] + mPadding * 3;
for (int i = 0; i < mTypeSet.size(); i++) {
//draw
LineChartItem type = mTypeSet.get(i);
String text = type.title;
int color = type.color;
mHintTitlePaint.setColor(color);
mHintTitlePaint.setTextSize(40);
Rect textRect = new Rect();
mHintTitlePaint.getTextBounds(text, 0, text.length(), textRect);
float x = (width - (mIndicWidth*2 + mPadding + textRect.width())) / 2 + i * width;
RectF indicRect = new RectF(x + mPadding, startY, x + mIndicWidth*2 + mPadding, startY + mIndicWidth*2);
canvas.drawRect(indicRect, mHintTitlePaint);
mHintTitlePaint.setColor(mCoordColor);
canvas.drawText(text, mPadding * 3 + x, startY + mIndicWidth*2, mHintTitlePaint);
} }

getHintTitleList将我们传入的数组筛选出提示标题,并依照数值排序。

最最后~!

!。,因为我们的坐标可能有非常多。0-——无穷都有可能,所以我们自己定义View的高度不是固定的,须要动态计算出来。

public void setItemList(List<LineChartItem> list) {
if(list.size()==0 || list == null)
return;
mItemList.clear();
mItemList.addAll(list);
int size = mItemList.size();
values = new float[mItemList.size()];
for (int i = 0; i < size; i++) {
values[i] = mItemList.get(i).value;
}
computerHeight();
invalidate();
} private void computerHeight() {
float[] array = getMinAndMaxCoord();
//坐标轴总数量
float totalCount = (array[1] -array[0])/ mGap ;
//上面折线图的最大Y坐标
float y = mPadding*3 + mGapHeight*totalCount;
//以下提示部分的起始Y坐标
float startY = y + mPadding * 3;
int height = (int) (startY+mIndicWidth*2+mPadding*2);
ViewGroup.LayoutParams lp = getLayoutParams();
lp.height = height;
this.setLayoutParams(lp);
Log.d(TAG,"HEIGHT:"+height); }

每次我们填充时间的时候,都会先调用computerHeight来动态计算视图高度

使用方式 SO EASY~!

 LineChartItem item1 = new LineChartItem(Color.parseColor("#00ff00"), 12.5f, "正常");
LineChartItem item2 = new LineChartItem(Color.parseColor("#0000ff"), 10.1f, "偏低");
LineChartItem item3 = new LineChartItem(Color.parseColor("#FF0000"), 20.2f, "偏高");
LineChartItem item4 = new LineChartItem(Color.parseColor("#FF0000"), 15.1f, "偏高");
List<LineChartItem> list = new ArrayList<LineChartItem>();
list.add(item1);
list.add(item2);
list.add(item3);
list.add(item4);
mLineChart.setItemList(list);

你仅仅用将数据传递给LineChart 。剩下的工作就会自己主动完毕了,就是这么简单,全然没有网上那些开源控件用起来那么复杂吧~~

源代码 我的github:https://github.com/devilthrone/JKChart

欢迎fork and starO(∩_∩)O

OVER!

最新文章

  1. 卡尔曼滤波器 Kalman Filter (转载)
  2. getRequestURI,getRequestURL的区别
  3. 异步get请求之Block方法
  4. linux设备驱动归纳总结(五):1.在内核空间分配内存【转】
  5. 2007 Asia - Nanjing F题,字典树
  6. LaTeX Pdf to Word
  7. 【转】 Android项目的mvc模式
  8. 关于WinCC OA
  9. List,DataTable实现行转列的通用方案
  10. 注意兼容浮点运算误差 0.7 + 0.1 ==0.8 为false
  11. ssh整合not found class 异常总结
  12. Spring学习10- bean的生命周期(阿里面试题目两次面试均提到)
  13. 《java虚拟机》----虚拟机字节码执行引擎
  14. 对alpha发布的总结技术随笔
  15. AOJ.综合训练.2016-12-1
  16. Linux_信号与信号量【转】
  17. 并发测试 java.lang.OutOfMemoryError: GC overhead limit exceeded Xms Xmx 阻塞请求 单节点 请求分发 负载均衡
  18. VS2012 未找到与约束ContractName Microsoft.VisualStudio.Text.ITextDocumentFactoryService 未找到与约束ContractName,无法打开项目的解决方案 SQLyog 注册码
  19. java流、文件以及IO
  20. HDU3415【单调队列】

热门文章

  1. swift语言点评三 - Basic Operators
  2. Vue this.$router.push、replace、go的区别
  3. React显示文件夹中SVG
  4. php xml 的基本操作类
  5. web移动端-弹性盒模型
  6. hdu5321 beautiful set(莫比乌斯反演)
  7. 带入gRPC:gRPC Deadlines
  8. 记录python之递归函数
  9. 紫书 例题 11-2 UVa 1395(最大边减最小边最小的生成树)
  10. 素数计数函数$\pi(x)\sim \Theta(\frac{x}{\log{x}})$的一个初等方法——素数定理的估计