Pull to refresh

Мой опыт написания программы под Android

Development for Android *
Sandbox


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

Идея пришла довольно внезапно и натолкнул меня на нее сайт livetyping.ru. «А почему бы и нет» подумал я и начал думать, как это будет выглядеть и как это писать. Кто поленился зайти на сайт, поясню, что смысл в том, чтобы юзер написал текст, а он потом сгенерировался в гиф файл, отображая процесс написания, в том числе все изменения. Получается довольно забавно, можно писать сообщение с одним смыслом, потом все вытирать и писать с абсолютно противоположным. Вообще, пользователя ограничивает только его фантазия.
Кто-то скажет что идея программы не очень… поскольку многие Android-телефоны не умеют просматривать анимированный гиф встроенными средствами. И отчасти будет прав, поскольку судя по отзывам в интернете у многих Android'ов эта поддержка отсутствует. Но, как оказалось, мой телефон с прошивкой 2.3.3 умеет это делать, что вселило в меня надежду что в будущем производители добавят поддержку во все девайсы. Может несколько оптимистично, но по оф. данным Android должен уметь декодировать гиф (правда не сказано какой).

Разработка


Ядро программы

Первая сложность заключалась в том, что сгенерированный текст должен быть в точности так же отформатирован, как и оригинал. Было два варианта:

  1. Сделать сервер, на который отправлять весь набранный пользователем текст, размер поля, размер и цвет шрифта, генерировать гиф и выдавать обратно ссылку или сам файл.
  2. Генерировать все на клиенте.

Сразу же был выбран второй вариант, поскольку с вебом я не дружу, та и не нужно затрат на сервер.
Для того, чтобы сделать гиф анимацию, нужно было сначала сгенерировать все кадры, а потом уже собрать их в кучу. Подумав, что данная операция будет быстрее работать на си, а не на джаве, я покопался в инете и нашел исходник нужного мне класса, который успешно скомпилил при помощи NDK. Данный класс позволял добавлять кадр как массив пикселей, полученный с экземпляра класса Bitmap, генерируя в конце анимированный гиф файл.
Прямо так, как написано в примере по ссылке:

static 
{
     System.loadLibrary("gifflen");
} 

....

public native int Init(String gifName, int w, int h, int numColors, int quality, int frameDelay);
public native void Close();
public native int AddFrame(int[] inArray);

....

// Filename, width, height, colors, quality, frame delay
if(Init("/sdcard/foo.gif", width, height, 256, 100, 4) != 0)
{
     Log.e("gifflen", "Init failed");
}

int[] pixels = new int[width*height];

// bitmap should be 32-bit ARGB, e.g. like the ones you get when decoding
// a JPEG using BitmapFactory
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);

// Convert to 256 colors and add to foo.gif
AddFrame(pixels);
Close();

Следующей задачей было додуматься как рендерить набранный текст в Bitmap. Покопавшись в Android API, был найден класс StaticLayout, который позволяет нарисовать данный текст при помощи Paint в Canvas, который в свою очередь в Bitmap. Все это выглядит приблизительно так:

Canvas canvas = new Canvas();
TextPaint paint = new TextPaint(Paint.LINEAR_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
Bitmap bitmap;
	
paint.setStyle(Paint.Style.FILL);
paint.setColor(fontColor);
paint.setTextSize((int) (fontSizeDP * mDensityScale + 0.5f));
paint.setAntiAlias(true);
paint.setTypeface(font);
paint.setTextAlign(Align.LEFT);

StaticLayout[] layoutTexts = new StaticLayout[mLiveTypeStringArray.size()];
			
for(int i = 0; i < layoutTexts.length; i++)
{
		layoutTexts[i] = new StaticLayout(mLiveTypeStringArray.get(i), paint, (int) (gifWidthDP * mDensityScale + 0.5f), Alignment.ALIGN_NORMAL, 1, 1, true);
		if(gifHeight < layoutTexts[i].getHeight())
			gifHeight = layoutTexts[i].getHeight();
}
	
if(gifHeight == 0)
	return 0;
	
bitmap = Bitmap.createBitmap((int) (gifWidthDP * mDensityScale + 0.5f), gifHeight, Bitmap.Config.RGB_565);
canvas.setBitmap(bitmap);

for(int i = 0; i < layoutTexts.length; i++)
{
		canvas.drawColor(backColor);
		layoutTexts[i].draw(canvas);
		gifEncoder.addFrame(bitmap);
}

Отлично! Набранный текст, точнее каждое его изменение, рендерилось в Bitmap'ы, из которых собирался гиф файл.
Потом появилась следующая проблема. Гиф файлы получались огромными! В среднем, гиф до 200 кадров занимал 3-5 мб. Это очень много, особенно если нужно его отправить с мобильного устройства. Затем я обратил внимание, что на livetyping подобные гифки занимают куда меньше (30-50 кб) и поинтересовался у автора как он достиг такого результата. Ответом была библиотека Gifsicle, которая могла очень сильно и качественно оптимизировать гифки. Подобный результат достигался благодаря сохранению не каждого отельного кадра, а только разницы между ними, что идеально подходило нам для сохранения анимированного текста.
Дальше я занялся портированием Gifsicle на Android при помощи NDK, которое вскоре удачно завершилось.
Кому интересно, вот мой Android.mk:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := malloc
LOCAL_SRC_FILES := fmalloc.c dmalloc.c
LOCAL_ARM_MODE := arm
LOCAL_CFLAGS := -DFPM_ARM -ffast-math -O3
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
include $(BUILD_STATIC_LIBRARY)

include $(CLEAR_VARS)

LOCAL_MODULE := gifwrite
LOCAL_SRC_FILES := ungifwrt.c
LOCAL_ARM_MODE := arm
LOCAL_CFLAGS := -DFPM_ARM -ffast-math -O3
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
include $(BUILD_STATIC_LIBRARY)

include $(CLEAR_VARS)

LOCAL_MODULE := gifopt
LOCAL_SRC_FILES := clp.c giffunc.c gifread.c gifunopt.c \
		merge.c optimize.c quantize.c support.c xform.c \
		gifsicle.c
LOCAL_ARM_MODE := arm
LOCAL_CFLAGS := -DFPM_ARM -ffast-math -O3
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_LDLIBS := -llog
LOCAL_STATIC_LIBRARIES := gifwrite malloc
include $(BUILD_SHARED_LIBRARY)

Каково же было мое удивление, когда я увидел, что на мобильном устройстве она работает неверно. Генерация и оптимизация одного и того же гиф файла каждый раз давала разный результат. Очень разный, разброс был от 50кб до 2 мб. Тогда я начал опять компилить, перекомпиливать с разными параметрами, искать утечку памяти и т.д… что угодно, чтобы объяснить этот феномен. Но я не находил ничего. Так прошло недели две.
Одним прекрасным утром я осознал что сбоить может не Gifsicle, поскольку ее автор тоже не мог понять в чем проблема, а первая библиотека(класс), который генерирует гиф. Я выключил оптимизацию и посмотрел на результат: файлы были почти всегда одного и того же размера, только иногда отличались на 1-2кб. Странно, но вроде не смертельно. Не хотев ковыряться в чужом сыром си коде, я нашел аналогичную библиотеку на Java ME, и портировал ее на Android. Так же заметил для себя поразительное сходство Java ME и Android API — много почти идентичных функций (раньше дела с ME не имел). Теперь все стало нормально.

Но опять таки не на долго… оказалось, что после оптимизации при проигрывании гиф файла на моем Android-устройстве при помощи стандартного Image View'ера иногда случаются ужасные глюки цветов и пропадание кадров. При этом на компе этот же файл просматривался отлично. Я подумал, что это все из-за плохой поддержки вьювером LZW сжатия и отключил его в сорсах Gifsicle. Теперь все действительно стало хорошо, хоть и файлы начали занимать немногим больше чем раньше.

Интерфейс

До этого момента я потратил где-то месяц. За второй месяц предстояло написать интерфейс программы. Мне всегда хотелось использовать какой-то Android UI pattern, из всех возможных я выбрал Dashboard. Поскольку «все уже написано до нас», я скачал исходники Google IO и взял код оттуда. Получилось где-то так:


Окно написания нового сообщения получилось таким:

Можно просмотреть написанное анимированное сообщение или сгенерировать его в гиф анимацию, после чего появляется кнопка «Share» с возможностью отправить по email, mms, twitter и т.д.

Высота поля для набора выбиралась так, чтобы на разных экранах клавиатура его не закрывала. Самые маленькие экраны были забанены, потому что там клавиатура занимает ну очень много места. Конечно, можно было бы сделать высоту, зависящую от размера экрана, но… стало лень.

Потом были сделаны настройки на основе стандартного класса PreferenceActivity:


А также список моих сообщений.
На карте памяти сохраняется только массив текстов (кадров) в бинарном виде. При просмотре сообщения имеется возможность сгенерировать его заново, предварительно изменив настройки (цвет и размер шрифта, цвет фона, скорость анимации).

Результат


Сначала планировалось две версии: бесплатная и платная. Сейчас реализована только бесплатная, которая находится в маркете около 20 дней. В платной планировалось больше шрифтов, больше цветов шрифта и фона, больше ограничение кадров — 500 (сейчас 140). Но, к сожалению успехи бесплатной версии оставляют желать лучшего: всего установок 583, из них активных 227, то есть больше половины скачавших уже удалили. Видимо идея не прокатила =)
Поэтому, развитие программы приостановлено и вряд ли будет продолжено.

На этом все, всем спасибо за внимание! На Ваши вопросы отвечу в комментах. Так же прошу прощения за возможные ошибки.

Update: Несколько мини-обзоров на зарубежных сайтах:
www.androidapplog.com/2011/09/android-app-reviews/animado-animated-messaging-app-review
www.addictivetips.com/mobile/animado-for-android-share-animated-text-messages-as-gif-files

Кому интересно вот ссылка на маркет.
Tags:
Hubs:
Total votes 59: ↑49 and ↓10 +39
Views 13K
Comments Comments 32