Пару недель назад возникла необходимость в отображении данных в виде графика. Покопавшись в интернетах и перебрав несколько вариантов, обратил внимание на библиотеку AChartEngine. В библиотеке реализован огромный потенциал по построению диаграмм и трендов, и самое главное - ее использование оказалось интуитивно понятным даже для такого новичка в Java как я.
Вместе с библиотекой идет хороший пример с демонстрацией возможностей библиотеки и, судя по отзывам на stackoverflow, она является очень популярной среди разработчиков.
Цель данного сообщения - показать обрубленную версию официального примера для более легкого понимания процесса вывода тренда, пример использования AsyncTask (хорошие статьи по использованию AsyncTask и загрузку данных из интернет можно прочитать на habrahabr.ru.), для получения данных с WEB, а также прикрепления AChartEngine-а к реальной задаче.
Процесс подключения библиотеки achartengine-1.0.0.jar стандартный, рассказывать тут нечего. Единственное на что стоит обратить внимание - после подключения библиотеки убедитесь чтобы в Project Properties->Java Build Path-> Order&Export эта библиотека стояла самой первой с установленной галочкой, иначе получите ClassNotFoundException.
Итак, что мне было нужно от либы: построить график изменяющегося во времени физ.параметра (дискретность данных непостоянна, т.е. временные интервалы между значениями почти всегда разные), данные физ.параметра я получаю с WEB-сервера посредством http-запроса к php-скрипту.
Общий механизм получения данных:
I. В асинхронной задаче выполняем запрос к серверу посредством передачи параметров запроса php-скрипту.
II. php-скрипт соединяется с СУБД и выполняет хранимую процедуру.
III.Получаем данные для графика
IV. Парсим полученную строку и заполняем серию для нашего чарта.
Так, шаг за шагом и пойдем:
php-скрипт, в моем случае, принимает три параметра:
1 - идентификатор параметра (tag_id);
2 - дата/время начала временного промежутка
3 - дата/время окончания временного промежутка
Запрос к серверу может выглядеть так:
http://www.my_super_mega_web_server.ru/android/?start=2011-11-06%201:15:00&end=2011-11-07%200:00:00&tag=2116 (это нерабочая ссылка ;-) )
Все три параметра будут меняться в зависимости от того, что хочет посмотреть пользователь, поэтому нам необходимо создать функцию которая динамически собирает такой URL.
php-скрипт возвращает нам данные вот в таком виде (кому интересно - это температура воздуха на улице):
Код скрипта здесь рассматривать не будем, это за рамками сообщения.
Вместе с библиотекой идет хороший пример с демонстрацией возможностей библиотеки и, судя по отзывам на stackoverflow, она является очень популярной среди разработчиков.
Цель данного сообщения - показать обрубленную версию официального примера для более легкого понимания процесса вывода тренда, пример использования AsyncTask (хорошие статьи по использованию AsyncTask и загрузку данных из интернет можно прочитать на habrahabr.ru.), для получения данных с WEB, а также прикрепления AChartEngine-а к реальной задаче.
Процесс подключения библиотеки achartengine-1.0.0.jar стандартный, рассказывать тут нечего. Единственное на что стоит обратить внимание - после подключения библиотеки убедитесь чтобы в Project Properties->Java Build Path-> Order&Export эта библиотека стояла самой первой с установленной галочкой, иначе получите ClassNotFoundException.
Итак, что мне было нужно от либы: построить график изменяющегося во времени физ.параметра (дискретность данных непостоянна, т.е. временные интервалы между значениями почти всегда разные), данные физ.параметра я получаю с WEB-сервера посредством http-запроса к php-скрипту.
Общий механизм получения данных:
I. В асинхронной задаче выполняем запрос к серверу посредством передачи параметров запроса php-скрипту.
II. php-скрипт соединяется с СУБД и выполняет хранимую процедуру.
III.Получаем данные для графика
IV. Парсим полученную строку и заполняем серию для нашего чарта.
Так, шаг за шагом и пойдем:
php-скрипт, в моем случае, принимает три параметра:
1 - идентификатор параметра (tag_id);
2 - дата/время начала временного промежутка
3 - дата/время окончания временного промежутка
Запрос к серверу может выглядеть так:
http://www.my_super_mega_web_server.ru/android/?start=2011-11-06%201:15:00&end=2011-11-07%200:00:00&tag=2116 (это нерабочая ссылка ;-) )
Все три параметра будут меняться в зависимости от того, что хочет посмотреть пользователь, поэтому нам необходимо создать функцию которая динамически собирает такой URL.
php-скрипт возвращает нам данные вот в таком виде (кому интересно - это температура воздуха на улице):
| 06.11.11 01:19 -11.46 | |
| 06.11.11 01:31 -11.31 | |
| 06.11.11 01:33 -11.46 | |
| 06.11.11 01:47 -11.61 | |
| 06.11.11 01:49 -11.76 | |
| 06.11.11 02:05 -11.91 | |
| 06.11.11 02:18 -12.06 | |
| 06.11.11 02:21 -12.21 |
По сути - это обычные строки, разделенные на дату/время и значение знаком табуляции.
Предвижу комментарии типа "почему строки, почему не бинарные данные в виде datetime и float?" Отвечаю: потому, что скрипт писал не я, + это пилотный, так сказать, проект.
Запрос данных и построение графика будет происходить сразу при создании активити, поэтому необходимо учесть, что при смене ориентации экрана - активити пересоздастся и снова начнется загрузка данных. В моем случае нет нужды отображать график в портретном режиме, поэтому я фиксирую разворот в положении landscape.
Код класса наследованного от AsyncTask:
Теперь код GraphActivity:
Поясню. При создании GraphActivity ей в Intent передается посылка содержащая идентификатор параметра(tagid) и его текстовое описание(descr), чтобы нам было чем подписать сам график. Если Вы собираетесь адаптировать пример под свои нужды, можете напрямую задать эти параметры и быстро посмотреть результат просто сделав эту активити главной. Итак, первый параметр для php-скрипта мы передали через Intent, параметр конца периода у нас будет текущее время, а параметр начала периода = конец - 2 часа.
За формирование URL у нас отвечает функция makeURL(int pTag).
У себя в программе эту активити я вызываю нажатием на соответствующую View (SimpleIndicator - это собственный компонент, не ищите его в палитре):
Вот собственно и все.
Запрос данных и построение графика будет происходить сразу при создании активити, поэтому необходимо учесть, что при смене ориентации экрана - активити пересоздастся и снова начнется загрузка данных. В моем случае нет нужды отображать график в портретном режиме, поэтому я фиксирую разворот в положении landscape.
Код класса наследованного от AsyncTask:
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.StringTokenizer;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.Toast;
public class DownloadTagDataAsyncTask extends
AsyncTask<String, Void, String> {
private HttpURLConnection mHttpConn;
@Override
protected String doInBackground(String... params) {
String strURL = "";
if (params.length != 0) {
strURL = params[0];
}
return DownloadData(strURL);
}
@Override
protected void onProgressUpdate(Void... values) {
super.onProgressUpdate(values);
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
if (hasError){
hasError = false;
Toast.makeText(GraphActivity.Instance, "Ошибка соединения. Повторите попытку.", Toast.LENGTH_SHORT);
GraphActivity.Instance.finish();
return;
}
StringTokenizer allData = new StringTokenizer(result, "\r\n", false);
int l = 0;
while (allData.hasMoreTokens()) {
String s = allData.nextToken();
l++;
if (s.equals(" ")) {
break;
}
}
float[] values = new float[l];
Date[] times = new Date[l];
allData = new StringTokenizer(result, "\r\n", false);
int i = 0;
while (allData.hasMoreTokens() && (i < l-1)) {
String s = allData.nextToken();
if (s.equals(" ")) {
break;
}
StringTokenizer record = new StringTokenizer(s, "\u0009", false);
if (record.hasMoreTokens()) {
String timePoint = record.nextToken();
String valData = record.nextToken();
values[i] = Float.valueOf(valData);
SimpleDateFormat sdf = new SimpleDateFormat("yy.dd.MM HH:mm");
Date ddd = null;
try {
ddd = sdf.parse(timePoint);
times[i] = new Date();
times[i] = ddd;
}catch(Exception e) {
}
}
i++;
}
GraphActivity.Instance.getDateDemoDataset(values, times);
GraphActivity.Instance.getDemoRenderer();
GraphActivity.Instance.buildChart();
GraphActivity.Instance.endProgress();
}
private InputStream OpenHttpConnection(String urlString) throws IOException {
InputStream in = null;
int response = -1;
URL url = new URL(urlString);
URLConnection conn = url.openConnection();
mHttpConn = null;
if (!(conn instanceof HttpURLConnection))
throw new IOException("Error. HTTP connection failed.");
try {
mHttpConn = (HttpURLConnection) conn;
mHttpConn.setAllowUserInteraction(false);
mHttpConn.setInstanceFollowRedirects(true);
mHttpConn.setRequestMethod("GET");
mHttpConn.connect();
response = mHttpConn.getResponseCode();
if (response == HttpURLConnection.HTTP_OK) {
in = mHttpConn.getInputStream();
}
} catch (Exception ex) {
hasError = true;
throw new IOException("Connection error.");
}
return in;
}
private String DownloadData(String URL) {
int BUFFER_SIZE = 20000;
InputStream in = null;
String str = "";
try {
in = OpenHttpConnection(URL);
} catch (IOException e1) {
e1.printStackTrace();
return str;
}
if (in == null) {
return str;
}
InputStreamReader isr = null;
try {
isr = new InputStreamReader(in, "cp-1251");
} catch (UnsupportedEncodingException e1) {
e1.printStackTrace();
}
int charRead;
char[] inputBuffer = new char[BUFFER_SIZE];
try {
while ((charRead = isr.read(inputBuffer)) > 0) {
// ---convert the chars to a String---
String readString = String
.copyValueOf(inputBuffer, 0, charRead);
str += readString;
inputBuffer = new char[BUFFER_SIZE];
}
in.close();
} catch (IOException e) {
e.printStackTrace();
return "";
}
if (mHttpConn != null) {
mHttpConn.disconnect();
}
return str;
}
}
GraphActivity.Instance - это ссылка на саму себя, чтобы из AsyncTask.onPostExecute(...) можно было обратиться к методам нашей активити.Теперь код GraphActivity:
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import org.achartengine.ChartFactory;
import org.achartengine.GraphicalView;
import org.achartengine.model.TimeSeries;
import org.achartengine.model.XYMultipleSeriesDataset;
import org.achartengine.renderer.XYMultipleSeriesRenderer;
import org.achartengine.renderer.XYSeriesRenderer;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
public class GraphActivity extends Activity {
private View mMainView;
protected static GraphActivity Instance;
private int mTag;
private String mSeriesCaption = "Chart";
private XYMultipleSeriesDataset mDataset = new XYMultipleSeriesDataset();
private XYMultipleSeriesRenderer mRenderer = new XYMultipleSeriesRenderer();
private GraphicalView mChartView;
private static ProgressDialog pd;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Remove title bar
this.requestWindowFeature(Window.FEATURE_NO_TITLE);
//Remove notification bar
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
Instance = this;
Intent intent = this.getIntent();
if (intent.getExtras() != null) {
mTag = intent.getExtras().getInt("tagid", 0);
mSeriesCaption = intent.getExtras().getString("descr");
}
pd = new ProgressDialog(this);
pd.setMessage("Загрузка данных. Ждите...");
startProgress();
mMainView = LayoutInflater.from(this).inflate(R.layout.graph, null);
setContentView(mMainView);
DownloadTagDataAsyncTask dat = new DownloadTagDataAsyncTask();
dat.execute(makeURL(mTag));
}
public void getDateDemoDataset(float[] pValues, Date[] pTimes) {
XYMultipleSeriesDataset dataset = new XYMultipleSeriesDataset();
TimeSeries series = new TimeSeries(mSeriesCaption);
for (int k = 0; k < pValues.length-1; k++) {
series.add(pTimes[k], pValues[k]);
}
dataset.addSeries(series);
mDataset = dataset;
}
public void getDemoRenderer() {
XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer();
renderer.setAxisTitleTextSize(16);
renderer.setChartTitleTextSize(20);
renderer.setLabelsTextSize(15);
renderer.setLegendTextSize(20);
renderer.setShowGrid(true);
renderer.setGridColor(Color.GRAY);
XYSeriesRenderer r = new XYSeriesRenderer();
r.setColor(Color.GREEN);
r.setFillPoints(true);
renderer.addSeriesRenderer(r);
mRenderer = renderer;
}
public void buildChart(){
if (mChartView == null) {
LinearLayout layout = (LinearLayout) findViewById(R.id.chart);
mChartView = ChartFactory.getTimeChartView(this, mDataset, mRenderer,"HH:mm");
//mRenderer.setClickEnabled(true);
//mRenderer.setSelectableBuffer(100);
layout.addView(mChartView, new LayoutParams(LayoutParams.FILL_PARENT,
LayoutParams.FILL_PARENT));
} else {
mChartView.repaint();
}
}
public String makeURL(int pTagId) {
Calendar cal = new GregorianCalendar();
Date currentDate = new Date();
Long time = currentDate.getTime();
long anotherDate = -1;
time = time + (60*60*1000*anotherDate);
currentDate = new Date(time);
String res = "http://www.my_super_mega_web_server.ru/android/?start="
+ new SimpleDateFormat("yyyy-MM-dd'%20'HH:mm:ss").format(currentDate)
+ "&end="
+ new SimpleDateFormat("yyyy-MM-dd'%20'HH:mm:ss").format(cal.getTime()) + "&tag=" + pTagId;
return res;
}
public void startProgress(){
pd.show();
}
public void endProgress(){
pd.hide();
pd.dismiss();
}
}
Поясню. При создании GraphActivity ей в Intent передается посылка содержащая идентификатор параметра(tagid) и его текстовое описание(descr), чтобы нам было чем подписать сам график. Если Вы собираетесь адаптировать пример под свои нужды, можете напрямую задать эти параметры и быстро посмотреть результат просто сделав эту активити главной. Итак, первый параметр для php-скрипта мы передали через Intent, параметр конца периода у нас будет текущее время, а параметр начала периода = конец - 2 часа.
За формирование URL у нас отвечает функция makeURL(int pTag).
У себя в программе эту активити я вызываю нажатием на соответствующую View (SimpleIndicator - это собственный компонент, не ищите его в палитре):
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN){
if (v instanceof SimpleIndicator){
Intent intent = new Intent(this, GraphActivity.class);
intent.putExtra("tagid", ((SimpleIndicator)v).getmTag());
intent.putExtra("descr", ((SimpleIndicator)v).getDescr());
this.startActivity(intent);
}
}
return false;
}
Вот собственно и все.
Комментариев нет:
Отправить комментарий