1. 程式人生 > >Android利用Achartengine實現實時曲線圖

Android利用Achartengine實現實時曲線圖

實時曲線圖在實際專案中經常會遇到,特別是與感測器相關的專案中。也正是因為公司專案需要實時展現從BLE裝置獲取到的心電圖資料,所以有機會對實時曲線圖的實現過程進行了較深入的探究。本文會講述兩種實現方式,其中每種實現方式裡都會包含兩種展現方式(曲線圖平移方向:左、右)。


假設可見視窗只能容納下100個數據點,那麼,在資料點個數超出一百後,如果不做任何處理的話,雖然資料已經繪製了,但我們看到的卻一直都是前100個數據點,除非我們水平拖動座標系。

第一種實現方式:

之前一直很困惑,為什麼Achartengine沒有像MPAndroidChart那樣提供一個centerViewPort()方法,來指定在可見視窗中顯示的點的X座標值,後來發現,通過renderer.setXAxisMin()和renderer.setXAxisMax()即可控制需要在可見視窗中顯示的點的範圍,就好比我們可以水平移動X軸那樣,在可見視窗中顯示我們感興趣的資料點。通過這種方式,每次新增資料時,我們都設定以下X座標的最大最小值,使我們感興趣的資料處於該範圍內即可。同時,通過這種方式,如果我們水平拖動的話還可以看到所有的歷史資料點。

種實現方式:

這種方式在於,每次新的資料點到來時,我們將所有的舊資料的X座標加(減)1,新到來的資料點永遠只在X座標為0處顯示,從而產生一種向右(左)平移的效果。這種方式我們無法看到所有的歷史資料點,我們只能看到最近更新的100個數據點。

描述的有點抽象,還是看下具體的程式碼吧:

package com.lamelias.realtimechart;

import java.util.ArrayList;
import java.util.Random;

import org.achartengine.ChartFactory;
import org.achartengine.GraphicalView;
import org.achartengine.chart.PointStyle;
import org.achartengine.model.XYMultipleSeriesDataset;
import org.achartengine.model.XYSeries;
import org.achartengine.renderer.XYMultipleSeriesRenderer;
import org.achartengine.renderer.XYSeriesRenderer;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Color;
import android.graphics.Paint.Align;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.TextView;

public class CardioChart extends Fragment {

	private static String TAG = CardioChart.class.getSimpleName();
	private static final int MAX_POINT = 100;
	private TextView tv;
	/* 呼吸波形的相關引數 */
	int flagBreBt = 0;
	private Handler handler;
	private String title = "Cardiograph";
	private XYSeries series;
	private XYMultipleSeriesDataset mDataset;
	private GraphicalView chart;
	private XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer();
	XYSeriesRenderer r = new XYSeriesRenderer();
	private Context context;
	private int addX = -1;
	int[] xv = new int[MAX_POINT];
	int[] yv = new int[MAX_POINT];
	RelativeLayout breathWave;
	int i = 0;

	int count = 0;// 每隔多少包更新一次心電圖
	private ArrayList<Integer> drawPack = new ArrayList<Integer>(); // 需要繪製的資料集
	Random random = new Random();
	private final int POINT_GENERATE_PERIOD=10; //單位是ms
	
	Runnable runnable = new Runnable() {

		@Override
		public void run() {
			ArrayList<Integer> datas = new ArrayList<Integer>();
			for (int i = 0; i < 1; i++) {
				datas.add(random.nextInt(3000));
			}
			
			/*以下方式左右拖動座標系時可以看到所有歷史資料點*/
			//updateCharts(datas);
			//leftUpdateCharts(datas);
			
			/*以下方式無法看到歷史資料點,在座標軸上只能看到MAX_POINT個數據點*/
			updateChart(random.nextInt(3000)); 
			//rightUpdateChart(random.nextInt(3000));
			
			handler.postDelayed(this, POINT_GENERATE_PERIOD);
		}
	};

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		handler = new Handler() ;
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {
		View view = inflater.inflate(R.layout.cardiochart, container, false);
		breathWave = (RelativeLayout) view.findViewById(R.id.cardiograph);
		tv = (TextView) view.findViewById(R.id.test);
		initCardiograph();

		return view;
	}
	
	/**
	 * @Title revcievedMessage 
	 * @Description Fragment接收Activity中訊息的第一種方式,直接在Fragment中提供一個public方法,供Activity呼叫
	 * @param action 
	 * @return void
	 */
	public void recievedMessage(String action){
		switch(action){
		case "START" :
			handler.postDelayed(runnable, POINT_GENERATE_PERIOD);
			break;
		case "STOP" :
			Log.w(TAG, "recieved Stop !");
			handler.removeCallbacksAndMessages(null);
			break;
	}
	}

	@Override
	public void onResume() {
		getActivity().registerReceiver(mBroadcastReceiver, makeIntentFilter());
		super.onResume();
	}

	@Override
	public void onDestroy() {
		getActivity().unregisterReceiver(mBroadcastReceiver);
		super.onDestroy();
	}
	
	/**
	 * @Title makeIntentFilter 
	 * @Description  Fragment接收Activity中訊息的第二種方式,通過廣播的方式
	 * @return 
	 * @return IntentFilter
	 */
	private IntentFilter makeIntentFilter() {
		IntentFilter mIntentFilter=new IntentFilter();
		mIntentFilter.addAction("START");
		mIntentFilter.addAction("STOP");
		return mIntentFilter;
	}
	
	private BroadcastReceiver mBroadcastReceiver=new BroadcastReceiver() {
		
		@Override
		public void onReceive(Context context, Intent intent) {
			switch(intent.getAction()){
				case "START" :
					handler.postDelayed(runnable, POINT_GENERATE_PERIOD);
					break;
				case "STOP" :
					Log.w(TAG, "recieved Stop !");
					handler.removeCallbacksAndMessages(null);
					break;
			}
			
		}
	};

	

	public void initCardiograph() {
		context = getActivity().getApplicationContext();
		// 這個類用來放置曲線上的所有點,是一個點的集合,根據這些點畫出曲線
		series = new XYSeries(title);
		// 建立一個數據集的例項,這個資料集將被用來建立圖表
		mDataset = new XYMultipleSeriesDataset();
		// 將點集新增到這個資料集中
		mDataset.addSeries(series);
		// 以下都是曲線的樣式和屬性等等的設定,renderer相當於一個用來給圖表做渲染的控制代碼
		/* int color = Color.parseColor("#08145e"); */
		int color = getResources().getColor(R.color.cardio_color3);
		PointStyle style = PointStyle.CIRCLE;
		buildRenderer(color, style, true);
		// 設定好圖表的樣式
		setChartSettings(renderer, "X", "Y", 0, MAX_POINT, 0, 3000, color,
				color);
		// 生成圖表
		chart = ChartFactory.getLineChartView(context, mDataset, renderer);
		chart.setBackgroundColor(getResources().getColor(
				R.color.cardio_bg_color));
		breathWave.removeAllViews();
		breathWave.addView(chart);

	}

	protected void buildRenderer(int color, PointStyle style, boolean fill) {

		// 設定圖表中曲線本身的樣式,包括顏色、點的大小以及線的粗細等
		r.setColor(color);
		r.setPointStyle(style);
		r.setFillPoints(fill);
		r.setLineWidth(3);
		renderer.addSeriesRenderer(r);
	}

	protected void setChartSettings(XYMultipleSeriesRenderer renderer,
			String xTitle, String yTitle, double xMin, double xMax,
			double yMin, double yMax, int axesColor, int labelsColor) {
		// 有關對圖表的渲染可參看api文件
		renderer.setBackgroundColor(getResources().getColor(
				R.color.cardio_bg_color));
		renderer.setChartTitle(title);
		renderer.setChartTitleTextSize(20);
		renderer.setLabelsTextSize(19);// 設定座標軸標籤文字的大小
		renderer.setXTitle(xTitle);
		renderer.setYTitle(yTitle);
		renderer.setXAxisMin(xMin);
		renderer.setXAxisMax(xMax);
		renderer.setYAxisMin(yMin);
		renderer.setYAxisMax(yMax);
		//renderer.setYAxisAlign(Align.RIGHT, 0);//用來調整Y軸放置的位置,表示將第一條Y軸放在右側
		renderer.setAxesColor(axesColor);
		renderer.setLabelsColor(labelsColor);
		renderer.setShowGrid(true);
		renderer.setGridColor(Color.GRAY);
		renderer.setXLabels(10);//若不想顯示X標籤刻度,設定為0 即可
		renderer.setYLabels(10);
		renderer.setLabelsTextSize(18);// 設定座標軸標籤文字的大小
		renderer.setXLabelsColor(labelsColor);
		renderer.setYLabelsColor(0, labelsColor);
		renderer.setYLabelsVerticalPadding(-5);
		renderer.setXTitle("");
		renderer.setYTitle("");
		renderer.setYLabelsAlign(Align.RIGHT);
		renderer.setAxisTitleTextSize(20);
		renderer.setPointSize((float) 1);
		renderer.setShowLegend(false);
		renderer.setFitLegend(true);
		renderer.setMargins(new int[] { 30, 45, 10, 20 });// 設定圖表的外邊框(上/左/下/右)
		renderer.setMarginsColor(getResources().getColor(
				R.color.cardio_bg_color));
	}
	

	/**
	 * @Title leftUpdateCharts 
	 * @Description 新生成的點一直在左側,產生向右平移的效果, 基於X軸座標從0開始,然後遞減的思想處理
	 * @param datas 
	 * @return void
	 */
	protected void  leftUpdateCharts(ArrayList<Integer> datas) {
		for (int addY : datas) {
			series.add(i, addY);
			i--;
		}
		if (Math.abs(i) < MAX_POINT) {
			renderer.setXAxisMin(-MAX_POINT);
			renderer.setXAxisMax(0);
		} else {
			renderer.setXAxisMin(-series.getItemCount());
			renderer.setXAxisMax(-series.getItemCount() + MAX_POINT);
		}

		chart.repaint();
	}
	
	/**
	 * @Title updateCharts 
	 * @Description 新生成的點一直在右側,產生向左平移的效果,基於X軸座標從0開始,然後遞加的思想處理
	 * @param datas 
	 * @return void
	 */
	protected void updateCharts(ArrayList<Integer> datas) {
		for (int addY : datas) {
			series.add(i, addY);
			i++;
		}
		if (i < MAX_POINT) {
			renderer.setXAxisMin(0);
			renderer.setXAxisMax(MAX_POINT);
		} else {
			renderer.setXAxisMin(series.getItemCount() - MAX_POINT);
			renderer.setXAxisMax(series.getItemCount());
		}

		chart.repaint();
	}

	/**
	 * @Title updateChart 
	 * @Description 新生成的點一直在x座標為0處,因為將所有舊點的x座標值加1,所以產生向右平移的效果
	 * @param addY 
	 * @return void
	 */
	private void updateChart(int addY) {

		// 設定好下一個需要增加的節點 
		addX = 0; 
		// 移除資料集中舊的點集
		mDataset.removeSeries(series);
		// 判斷當前點集中到底有多少點,因為螢幕總共只能容納MAX_POINT個,所以當點數超過MAX_POINT時,長度永遠是MAX_POINT
		int length = series.getItemCount();
		if (length > MAX_POINT) {
			length = MAX_POINT;
		}
		// 將舊的點集中x和y的數值取出來放入backup中,並且將x的值加1,造成曲線向右平移的效果
		for (int i = 0; i < length; i++) {
			xv[i] = (int) series.getX(i) + 1;
			yv[i] = (int) series.getY(i);
		}
		// 點集先清空,為了做成新的點集而準備 
		series.clear();
		// 將新產生的點首先加入到點集中,然後在迴圈體中將座標變換後的一系列點都重新加入到點集中
		// 這裡可以試驗一下把順序顛倒過來是什麼效果,即先執行迴圈體,再新增新產生的點
		series.add(addX, addY);
		for (int k = 0; k < length; k++) {
			series.add(xv[k], yv[k]);
		}

		// 在資料集中新增新的點集
		mDataset.addSeries(series); 
		// 檢視更新,沒有這一步,曲線不會呈現動態
		// 如果在非UI主執行緒中,需要呼叫postInvalidate(),具體參考api 
		//chart.invalidate();
		chart.repaint();
	}
	
	/**
	 * @Title rightUpdateChart 
	 * @Description 新生成的點一直在x座標為MAX_POINT處,因為將所有舊點的x座標值減1,所以產生向左平移的效果,無法看到歷史資料點
	 * @param addY 
	 * @return void
	 */
	private void rightUpdateChart(int addY) {

		// 設定好下一個需要增加的節點 
		addX =MAX_POINT; 
		// 移除資料集中舊的點集
		mDataset.removeSeries(series);
		// 判斷當前點集中到底有多少點,因為螢幕總共只能容納MAX_POINT個,所以當點數超過MAX_POINT時,長度永遠是MAX_POINT
		int length = series.getItemCount();
		if (length > MAX_POINT) {
			length = MAX_POINT;
		}
		// 將舊的點集中x和y的數值取出來放入backup中,並且將x的值-1,造成曲線向左平移的效果
		for (int i = 0; i < length; i++) {
			xv[i] = (int) series.getX(i) - 1;
			yv[i] = (int) series.getY(i);
		}
		// 點集先清空,為了做成新的點集而準備 
		series.clear();
		// 將新產生的點首先加入到點集中,然後在迴圈體中將座標變換後的一系列點都重新加入到點集中
		// 這裡可以試驗一下把順序顛倒過來是什麼效果,即先執行迴圈體,再新增新產生的點
		series.add(addX, addY);
		for (int k = 0; k < length; k++) {
			series.add(xv[k], yv[k]);
		}

		// 在資料集中新增新的點集
		mDataset.addSeries(series); 
		// 檢視更新,沒有這一步,曲線不會呈現動態
		// 如果在非UI主執行緒中,需要呼叫postInvalidate(),具體參考api 
		//chart.invalidate();
		chart.repaint();
	}
}
完整的工程已上傳至CSDN,為便於展示,將實際專案裡獲取到的心電圖資料修改成隨機數資料,詳見以下連結:http://download.csdn.net/detail/lamelias/8251705

使用時請先匯入android-support-V7-appcompat,然後再匯入RealTimeChart,二者匯入完畢後。注意修改一下Library引用,方法如下:右擊RealTimeChart工程,選擇Properties->Android->下方Library->add->選擇android-support-V7-appcompat即可。