понедельник, 9 июля 2012 г.

Отображение графиков на Android. AChartEngine. AsyncTask.

Пару недель назад возникла необходимость в отображении данных в виде графика. Покопавшись в интернетах и перебрав несколько вариантов, обратил внимание на библиотеку 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-скрипт возвращает нам данные вот в таком виде (кому интересно - это температура воздуха на улице):

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:

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;
 }

Вот собственно и все.

Комментариев нет:

Отправить комментарий