java原生态的统计图控件——折线图例子

前言

由于一个项目要展示某系统内一段时间内的温度、湿度、二氧化碳浓度、光照强度等变化情况,需要用到折线图控件,便上网搜索一番,发现了AChartEngine、HoloGraphLibrary等开源控件库。

体验了一番,AChartEngine功能虽多,但不易上手,界面也不美观;HoloGraphLibrary虽然很漂亮,但功能又太少。

便决定自己开发折线图控件,锻炼锻炼。

根据需求,需要实现的功能点有:

刻度自适应(根据数据来调整刻度值及其间隔);
数据多的需要滑动展示更多。
需要注意的地方有:

节省资源,注意判断渲染的起止点;
注意临界值的处理。
 

开始
最终代码如下,附有详细注释,读者可以自行理解:

package hk.jerry.linechart;

import java.util.ArrayList;
import java.util.List;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

@SuppressLint({ "InlinedApi", "ClickableViewAccessibility" })
public class LineChartView extends View {
 private Paint paintAxis = new Paint(); //轴线与刻度画刷
 private Paint paintLine = new Paint(); //折线与标题画刷
 private Paint paintPoint = new Paint(); //数据点画刷
 private Paint paintClear = new Paint(); //清除画刷
 private String title = ""; //标题
 private int chartStartX; //图表X轴开始坐标
 private int chartStopX; //图表X轴结束坐标
 private int chartStartY; //图表Y轴开始坐标
 private int chartStopY; //图表Y轴结束坐标
 private int chartHeight; //图表高度
 private int chartWidth; //图表宽度
 private float max; //数据点最大值
 private float min; //数据点最小值
 private List dataSets = new ArrayList(); //数据集
 private int levelsX; //X轴刻度数
 private int levelsY; //Y轴刻度数
 private float spaceX; //X轴刻度间距
 private float spaceY; //Y轴刻度间距
 private int unit = 5; //刻度间的差
 private int length; //图表有效区域的X轴长度
 private int downX; //用户滑动前按下的X轴坐标
 private int offsetLeft = 50; //偏移量
 private boolean scrollabled = true; //是否可以滑动
 private boolean drew = false; //是否已绘
 private int maxDataSetsLength = -1; //最大数据集长度,-1为不限制

 public LineChartView(Context context) {
  super(context);
 }

 public LineChartView(Context context, AttributeSet attrs) {
  super(context, attrs);
 }

 @Override
 protected void onDraw(Canvas canvas) {
  super.onDraw(canvas);
  if (dataSets.isEmpty()) { //如果数据集为空则不进行绘制
   if (drew) {
    canvas.drawRect(0, 0, getWidth(), getHeight(), paintClear);
   } else {
    return;
   }
  }
  drew = true; //否则开始绘制,并标记 drew 为 true
  init();
  canvas.drawColor(Color.WHITE); //背景色
  //绘制X轴刻度、数据点、折线
  canvas.drawLine(chartStartX, chartStopY, chartStopX, chartStopY, paintAxis);
  paintAxis.setTextAlign(Align.CENTER);
  int j = (int) Math.ceil(0 - offsetLeft / spaceX) - 1;//此举是为多绘制可显示区域的前后各一个数据点作为缓冲
  for (int i = 0; i <= levelsx=""> 10 ? 10 : levelsX) + 1; i++, j++) {
   if (j < 0 || j >= dataSets.size()) { //防止数组下标越界
    continue;
   }
   DataSet currentPoint = dataSets.get(j);
   float x = getPointX(j);
   canvas.drawLine(x, chartStopY, x, chartStopY + 5, paintAxis);//绘制刻度
   canvas.drawText(currentPoint.name, x, chartStopY + 5 + 20, paintAxis);//绘制刻度文字
   //绘制折线
   float y = getPointY(currentPoint.value);
   if (j + 1 < dataSets.size()) {
    canvas.drawLine(x, y, getPointX(j + 1), getPointY(dataSets.get(j + 1).value), paintLine);
   }
   //绘制数据点
   canvas.drawCircle(x, y, 5, paintPoint);
  }
  //在绘制Y轴前覆盖越界内容
  canvas.drawRect(0, 0, chartStartX, getHeight(), paintClear);
  //绘制Y轴刻度
  canvas.drawLine(chartStartX, chartStartY, chartStartX, chartStopY, paintAxis);
  paintAxis.setTextAlign(Align.RIGHT);
  for (int i = 0; i < levelsY; i++) {
   float pointY = chartStopY - (i + 1) * spaceY; //点Y的位置
   canvas.drawLine(45, pointY, chartStartX, pointY, paintAxis); //绘制刻度
   canvas.drawText(String.valueOf((int) min + (i) * unit), 40, pointY + 8, paintAxis); //绘制刻度文字
  }
  //绘制标题
  if (title != null && !title.equals("")) {
   canvas.drawText(title, getWidth() / 2, 30, paintLine);
  }
 }

 /*
  * 根据数据点序号获得X坐标值
  */
 private float getPointX(int i) {
  return chartStartX + (i) * spaceX + offsetLeft;
 }

 /*
  * 根据数据值获得Y坐标值
  */
 private float getPointY(float value) {
  int exceedNum = (int) (Math.ceil(value - min) / unit) + 1; //计算将要逾越的刻度点数(不包含零刻线)
  float y = chartStopY - exceedNum * spaceY - (value - min) % unit / unit * spaceY;
  return y;
 }

 public void setScrollable(boolean scrollabled) {
  this.scrollabled = scrollabled;
 }

 /*
  * 设置标题
  */
 public void setTitle(String title) {
  this.title = title;
  invalidate();
 }

 /*
  * 清空图表
  */
 public void empty() {
  dataSets = new ArrayList();
  title = "";
  length = 0;
  invalidate();
 }

 /*
  * 初始化画刷与计算相关数据
  */
 private void init() {
  //计算图表
  chartStartX = 50;
  chartStopX = getWidth();
  chartStartY = 50;
  chartStopY = getHeight() - 30;
  chartWidth = chartStopX - chartStartX;
  chartHeight = chartStopY - chartStartY;
  //轴线画刷
  paintAxis.setAntiAlias(true);
  paintAxis.setStrokeWidth(3);
  paintAxis.setColor(Color.parseColor("#858585"));
  paintAxis.setTextSize(18);
  //数据点画刷
  paintPoint.setAntiAlias(true);
  paintPoint.setStrokeWidth(3);
  paintPoint.setStyle(Paint.Style.FILL_AND_STROKE);
  paintPoint.setColor(Color.parseColor("#00aa3a"));
  //折线画刷
  paintLine.setAntiAlias(true);
  paintLine.setStrokeWidth(5);
  paintLine.setTextAlign(Align.CENTER);
  paintLine.setTextSize(35);
  paintLine.setColor(Color.parseColor("#00aa3a"));
  //擦除画刷
  paintClear.setColor(Color.WHITE);
  //获得最大值与最小值,以便于计算
  min = max = dataSets.get(0).value;
  for (int i = 1; i < dataSets.size(); i++) {
   DataSet set = dataSets.get(i);
   if (set.value < min) {     min = set.value;    }    if (set.value > max) {
    max = set.value;
   }
  }
  //计算X、Y轴相关数据以便于稍后绘制
  float minus = max - min;
  unit = (int) Math.ceil(minus / 10);
  unit = unit == 0 ? 1 : unit; //如果间距为0的话,则将其改为1,防止刻度无法递增
  levelsY = (int) (Math.ceil((max - min) / unit)); //Y轴需要绘制的刻度数
  if (levelsY == 0) { //如果只有0个刻度时,即一直为持平趋势时增加一个刻度,使唯一一个刻度能绘制至中间
   levelsY++;
  }
  spaceY = chartHeight / (levelsY + 1); //Y轴刻度间的距离,+1是为了给前后留出空间
  levelsX = dataSets.size(); //X轴需要绘制的刻度数
  spaceX = chartWidth / (levelsX > 10 ? 10 : levelsX + 1); //X轴刻度间的距离,+1是为了给前后留出空间
  length = (int) (dataSets.size() * spaceX);
 }

 /*
  * 添加数据集
  */
 public void addDataSet(DataSet dataSet) {
  if (maxDataSetsLength != -1 && dataSets.size() > maxDataSetsLength) {
   dataSets.remove(0);
  }
  dataSets.add(dataSet);
  scrollToEnd();
 }

 public void setMaxDataSetsLength(int maxDataSetsLength) {
  this.maxDataSetsLength = maxDataSetsLength;
 }

 /*
  * 滚动到首部
  */
 public void scrollToStart() {
  if (!drew) {
   invalidate();
  }
  offsetLeft = 50;
  invalidate();
 }

 /*
  * 滚动到尾部
  */
 public void scrollToEnd() {
  if (!drew) {
   invalidate();
  }
  offsetLeft = chartWidth >= length ? 50 : (0 - (length - chartWidth) - 50);
  invalidate();
 }

 /*
  * 是否已滚动到尾部
  */
 public boolean isScrolledToStart() {
  return offsetLeft >= 50;
 }

 /*
  * 是否已滚动到首部
  */
 public boolean isScrolledToEnd() {
  return chartWidth >= length ? (offsetLeft <= 50) : (offsetLeft < 0 - (length - chartWidth));
 }

 /*
  * 设置数据集
  */
 public void setDataSets(List dataSets) {
  length = 0;
  this.dataSets = dataSets;
  invalidate();
  scrollToEnd();
 }

 /*
  * 触摸时间,检测用户是否滑动
  */
 @SuppressLint("ClickableViewAccessibility")
 @Override
 public boolean onTouchEvent(MotionEvent event) {
  switch (event.getAction()) {
  case MotionEvent.ACTION_DOWN:
   downX = (int) event.getRawX();
   break;
  case MotionEvent.ACTION_MOVE:
   if (!scrollabled)
    return true;
   //distanceX<0为手指向左滑动,distancex>0为手指向右滑动
   int distanceX = (int) event.getRawX() - downX;//移动的距离
   offsetLeft += distanceX;
   downX = (int) event.getRawX();
   if (isScrolledToStart()) {
    scrollToStart();
   } else if (isScrolledToEnd()) {
    scrollToEnd();
   } else {
    invalidate();
   }
   break;
  case MotionEvent.ACTION_UP:
   break;
  }
  return true;
 }

 public static class DataSet {
  public String name;
  public float value;

  public DataSet(String name, float value) {
   this.name = name;
   this.value = value;
  }
 }
}
我们可以在布局中这样使用:

<hk.jerry.linechart.LineChartView

 android:id="@+id/lc_test"

 android:layout_width="match_parent"

 android:layout_height="match_parent"/>

然后添加数据:

LineChartView lcHistory = (LineChartView) findViewById(R.id.lc_test);

//测试数据 lcHistory.setTitle("测试图表");

Random random = new Random(10);

for (i = 0; i < 50; i++) {

    lcHistory.addDataSet(new DataSet((50 - i) + "分钟前", random.nextInt(40)));
}
 

运行效果

时间: 2024-10-27 06:24:42

java原生态的统计图控件——折线图例子的相关文章

java中的swt控件的学习

问题描述 java中的swt控件的学习 新人初学java,最近开始学习swt控件,对于这一块,可以说是一点经验也没有,所以只好麻烦各位前辈,给点意见: 1.由于是第一次学习控件,具体原理什么的,都不懂,还望各位能推荐一下好的学习资源,(我搜了网上的资源,有很多,但是不知道是否适合新手),谢谢 2.对于学习的建议,谢谢各位了, 3.之前在学习的过程中,得到了许多人的帮助,也谢谢这些人的无私帮助,小弟不才,以后请多指教,谢谢 解决方案 JAVA SWT 控件与面板JAVA工程笔记:SWT控件学习之C

android控件-android中xml设置控件的属性和java文件里设置控件的属性有何不同

问题描述 android中xml设置控件的属性和java文件里设置控件的属性有何不同 android中xml设置控件设置的属性和java文件里设置控件的属性有何不同 是不是java文件里设置可以改变动态的改变控件位置,文字颜色,文字大小 而在xml文件设置之后就无法改变 那具体在java文件设置的属性是如何改变的呢 解决方案 java中可以重新设置去改变xml的设置.但是xml不能去改变java中的设置. 解决方案二: 你可以这样理解,xml设置以后基本不会改变,如果会改变,就要在java里面设

Repeater控件分页的例子

分页|控件   Repeater和DataList控件提供了一个快速.灵活的表现数据的方式,但是,它们没有内建的分页功能:DataGrid控件提供了内建的分页功能,但它的结构比较复杂.下面就用PagedDataSource类实现Repeater和DataList的分页. PagedDataSource封装了DataGrid的分页属性,我们可以象DataGrid那样进行分页.代码如下:   <%@ Page Language="C#" %> <%@ import nam

winform-winfrom 关于控件集合的例子 请看下面代码

问题描述 winfrom 关于控件集合的例子 请看下面代码 int n = this.Controls.OfType<CheckBox>().Where(x => x.Checked).Count(); if (n > 4 - 1) { LAB.Visible = true; LAB.Text = "启用四个以上 可能会造成系统卡顿!"; LAB.ForeColor = Color.Yellow; } 我有10个复选框 每个复选框勾选的时候 会到这个集合里面来

C# 绘制统计图大全(柱状图, 折线图, 扇形图)_C#教程

统计图形种类繁多, 有柱状图, 折线图, 扇形图等等, 而统计图形的绘制方法也有很多, 有Flash制作的统计图形, 有水晶报表生成统计图形, 有专门制图软件制作, 也有编程语言自己制作的:这里我们用就C# 制作三款最经典的统计图: 柱状图, 折线图和扇形图:既然是统计, 当然需要数据, 这里演示的数据存于Sql Server2000中, 三款统计图形都是动态生成. 其中柱状图我会附上制作步骤, 其他两款统计图直接附源码. 说明: 需求不一样, 统计图形绘制后的显示效果也不一样, 比如这里柱状图

Android动态设置RelativeLayout控件的高度例子

在做项目的时候其中一个需求是要求banner图的宽和高是1:1所以我需要获取手机屏幕的宽度然后动态的把高度值设置为手机屏幕宽度的值在这项目中就是设置RelativeLayout的高度值代码如下 mRllayoutBanner = (RelativeLayout) findViewById(R.id.rl_banner); WindowManager wm = this.getWindowManager(); int width = wm.getDefaultDisplay().getWidth(

VB.NET 中 使用 ListView 控件的简单例子

控件 ListView 控件 在 程序开发过程中的使用是非常广泛的.因为其不支持数据库的绑定所以在数据库程序开发领域无法与datagridview抗衡 但是ListView的确是一个非常好用的控件.下面就把 一个简单的 ListView的例子发出来. Public Class Form6Class Form6     <summary>     英雄类     </summary>     <remarks></remarks>    Public Clas

jquery日期时间My97DatePicker选择控件函数使用例子

1.推荐日期选择控件:My97DatePicker My97DatePicker是一个非常好用的日期/时间选择控件,支持多种日期时间类型等: 支持的浏览器 IE 6.0+ , Firefox 2.0+ , Chrome, Opera 9.5+ , Safari 3.0+ 注意:IE 8/9/10/11是完美支持的 功能如下: 常规功能 支持多种调用模式 下拉,输入,导航选择日期 支持周显示 只读开关,高亮周末功能 操作按钮自定义 自动选择显示位置 自定义弹出位置 自定义星期的第一天new 特色功

Javascript日历控件bootstrap-datetimepicker使用例子

在开发微网站的酒店预定时,在填写表单的住店和离点时需要用到日历控件,之前我收藏了很多相关的js日历控件,但现在要用时却没有一款适合用的,因为开发的是手机网站,所以比较挑剔. 要求: 在点击日期表单的输入框时,禁止弹出文本输入框,并且不能往输入框输入内容,只能从日历控件弹出的日历选取日期. 能满足上面要求的JS日期控件,真的是凤毛麟角.为了能找到这个控件,我花了不少时间啊,最终找到了bootstrap-datetimepicker-master,这是bootstrap的一个插件,看开发文档似乎对我