Пару недель назад возникла необходимость в отображении данных в виде графика. Покопавшись в интернетах и перебрав несколько вариантов, обратил внимание на библиотеку 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; }
Вот собственно и все.
Комментариев нет:
Отправить комментарий