01 需求
最近项目需求需要接收数据,绘制折线图,具体需求如下:
- 双Y轴,其中一个可以动态调节y轴刻度,另外一个刻度固定
- 显示两条折线
- X轴显示时间
- 动态的实时的展示数据
最后的效果如下图所示
02 技术选型
A 自定义view 实现绘制曲线, 实时刷新, y轴自适应, 多曲线绘制, 工期紧,难度略大
B 使用开源组件:
- AChartEngine
- MPAndroidChart
本项目选择的是AchartEngine 直接导入jar包,版本如下, Android studio版本 3.1.2
implementation files('libs/achartengine-1.2.0.jar')
03 实现
代码的实现参考了github项目中的demo代码,MultipleTemperatureChart.java
双Y轴的实现要点:
- 自定义继承XYSeries的类要添加一个包含scaleNumber的构造函数
- 如果绘制多条曲线,比如两条,render要有两个,dataset也要有两个,其中一个label的align设置位于左边,另外一个设置在右边
- 设置不同曲线对应的的render时,要用参数scale来区分设置的曲线对应哪个dataset,getDemoRenderer()函数中有很多代码用到了scale参数。设置dataset的时候也用到了scaleNumber参数,如getDateDemoDataset()中创建series和seriesPro的代码。
- render的数量和dataset数量要保持一致否则会抛出异常"Dataset and renderer should be not null and should have the same number of series"
主要代码:
A 自定义TimeSeries
package com.jdd.net.bledemo.ui.achartengine_custom;
import org.achartengine.model.XYSeries;
import java.util.Date;
public class TimeSeries extends XYSeries {
public TimeSeries(String title) {
super(title);
}
public TimeSeries(String title, int scaleNumber) {
super(title, scaleNumber);//比原来的TimeSeries加了此函数
}
public synchronized void add(Date x, double y) {
super.add((double)x.getTime(), y);
}
protected double getPadding(double x) {
return 1.0D;
}
}
主要的Activity代码(有些蓝牙连接的代码可以忽略,主要看 render 和dataset部分)
package com.jdd.net.bledemo;
import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Build;
import android.os.Bundle;
import android.app.Activity;
import android.os.CountDownTimer;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.ActivityCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.FileUtils;
import com.blankj.utilcode.util.PathUtils;
import com.jdd.net.bledemo.receiver.BluetoothReceiverSimple;
import com.jdd.net.bledemo.ui.achartengine_custom.TimeSeries;
import com.jdd.net.bledemo.ui.chart.MultipleTemperatureChart;
import com.jdd.net.bledemo.utils.Constant;
import com.jdd.net.bledemo.utils.Tools;
import org.achartengine.ChartFactory;
import org.achartengine.GraphicalView;
import org.achartengine.chart.CubicLineChart;
import org.achartengine.chart.PointStyle;
import org.achartengine.chart.XYChart;
import org.achartengine.model.XYMultipleSeriesDataset;
import org.achartengine.renderer.XYMultipleSeriesRenderer;
import org.achartengine.renderer.XYSeriesRenderer;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import top.wuhaojie.bthelper.BtHelperClient;
/**
* @author hao
*
* 协议版本 蓝牙2.0 默认的串口通信 ssp uuid是默认的
* 00001101-0000-1000-8000-00805f9b34fb
* 00001101-0000-1000-8000-00805F9B34FB
*
*
*
*
*
*/
public class ClientActivity extends Activity implements OnClickListener{
private static final String TAG = "ClientActivity";
/** Called when the activity is first created. */
private Button autopairbtn=null;
private Button btnReadData;
private Button btnCloseCon; //断开蓝牙连接的按钮
private TextView tvWordLenTips; //统计记录的长度
private TextView tvFilePath; //文件的地址
private BluetoothAdapter bluetoothAdapter;
//获取已经配对过的设备的集合
Set<BluetoothDevice> bondedDevices;
private static final int REQUEST_PERMISSION_ACCESS_LOCATION = 555;
private String MY_UUID ;
private final String SSP_UUID = "00001101-0000-1000-8000-00805F9B34FB";
public BtHelperClient btHelperClient;
private BluetoothSocket mClientBleSocket;
clientThread estabThread; //建立socket连接
readThread mreadThread; // 读取数据的链接
public volatile boolean isReading = true;
public String data_text_p1 = "";
private Button btnWrite ;
private long startTime;
private boolean firstFlag = true;
private TextView tv_timer;
private EditText etShowContent;
private int readCount = 0;
//==============data for line chart start==============
int constNum = 100;
int unit = 1;
private Timer timer = new Timer();
private GraphicalView chart;
private TimerTask task;
private int addY = -1;
private long addX;
private TimeSeries series; //实时数据对应的series,使用自定义数据
private TimeSeries seriesPro; //处理后的数据对应的坐标
private XYMultipleSeriesDataset dataset;
private Handler handler;
private Random random=new Random();
Date[] xcache = new Date[constNum];
int[] ycache = new int[constNum];
Date[] xcachePro = new Date[constNum];
int[] ycachePro = new int[constNum];
//==============data fo人 line chart end==============
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_client);
requestPermission();
registerBroadcast();
initBle();
initViews();
initChart();
}
private void initChart(){
LinearLayout layout1 = findViewById(R.id.ll_chart_zone);
//生成图表
chart = ChartFactory.getTimeChartView(this, getDateDemoDataset(), getDemoRenderer(), "hh:mm:ss");
//layout1.addView(chart, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,580));
//layout1.addView(chart, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,780));
layout1.addView(chart, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT));
// MultipleTemperatureChart dChart = new MultipleTemperatureChart();
// XYChart chartNew = dChart.getDoubleLineChart(this);
// GraphicalView chart2 = new GraphicalView(this, chartNew);
// layout1.addView(chart2, new ViewGroup.LayoutParams(1500,580));
handler = new Handler() {
@Override
public void handleMessage(Message msg) {
//刷新图表
addY=random.nextInt()%5+10 + unit;
unit++;
if (unit >= 1000){
unit = 100;
}
updateChart(addY);
super.handleMessage(msg);
}
};
task = new TimerTask() {
@Override
public void run() {
Message message = new Message();
message.what = 200;
handler.sendMessage(message);
}
};
timer.schedule(task, 2*1000,200); //20 / 50Hz
}
private void initViews(){
autopairbtn= findViewById(R.id.btn_match);
btnReadData= findViewById(R.id.btn_read_data);
btnCloseCon= findViewById(R.id.btn_close_device);
tvWordLenTips= findViewById(R.id.tv_word_len_tips);
tvFilePath= findViewById(R.id.tv_file_path);
tv_timer = findViewById(R.id.tv_timer);
etShowContent = findViewById(R.id.et_show_content);
//btnWrite= findViewById(R.id.btn_write);
/*btnWrite.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
createFile(System.currentTimeMillis());
}
});
*/
btnCloseCon.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mreadThread != null){
mreadThread.cancel();
isReading = false;
Log.d(TAG, "onClick: 结束读取数据的线程");
createFile(System.currentTimeMillis());
}
}
});
btnReadData.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
/**
* 如果已经进行了连接,那么搜索操作将会显著地降低连接的速率,因此你应当在连接时停止搜索。
* 可通过cancelDiscovery()方法停止搜索。
*/
bluetoothAdapter.cancelDiscovery(); //停止搜索
//从已经配对的设备中找到目标设备
BluetoothDevice device = findBondedDevice();
if (device != null){
Log.d(TAG, "onClick: device name is " + device.getName());
Toast.makeText(ClientActivity.this, "设备名称--->"+ device.getName(), Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(ClientActivity.this, "没有找到配对设备", Toast.LENGTH_SHORT).show();
return;
}
//建立连接
establishConn(device);
//读取数据
}
});
autopairbtn.setOnClickListener(this);
}
//设置按钮的监听方法
//自动配对的方法
@Override
public void onClick(View arg0) {
if (!bluetoothAdapter.isEnabled())
{
bluetoothAdapter.enable();//异步的,不会等待结果,直接返回。
}else{
//开始搜索设备
//来开始广播。当广播的事件是我们刚刚注册的事件时就会触发广播接收器,并且触发广播接收器中的onReceiver()方法。
//该搜索的过程调用12秒钟的查询,随后返回找到的设备。
bluetoothAdapter.startDiscovery();
Toast.makeText(this, "开始搜索设备", Toast.LENGTH_SHORT).show();
}
}
/**
* 在destory方法中需要解除注册
*/
private void registerBroadcast(){
Log.d(TAG, "registerBroadcast: ");
//注册广播
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothDevice.ACTION_FOUND);
registerReceiver(new BluetoothReceiverSimple(), intentFilter);
}
private void initBle(){
//MY_UUID is the app's UUID string, also used by the server code
MY_UUID = UUID.randomUUID().toString();
Log.d(TAG, "initBle: 生成的UUID是" + MY_UUID );
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
bondedDevices = bluetoothAdapter.getBondedDevices();
int size = bondedDevices.size();
Log.d(TAG, "initBle: 绑定设备的数量---->" + size);
//注册服务
btHelperClient = BtHelperClient.from(ClientActivity.this);
}
//申请权限
private void requestPermission() {
if (Build.VERSION.SDK_INT >= 23) {
int checkAccessFinePermission = ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION);
if (checkAccessFinePermission != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
REQUEST_PERMISSION_ACCESS_LOCATION);
Log.e(getPackageName(), "没有权限,请求权限");
return;
}
Log.e(getPackageName(), "已有定位权限");
//这里可以开始搜索操作
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case REQUEST_PERMISSION_ACCESS_LOCATION: {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.e(getPackageName(), "开启权限permission granted!");
//这里可以开始搜索操作
} else {
Log.e(getPackageName(), "没有定位权限,请先开启!");
}
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
private BluetoothDevice findBondedDevice(){
BluetoothDevice device = null;
bondedDevices = bluetoothAdapter.getBondedDevices();
Log.d(TAG, "findBondedDevice: 当前绑定设备的数量是-->" + bondedDevices.size());
if (bondedDevices!= null){
for (BluetoothDevice item :
bondedDevices) {
String name = item.getName();
if (name != null && name.contains(Constant.DEVICE_NAME)){
device = item;
//获取列表中的第一个匹配的设备
break;
}
}
}
return device;
}
private void establishConn(BluetoothDevice device
){
bluetoothAdapter.cancelDiscovery();
estabThread = new clientThread(device);
estabThread.start(); //开启线程
}
/**
* 客户端连接蓝牙串口设备的线程
* 费时的过程放到线程中执行
*
*/
/**
* 读取数据
*/
private class readThread extends Thread {
private BluetoothSocket clientSocket;
//private final InputStream mmInStream;
public readThread(BluetoothSocket socket){
clientSocket = socket;
/*InputStream tmpIn = null;
try {
tmpIn = socket.getInputStream();
show("客户端:获得输入流");
//tmpOut = socket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "temp sockets not created", e);
}*/
//mmInStream = tmpIn;
//mmOutStream = tmpOut;
}
public void run() {
byte[] buffer = new byte[1024];
int bytes;
InputStream is = null;
try {
is = clientSocket.getInputStream();
} catch (IOException e1) {
e1.printStackTrace();
}
while (isReading) {
try {
if ((bytes = is.read(buffer)) > 0) {
byte[] buf_data = new byte[bytes];
for (int i = 0; i < bytes; i++) {
buf_data[i] = buffer[i];
}
String s = Tools.bytesToHexString(buf_data);
int num = -1;
try{
num = Integer.parseInt(Tools.HexToTenSpecialBle(Tools.cutString(s)),16);
num = num / 10000;
}catch (Exception e){
//
num = -1;
}
if (firstFlag ){
startTime = System.currentTimeMillis();
//dataTimer.start();
firstFlag = false;
}
//updateChart(num);
readCount++;
if (readCount >= 25){
readCount = 0;
if (num >=0){
//updateChart(num);
}
show("客户端:读取数据了" + num + "#\n");
}
//
//recordData( s);
}else{
Log.d(TAG, "run: 获取的数据为空");
}
} catch (IOException e) {
try {
Log.d(TAG, "run: 出现异常");
is.close();
} catch (IOException e1) {
e1.printStackTrace();
}
break;
}
/*try {
Scanner in = new Scanner(mmInStream,"UTF-8");
String str = in.nextLine();
Log.i(TAG,"read: "+str);
} catch (Exception e) {
try {
Log.d(TAG, "run: 出现异常");
mmInStream.close();
} catch (IOException e1) {
e1.printStackTrace();
}
break;
}*/
}
}
public void cancel(){
try{
clientSocket.close();
Log.d(TAG, "读取数据的线程 cancel: 关闭蓝牙socket连接");
}catch (IOException e){
//
}
}
}
/**
* 开启客户端,简历客户端socket连接
*/
private class clientThread extends Thread {
BluetoothDevice device;
BluetoothSocket socket;
public clientThread(BluetoothDevice de
){
device = de;
//socket = clientSocket;
BluetoothSocket tmp = null;
try {
//尝试建立安全的连接
tmp = de.createRfcommSocketToServiceRecord(UUID.fromString(SSP_UUID));
//尝试建立不安全的连接
//tmp = mmDevice.createInsecureRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
Log.i(TAG,"获取 BluetoothSocket失败");
e.printStackTrace();
}
socket = tmp;
}
public void run() {
try {
if(bluetoothAdapter.isDiscovering()){
bluetoothAdapter.cancelDiscovery();
}
//连接
show("客户端:开始连接...");
// 由于connect()为阻塞调用,因此该连接过程应始终在主 Activity 线程以外的线程中执行
socket.connect();
show("客户端:连接成功");
//启动接受数据
} catch (IOException e) {
show("客户端:连接服务端异常!断开连接重新试一试");
e.printStackTrace();
}
show("客户端:启动接受数据");
mreadThread = new readThread(socket);
mreadThread.start();
}
public void cancel(){
try{
socket.close();
}catch (IOException e){
//
}
}
}
private void show(String msg){
Log.d(TAG, "show: msg:--->" + msg + "\n");
}
private void recordData(String msg){
data_text_p1 = data_text_p1 +
//"nSpo2:" + nSpO2 +"\n\r" +
//"nPR:" + nPR +"\n\r" +
(System.currentTimeMillis()- startTime) +
"," +
msg +"\n" ;
}
//创建文本文件
/**
* 需要将txt文本和视频文件保存在同一个目录中
*/
private void createFile(Long timeGen){
Log.d(TAG, "写入数据到文件,createFile: start");
String str0 = data_text_p1 ;
//获取外村应用文件数据路径
String fileDataPath = PathUtils.getExternalAppDataPath();
String appPath = PathUtils.getExternalStoragePath();
//Log.d(TAG, "createFile: 外存应用数据路径-->" + fileDataPath);
Log.d(TAG, "createFile: 外存应用数据路径-->" + appPath);
String folderPath = appPath + File.separator +
"bluetooth" +
File.separator +
"data"
;
String path = folderPath +
File.separator +
"data" + "_" +
getCurrentTimeNew(timeGen) + "_0" +
".txt";
String showText = "";
if (TextUtils.isEmpty(str0)){
showText = "文件的长度是:0 " + ",文件空" ;
}else{
int wordLen = str0.length();
if (wordLen >=1000){
showText = "文件长度是:" + wordLen + "" + str0.substring(0,999);
etShowContent.setText(str0.substring(0,999));
}else{
showText = "文件的长度是:" + wordLen + "" ;
}
}
tvWordLenTips.setText(showText);
Log.d(TAG, "createFile: 创建的文件是-->" + path);
tvFilePath.setText("文件本地路径:" + path);
//createOrExistsDir : 判断目录是否存在,不存在则判断是否创建成功
//createOrExistsFile : 判断文件是否存在,不存在则判断是否创建成功
//创建文件夹
boolean dirFlag = FileUtils.createOrExistsDir(folderPath);
Log.d(TAG, "createFile: create dir result is-->" + dirFlag);
//创建文件
boolean fileFlag = FileUtils.createOrExistsFile(path);
Log.d(TAG, "createFile: create file result is-->" + fileFlag);
//String txt = "2019-07-11 22:19:17.281 23091-23091/com.creative.libdemo D/MainActivity: createFile: 创建的文件是\n" +
// "-->/storage/emulated/0/Android/data/com.creative.libdemo/dataRecordDemo/ble_data.txt";
writeStringToFile(path,str0);
}
private void writeStringToFile(String filePath, String str){
//writeFileFromString : 将字符串写入文件
boolean result = FileIOUtils.writeFileFromString(filePath,str);
Log.d(TAG, "writeStringToFile: 写入结果-->" + result);
}
private String getCurrentTimeNew(long timeMillis){
SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd_HHmmss_SSS");
Date date = new Date(timeMillis);
String dateStr = formatter.format(date);
return dateStr;
}
private CountDownTimer dataTimer = new CountDownTimer(10*1000,1000) {
@Override
public void onTick(long millisUntilFinished) {
//剩余的秒数,设置更新剩余的描述
tv_timer.setVisibility(View.VISIBLE);
tv_timer.setText("" + millisUntilFinished/1000 + "秒后结束采集");
}
@Override
public void onFinish() {
if (mreadThread != null){
mreadThread.cancel();
isReading = false;
Log.d(TAG, "onClick: 结束读取数据的线程");
createFile(System.currentTimeMillis());
}
}
};
// =================绘图函数 start ===================
private void updateChart(int tempY) {
//设定长度为20
int length = series.getItemCount();
int lengthPro = seriesPro.getItemCount();
if(length>=constNum) length = constNum;
//addY=random.nextInt()%5+10;
addY=tempY;
addX=new Date().getTime();
//将前面的点放入缓存
for (int i = 0; i < length; i++) {
xcache[i] = new Date((long)series.getX(i));
ycache[i] = (int) series.getY(i);
xcachePro[i] = new Date((long)seriesPro.getX(i));
ycachePro[i] = (int) seriesPro.getY(i);
}
series.clear();
seriesPro.clear();
//将新产生的点首先加入到点集中,然后在循环体中将坐标变换后的一系列点都重新加入到点集中
series.add(new Date(addX), addY);
seriesPro.add(new Date(addX), addY- unit);
for (int k = 0; k < length; k++) {
series.add(xcache[k], ycache[k]);
seriesPro.add(xcachePro[k], ycachePro[k]);
}
//在数据集中添加新的点集
dataset.removeSeries(series);
dataset.removeSeries(seriesPro);
dataset.addSeries(series);
dataset.addSeries(seriesPro);
//曲线更新
chart.invalidate();
}
private XYMultipleSeriesRenderer getDemoRenderer() {
XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer(2);
renderer.setPointSize(4.5f);
int[] colors = new int[] { Color.RED, Color.BLUE };
//先添加孩子render
XYSeriesRenderer r1 = new XYSeriesRenderer();
r1.setColor(Color.RED);
r1.setChartValuesTextSize(15);
r1.setChartValuesSpacing(6);
r1.setPointStyle(PointStyle.POINT);
r1.setFillBelowLine(true);
r1.setFillBelowLineColor(Color.TRANSPARENT);
r1.setFillPoints(true);
XYSeriesRenderer r2 = new XYSeriesRenderer();
r2.setColor(Color.BLUE);
r2.setChartValuesTextSize(15);
r2.setChartValuesSpacing(6);
r2.setPointStyle(PointStyle.POINT);
r2.setFillBelowLine(true);
r2.setFillBelowLineColor(Color.TRANSPARENT);
r2.setFillPoints(true);
renderer.addSeriesRenderer(r1);
renderer.addSeriesRenderer(r2);
//再设置线的宽度
int length = renderer.getSeriesRendererCount();
for (int i = 0; i < length; i++) {
XYSeriesRenderer r = (XYSeriesRenderer) renderer.getSeriesRendererAt(i);
r.setLineWidth(4f); //折线图线条的宽度
r.setFillPoints(true);
}
//设置主坐标轴,默认主坐标轴是左侧的Y轴
renderer.setChartTitle("信号图");//标题
renderer.setChartTitleTextSize(60);
renderer.setXTitle("时间"); //x轴说明
renderer.setYTitle("原始数据");
renderer.setAxisTitleTextSize(35);
renderer.setAxesColor(Color.BLACK);
renderer.setLabelsTextSize(18); //数轴刻度字体大小
renderer.setLabelsColor(Color.BLACK);
renderer.setLegendTextSize(38); //曲线说明
renderer.setXLabelsColor(Color.BLACK);
renderer.setShowLegend(true);
renderer.setMargins(new int[] {30, 60, 35, 30});//上左下右{ 20, 30, 100, 0 })
renderer.setShowGrid(true);
renderer.setXLabelsAlign(Paint.Align.RIGHT);
renderer.setYLabelsAlign(Paint.Align.RIGHT);
renderer.setMarginsColor(Color.WHITE);
renderer.setPanEnabled(false,false);
renderer.setShowGrid(true);
//renderer.setYAxisMax(20);//纵坐标最大值
//renderer.setYAxisMin(0);//纵坐标最小值
renderer.setInScroll(false);
renderer.setYAxisMax(30,1);
renderer.setYAxisMin(0,1);
//设置第二组数据的Y轴
renderer.setYLabelsColor(0, colors[0]);
renderer.setYLabelsColor(1, colors[1]);
renderer.setYTitle("处理后数据",1);
renderer.setYLabelsColor(1,colors[1]);
renderer.setYLabelsColor(0,colors[0]);
renderer.setYAxisAlign(Paint.Align.RIGHT, 1);
renderer.setYLabelsAlign(Paint.Align.RIGHT, 1);
renderer.setGridColor(colors[0], 0);
renderer.setGridColor(colors[1], 1);
return renderer;
}
private XYMultipleSeriesDataset getDateDemoDataset() {//初始化的数据
dataset = new XYMultipleSeriesDataset();
final int nr = 10;
long value = new Date().getTime();
Random r = new Random();
series = new TimeSeries("原始数据 " ,0);
seriesPro = new TimeSeries("处理后数据 " ,1);
for (int k = 0; k < nr; k++) {
series.add(new Date(value+k*1000), 20 +r.nextInt() % 10);//初值Y轴以20为中心,X轴初值范围再次定义
seriesPro.add(new Date(value+k*1000), 20 +r.nextInt() % 10);//初值Y轴以20为中心,X轴初值范围再次定义
}
dataset.addSeries(series);
dataset.addSeries(seriesPro);
return dataset;
}
@Override
public void onDestroy() {
//当结束程序时关掉Timer
timer.cancel();
super.onDestroy();
}
// =================绘图函数 end ===================
}
求完整的项目的小伙伴请留言。
04 参考
A 七款Android开发者常用UI组件