Android利用Achartengine實現實時曲線圖
實時曲線圖在實際專案中經常會遇到,特別是與感測器相關的專案中。也正是因為公司專案需要實時展現從BLE裝置獲取到的心電圖資料,所以有機會對實時曲線圖的實現過程進行了較深入的探究。本文會講述兩種實現方式,其中每種實現方式裡都會包含兩種展現方式(曲線圖平移方向:左、右)。
假設可見視窗只能容納下100個數據點,那麼,在資料點個數超出一百後,如果不做任何處理的話,雖然資料已經繪製了,但我們看到的卻一直都是前100個數據點,除非我們水平拖動座標系。
第一種實現方式:
之前一直很困惑,為什麼Achartengine沒有像MPAndroidChart那樣提供一個centerViewPort()方法,來指定在可見視窗中顯示的點的X座標值,後來發現,通過renderer.setXAxisMin()和renderer.setXAxisMax()即可控制需要在可見視窗中顯示的點的範圍,就好比我們可以水平移動X軸那樣,在可見視窗中顯示我們感興趣的資料點。通過這種方式,每次新增資料時,我們都設定以下X座標的最大最小值,使我們感興趣的資料處於該範圍內即可。同時,通過這種方式,如果我們水平拖動的話還可以看到所有的歷史資料點。
第二種實現方式:
這種方式在於,每次新的資料點到來時,我們將所有的舊資料的X座標加(減)1,新到來的資料點永遠只在X座標為0處顯示,從而產生一種向右(左)平移的效果。這種方式我們無法看到所有的歷史資料點,我們只能看到最近更新的100個數據點。
描述的有點抽象,還是看下具體的程式碼吧:
完整的工程已上傳至CSDN,為便於展示,將實際專案裡獲取到的心電圖資料修改成隨機數資料,詳見以下連結:http://download.csdn.net/detail/lamelias/8251705package 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(); } }
使用時請先匯入android-support-V7-appcompat,然後再匯入RealTimeChart,二者匯入完畢後。注意修改一下Library引用,方法如下:右擊RealTimeChart工程,選擇Properties->Android->下方Library->add->選擇android-support-V7-appcompat即可。