Набрел тут на шедевральный топик на форуме. С разработкой, собственно, для мобильных устройств он не связан, зато связан с вопросами разработки ПО вцелом и с вопросами проектирования ПО в частности. Ну и манера изложения мыслей у автора вызывает у меня пепяку мозга меня, например, приводит в восторг.
В общем, очень рекомендую к прочтению. Спорных мыслей, конечно, немало, но никто ж не спорит что можно спорить. 😉

Previous ArticleNext Article
Технический директор IT-Dimension, компании-разработчика кросс-платформенного программного обеспечения

Leave a Reply

Your email address will not be published. Required fields are marked *

М.

Мой путь в Motorola SHOP4APPS или “Где здесь деньги?”

Таки-да, позавчера произошло Событие – мое Android-приложение PDFMyWeb Pro наконец-то пропустили в Motorola’овский SHOP4APPS. И это после 7ми месяцев мучений, более десятка попыток его туда запостить, кучи потраченного времени и нервов. Но, в общем, это случилось и сейчас я попробую провести небольшой анализ того, стоилоа ли игра свеч.

Начало

А началось все давно (черт возьми, почти все посты о попадании на различные площадки по продаже мобильных приложений у меня начинаются именно с этой фразы. Супер-просто ни разу не было, на сколько я помню). В декабре прошлого года вышла первая версия моей утилиты SMSMyFile для обмена файлами через SMS и после успешного попадания в Android Market решено было покорять новые горизонты. Одним из таких “новых” стал мотороловский маркет Shop4Apps. информации о нем было очень немного (в основном потому что у моторолы какая-то странная политика по поводу новинок – они доступны только по предварительной регистрации, акцептования кучи NDA и найти информацию о чем-то на их сайте не так уж и просто), но я нашел как все-таки зарегистрироваться в их программе для разработчиков. Регистрация там, скажу я вам, не такая уж и простая. Мало того что выспрашивают кучу личной информации, чуть ли не группу крови и кличку любимой собачки брата жены, так им еще и обязательно нужен валидный PayPal-аккаунт, без наличия которого о сабмите платных приложений (да и вобще каких-либо приложений, как я понял) не может быть и речи. К чему бы это? Я бы может бесплатный софт без PayPal’а постил, а ведь нет, нельзя.

Но ладно, PayPal – штука наживная (как обзавестись американским PayPal-аккаунтом я уже писал ранее), поэтому регистрация прошла более-менее гладко (но долго, помнится ответа о том, что мою информацию рассмотрели и пустили в Developer Program, я ждал около полутора недель, еще тогда меня это жутко бесило, и, как потом оказалось, не спроста).

В общем, через полторы недели недели наконец-то получил доступ к ресурсам для разработчиков. Из приятных моментов можно отметить только то, что для разработчиков доступны спецификации всех мотороловских телефонов, включая те, которе только планируются к выпуску, а также add-on’ы для Android-эмулятора для всех устройств от моторолы.

Первый блин комом

Но ладно, вдоволь наигравшись со скинами для эмулятора, решил, все-таки, приступить к делу и запостить приложение хотя бы просто “для посмотреть” как это работает и есть ли в этом смысл.

Процесс сабмита приложений оказался не менее долгим и утомительным чем регистрация. Итак, что они хотят:

  • Инсталлятор (.apk файл)
  • Указать тип приложения (программа, виджет или программа+виджет)
  • Название приложения на английском (ограничение – от 3 до 50 символов)
  • Адрес Web-страницы службы поддержки o_O
  • Полное текстовое описание (ограничение – не менее 200 и не более 400 символов, тоесть просто “Моя программа делает такую-то штуку” нельзя писать)
  • Короткое описание (ограничение – не менее 100 и не более 200 символов)
  • Категория приложения (надо обязательно указать 3 категории)
  • Перечень поддерживаемых языков
  • Указать перечень площадок, для которых будет доступно приложение
  • Для каждой площадки указать цену
  • Указать типы устройств, для которых будет доступно приложение (можно выбрать все типы или чекбоксами отдельно CLIQ и BACKFLIP – судя по всему, только для этих устройств доступен их маркет)
  • Указать откуда будут браться скриншоты o_O (доступен только один вариант почему-то, который указывеает что скриншоты будут загружаться при сабмите приложения… так и не понял глубинного смысла этой опции)
  • Еще раз для каждого маркета название
  • Еще раз для каждого маркета адрес службы поддержки
  • Еще раз для каждого маркета полное описание
  • Еще раз для каждого маркета краткое описание (фак мой мозг! мне уже надоело набивать все эти тексты руками!)
  • Лицензионное соглашение (по умолчанию предлагается стандартное, но можно указать свое. Это нововведение появилось недавно – раньше надо было свое вводить)
  • Splash-screen 240×240 PNG
  • Три (обязательно именно 3) скриншота сразмером минимум 203×176 (во-первых, откуда такие адские размеры, во-вторых, а если у меня всего один скрин и программа с одной кнопкой “Пыщь!”, что тогда делать?)

На то, чтобы разобраться с их формой сабмита (вся эта немерянная куча информации находится на одной странице и чтобы найти что-то, что, возможно, забыл, надо быть очень внимательным), у меня ушло часов 6! И это при всем том что я уже хорошо освоился с муторной процедурой сабмита в Palm’овский маркет… Но ладно, время уже не вернешь, данные я успешно заполнил и теперь пришло время постить.

Если вы думаете что на этом процесс мучений заканчивается, то вы ошибаетесь. Кроме того что вы потратили время чтобы повводить всю эту инфу, для вашего приложения еще будет организовано добровольно-принудительное автоматизированное тестирование (ну или не очень автоматизированное, но оно точно будет).

Сам факт тестирования меня лично заставляет нервничать. Мне почему-то сразу вспоминается App Store и товарищи, которые пол-года ждали аппрува приложения от Apple. На самом деле Motorola оказалась в этом плане ни разу ни лучше.

Тестируют они с помощью DeviceAnywhere. Тестирование заключается вот в чем – скачать программу, запустить, проверить что появилось главное окно. Скрыть приложение, проверить что окно исчезло, переоткрыть приложение, проверить что окно появилось, закрыть приложение, проверить что закрывается, удалить приложение – проверить что удаляется.

Собственно, процесс не опасный.

После тестирования есть еще Content Review – не знаю что они там смотрят, но это было… ДОЛГО!!!!

Первая сборка приложения ожидала ревью 2 месяца! Черт возьми, я не могу поверить что у них там целая огромная очередь желающих запостить программу в их никому не известный маркет и что из-за этого мне пришлось ждать 2 месяца! На ревью оно попало 2го февраля, ответ пришел 30го марта! И ответ заключался в том что вот они добавили сплеш-скрин (когда я сабмитил приложение, его не надо было указывать, иначе информация была бы неполной и я не смог бы отправить н атестирование), но так как его нет, то они отказались принимать приложение в маркет.

Ок.. сплеш-скрин тоже дело наживное. Добавил. К этому моменту уже ышла новая версия SMSMyFile и я поменял бинарник. Снова отправка на тестирование (оно длится от 10 до 14 рабочих дней, то есть 14 – раньше этого срока нет смысла надеяться, обычно на 14й день присылают ответ). И ога! Не прошло тестирование – приложение не запустилось в эмуляторе!

Я вот по этому поводу их не очень понял. Приложение успешно лежит себе в Android Market, работает у пользователей, даже прошлая версия, которую я сабмитил нормально работала, сертификаты те же, изменения по сравнению с прошлой версией минимальны. Почему оно у них не запустилось – загадка.

После этого интерес к мотороле у меня угас. Мне почему-то стало казаться что этим ребятам совсем не нужны деньги, они делают все возможное чтобы программы к ним не попадали.

Второй блин… тоже комом!

Снова желание у меня появилось когда выпустили приложение PDFMyWeb. Снова пришлось проходить муторный процесс ввода информации о приложение, снова ждать тестирования и снова приложение не прошло тестирование. На этот раз это была моя проблема – приложение падало при запуске на Android 1.5. Поправил, запостил снова. Хотелось бы также отметить что кроме всего прочего у моторолы очень жесткая политика по поводу подписи .apk файлов.

  • Вы собираете приложение в Eclipse
  • Затем создаете бинарник через Android Tools -> Export Signed Package
  • Этот бинарник принимается Android Market’ом
  • А вот Motorola его не примет потому что помимо вашего сертификата там еще лежит отладочный сертификат

Правильный Workflow для Motorola

  • Вы собираете приложение в Eclipse
  • Открываете его 7-zip’ом
  • Удаляете папку META-INF
  • Руками через командную строку и jarsigner подписываете .apk файл
  • Делаете оптимизацию через zipalign
  • И только после этого Motorola примет ваше приложение

На то чтобы узнать правильный Workflow у меня ушло еще 10 дней ожидания результатов тестирования 🙂

До всего пришлось додумываться самостоятельно. Тексты с описанием результатов тестирования какие-то у них мало содержательные, но люди говорят что можно писать в службу поддержки и общаться с теми кто тестировал приложение, они, теоретически, могут подсказать что именно было не так (но мне в это слабо верится, а еще более слабо верится в оперативность ответа).

И вот таконец-то мое приложение приняли!

Да, все классно, но где же Profit?

Ну да, приняли, и что дальше? Да, действительно, что дальше? Где отчеты по продажам? Как, черт возьми, я могу узнать что те деньги которые (возможно), мне пришлют – это именно та сумма, которая мне причитается?

Отчетов нет как таковых в принципе! Приходится полагаться на честность Motorola, но исходя из прошлого более чем полугодичного опыта пользования их сервисом, мне кажется что это просто непаханное поле для махинаций с их стороны. Они вполне могут сказать что за месяц не продали ни одной копии прилжения а обратного я никак не докажу!

Хотя да, эфемерные обещания каких-то отчетов у них на форуме проскакивают время от времени, но… это только обещания и многих это бесит.

Выводы

И вот после всего я сижу и думаю, а стоила ли игра свеч… Как по мне, раз уж вендор так вставляет палки в колеса и создает проблемы для разработчиков, то может не морочиться с ним, а дать им благополучно загнуться?  Хотя Motorola Droid и является одним из самых продаваемых Android-устройств, но судя по всему этот маркет для него недоступен и шансов что приложение будет супер-продаваемым здесь нет? С другой же стороны хочется иметь какой-то дополнительный поток продаж, а, соответственно, прибыли…

В общем смешанные чувства после всего этого, но негатива в разы больше.

Если у кого-то из читателей есть успех в маркете от Motorola, то хотелось бы услышать что-то по этому поводу.

Р.

Работа с акселерометром в Android

Для одного из текущих проектов понадобилась поддержка акселерометра. Учитывая то, что еще месяц назад Android API я в глаза не видел, мне казалось что получение данных с акселерометра – это какой-то адский труд. Оказалось все намного проще.

Для работы с различными датчиками в Android используется класс Sensor. Список датчиков можно получить через SensorManager. Например таким вот образом при создании Activity можно получить объект Sensor, связанный с акселеромтером:

public class AccelerometerTest extends Activity {

	SensorManager mSensorManager;
	Sensor mAccelerometerSensor;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mSensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
        List<Sensor> sensors = mSensorManager.getSensorList(Sensor.TYPE_ALL);
        if(sensors.size() > 0)
        {
        	for (Sensor sensor : sensors) {
        		switch(sensor.getType())
        		{
        		case Sensor.TYPE_ACCELEROMETER:
        			if(mAccelerometerSensor == null) mAccelerometerSensor = sensor;
        			break;
        		default:
        			break;
        		}
		}
        }
    }


Для того, чтобы получать данные с акселерометра нам необходимо проделать еще несколько несложных операций:

  • Реализовать интерфейс SensorEventListener
  • , в частности нас интересует метод onSensorChanged()

  • Реализовать метод onResume() где подписать Activity на сообщения от акселеромтера
  • Реализовать метод onPause() где отписать Activity от сообщений акселерометра
public class AccelerometerTest extends Activity implements SensorEventListener {
    @Override
    protected void onPause() {
    	mSensorManager.unregisterListener(this);
    	super.onPause();

    }

    @Override
    protected void onResume() {
    	super.onResume();
    	mSensorManager.registerListener(this, mAccelerometerSensor, SensorManager.SENSOR_DELAY_GAME);
    	mSensorManager.registerListener(this, mMagneticFieldSensor, SensorManager.SENSOR_DELAY_GAME);
    }

	@Override
	public void onAccuracyChanged(Sensor sensor, int accuracy) {
	}

	@Override
	public void onSensorChanged(SensorEvent event) {
		float [] values = event.values;
		switch(event.sensor.getType())
		{
		case Sensor.TYPE_ACCELEROMETER:
			{
				// Здесь можно обрабатывать данные от сенсора
			}
			break;
		}
	}
}

Простейший пример отображения данных – отображать их в TextView

public class AccelerometerTest extends Activity implements SensorEventListener {
	SensorManager mSensorManager;
	Sensor mAccelerometerSensor;

	TextView mForceValueText;
	TextView mXValueText;
	TextView mYValueText;
	TextView mZValueText;

	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		...
		mForceValueText = (TextView)findViewById(R.id.value_force);
		mXValueText = (TextView)findViewById(R.id.value_x);
		mYValueText = (TextView)findViewById(R.id.value_y);
		mZValueText = (TextView)findViewById(R.id.value_z);
	}
	...
	@Override
	public void onSensorChanged(SensorEvent event) {
		float [] values = event.values;
		switch(event.sensor.getType())
		{
		case Sensor.TYPE_ACCELEROMETER:
			{
				mXValueText.setText(String.format("%1.3f", 
					event.values[SensorManager.DATA_X]));
				mYValueText.setText(String.format("%1.3f", 
					event.values[SensorManager.DATA_Y]));
				mZValueText.setText(String.format("%1.3f", 
					event.values[SensorManager.DATA_Z]));

				double totalForce = 0.0f;
				totalForce += Math.pow(
					values[SensorManager.DATA_X]/SensorManager.GRAVITY_EARTH, 2.0);
				totalForce += Math.pow(
					values[SensorManager.DATA_Y]/SensorManager.GRAVITY_EARTH, 2.0);
				totalForce += Math.pow(
					values[SensorManager.DATA_Z]/SensorManager.GRAVITY_EARTH, 2.0);
				totalForce = Math.sqrt(totalForce);
				mForceValueText.setText(String.format("%1.3f", totalForce));
			}
			break;
		}
	}
}

res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<TableLayout 	android:id="@+id/TableLayout01"
				android:layout_width="fill_parent"
				android:layout_height="fill_parent"
				xmlns:android="http://schemas.android.com/apk/res/android">
	<TableRow 	android:layout_width="wrap_content"
				android:layout_height="wrap_content"
				android:id="@+id/row_force"
				android:layout_margin="5dip">
		<TextView 	android:layout_width="wrap_content"
					android:layout_height="wrap_content"
					android:id="@+id/label_force"
					android:text="Force:"
					android:gravity="right"></TextView>
		<TextView 	android:layout_height="wrap_content"
					android:layout_width="fill_parent"
					android:layout_weight="1"
					android:id="@+id/value_force"
					android:text="-"
					android:layout_marginLeft="5dip"></TextView>
	</TableRow>
	<TableRow 	android:layout_height="wrap_content"
				android:layout_width="fill_parent"
				android:id="@+id/row_x"
				android:layout_margin="5dip">
		<TextView 	android:layout_width="wrap_content"
					android:layout_height="wrap_content"
					android:text="X:"
					android:id="@+id/label_x"
					android:gravity="right"></TextView>
		<TextView	android:layout_height="wrap_content"
					android:text="-"
					android:layout_width="fill_parent"
					android:layout_weight="1"
					android:id="@+id/value_x"
					android:layout_marginLeft="5dip"></TextView>
	</TableRow>
	<TableRow 	android:layout_height="wrap_content"
				android:layout_width="fill_parent"
				android:id="@+id/row_y"
				android:layout_margin="5dip">
		<TextView 	android:layout_width="wrap_content"
					android:layout_height="wrap_content"
					android:text="Y:"
					android:id="@+id/label_y"
					android:gravity="right"></TextView>
		<TextView 	android:layout_height="wrap_content"
					android:text="-"
					android:layout_width="fill_parent"
					android:layout_weight="1"
					android:id="@+id/value_y"
					android:layout_marginLeft="5dip"></TextView>
	</TableRow>
	<TableRow 	android:layout_height="wrap_content"
				android:layout_width="fill_parent"
				android:id="@+id/row_z"
				android:layout_margin="5dip">
		<TextView	android:layout_width="wrap_content"
					android:layout_height="wrap_content"
					android:text="Z:"
					android:id="@+id/label_z"
					android:gravity="right"></TextView>
		<TextView 	android:layout_height="wrap_content"
					android:text="-"
					android:layout_width="fill_parent"
					android:layout_weight="1"
					android:id="@+id/value_z"
					android:layout_marginLeft="5dip"></TextView>
	</TableRow>
</TableLayout>

В результате получим что-то подобное:

Из примера можно увидеть что в классе SensorManager есть константы DATA_X, DATA_Y, DATA_Z, которые используются в качетсве индексов в массиве значений, возвращаемых акселерометром.
Отображение данных в TextView – это, конечно, неплохо, но не дает общей картины изменений показаний акселерометра при изменении положения телефона. Для того, чтобы увидеть изменение показаний во времени, решил добавить отображение в виде графика.
Для создания графиков набрел на чудесную библиотеку AChartEngine. Библиотека бесплатная, доступна на Google Code.
Добавляем в layout пару кнопок – для начала/останова записи показаний акселерометра и для открытия окна с графиком.

<?xml version="1.0" encoding="utf-8"?>
<TableLayout 	android:id="@+id/TableLayout01"
				android:layout_width="fill_parent"
				android:layout_height="fill_parent"
				xmlns:android="http://schemas.android.com/apk/res/android">
...
<TableRow 	android:id="@+id/TableRow01"
				android:layout_height="wrap_content"
				android:layout_width="fill_parent">
		<ViewStub	android:id="@+id/ViewStub01"
					android:layout_width="wrap_content"
					android:layout_height="wrap_content"></ViewStub>
		<LinearLayout 	android:id="@+id/LinearLayout01"
						android:layout_height="wrap_content"
						android:layout_width="fill_parent"
						android:layout_weight="1">
			<Button 	android:layout_height="wrap_content"
						android:layout_weight="1"
						android:text="Start recording"
						android:layout_width="fill_parent"
						android:id="@+id/button_start"></Button>
			<Button 	android:layout_height="wrap_content"
						android:layout_weight="1"
						android:text="Show"
						android:layout_width="fill_parent"
						android:id="@+id/button_show"></Button>
		</LinearLayout>
	</TableRow>
</TableLayout>

В результате этих изменений получаем такой layout:

Теперь научим Activity реагировать на нажания кнопок:

package com.itdimension.accelerometertest;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

import org.achartengine.*;
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;

public class AccelerometerTest extends Activity implements SensorEventListener {
	...
	double margins[] = {0, 0};

	Button mStartButton;
	Button mShowButton;

	List<List<Double>> mValues;
	boolean mIsRecording = false;

	OnClickListener mStartButtonListener = new OnClickListener() {

		@Override
		public void onClick(View v) {
			mIsRecording = !mIsRecording;
			if(mIsRecording) {
				mValues.get(SensorManager.DATA_X).clear();
				mValues.get(SensorManager.DATA_Y).clear();
				mValues.get(SensorManager.DATA_Z).clear();
				margins[0] = 0;
				margins[1] = 0;
			}
		}
	};

	OnClickListener mShowButtonListener = new OnClickListener() {

		@Override
		public void onClick(View v) {
			try
			{
				Intent intent = getChartIntent();
				startActivity(intent);
			}
			catch (Exception e) {
				new AlertDialog.Builder(AccelerometerTest.this)
					.setTitle("Error")
					.setMessage(e.getMessage())
					.create()
					.show();
			}

		}
	};

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        ...
        mValues = new ArrayList<List<Double>>();
        mValues.add(new ArrayList<Double>());
        mValues.add(new ArrayList<Double>());
        mValues.add(new ArrayList<Double>());
	...
        mStartButton = (Button)findViewById(R.id.button_start);
        mShowButton = (Button)findViewById(R.id.button_show);

        mStartButton.setOnClickListener(mStartButtonListener);
        mShowButton.setOnClickListener(mShowButtonListener);
    }
    ...

	@Override
	public void onSensorChanged(SensorEvent event) {
		float [] values = event.values;
		switch(event.sensor.getType())
		{
		case Sensor.TYPE_ACCELEROMETER:
			{
				if(mIsRecording)
				{
					recordSensorValue(event);
				}
				...
			}
			break;
		}
	}

	private void recordSensorValue(SensorEvent event) {
		double value;
		for(int i = SensorManager.DATA_X; i <= SensorManager.DATA_Z; i++)
		{
			value = (double)event.values[i];
			margins[0] = Math.min(margins[0], value);
			margins[1] = Math.max(margins[1], value);
			mValues.get(i).add(value);
		}
	}

	Intent getChartIntent() {
		int [] colors = new int[] { 
		      Color.RED, Color.GREEN, Color.BLUE };
		PointStyle[] styles = new PointStyle[] { 
		      PointStyle.POINT, PointStyle.POINT, PointStyle.POINT };
		XYMultipleSeriesRenderer renderer = buildRenderer(colors, styles);
		setChartSettings(renderer, "Sensor Values", "Index", "Value",
	    		0,
	    		mValues.get(SensorManager.DATA_X).size(),
	    		margins[0] * 1.5,
	    		margins[1] * 1.5,
	        	Color.GRAY, Color.LTGRAY);
		return ChartFactory.getLineChartIntent(this, buildDataset(), renderer);
	}

	protected void setChartSettings(XYMultipleSeriesRenderer renderer, 
		      String title, String xTitle,
		      String yTitle, double xMin, 
		      double xMax, double yMin, double yMax, 
		      int axesColor, int labelsColor) {
		    renderer.setChartTitle(title);
		    renderer.setXTitle(xTitle);
		    renderer.setYTitle(yTitle);
		    renderer.setXAxisMin(xMin);
		    renderer.setXAxisMax(xMax);
		    renderer.setYAxisMin(yMin);
		    renderer.setYAxisMax(yMax);
		    renderer.setAxesColor(axesColor);
		    renderer.setLabelsColor(labelsColor);
		  }

	protected XYMultipleSeriesRenderer buildRenderer(int[] colors, PointStyle[] styles) {
	    XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer();
	    int length = colors.length;
	    for (int i = 0; i < length; i++) {
	      XYSeriesRenderer r = new XYSeriesRenderer();
	      r.setColor(colors[i]);
	      r.setPointStyle(styles[i]);
	      renderer.addSeriesRenderer(r);
	    }
	    return renderer;
	  }

	XYMultipleSeriesDataset buildDataset() {
		XYMultipleSeriesDataset result = new XYMultipleSeriesDataset();
		XYSeries xSeries = new XYSeries("X");
		XYSeries ySeries = new XYSeries("Y");
		XYSeries zSeries = new XYSeries("Z");

		int count = mValues.get(SensorManager.DATA_X).size();
		for(int i = 0; i < count; i++)
		{
			xSeries.add(i, mValues.get(SensorManager.DATA_X).get(i));
			ySeries.add(i, mValues.get(SensorManager.DATA_Y).get(i));
			zSeries.add(i, mValues.get(SensorManager.DATA_Z).get(i));
		}

		result.addSeries(xSeries);
		result.addSeries(ySeries);
		result.addSeries(zSeries);

		return result;
	}
}

После всех этих манипуляций, при нажатии на кнопку “Show” получим приблизительно такой график:


Ну вот, на этом пока все.
Скачать исходный код примера можно здесь.