суббота, 17 декабря 2011 г.

Отзывчивый интерфейс на Android

Если вы делате что-то сверх тривиального Hello World, то обеспечить "моментальную", без всяких тормозов реакцию интерфейса на действия пользователя дело не такое тривиальное, как это может показаться. Рассмотрим это на примере из простого графического редактора, который я сейчас разрабатываю.

 

Встроенного механизма для хранения всех операций рисования нет, при каждом вызове процедуры рисования мы должны нарисовать всю картинку. Первый, логичный шаг - сделать класс DrawOperation, хранить все экземпляры в упорядоченном списке и в процедуре рисования быстренько по этому списку пробежаться и все нарисовать. При этом заодно элементарно решается выполнение undo: удалили последний элемент из списка и привет. В каждом экземпляре DrawOperation хранится траектория движения "кисти", вид кисти, цвет, размер и т.п. Очень удобно и логично.

Проблема лишь в том, что "быстренько" не выйдет. Уже после нескольких мазков нашей виртуальной кистью отрисовка начинает заметно притормаживать.

Шаг второй, не менее логичный. Заводим виртуальный холст (Bitmap+Canvas), на котором и будем выполнять само рисование, а затем просто выводить его поверх заранее подготовленного фона. А список операций будем вести параллельно, он нам таки пригодится для undo.

Замечу (но разъяснять не стану, поверьте  - он понадобится), что в действительности придется завести еще один виртуальный холст, для текущей операции. По завершении они объединяются, а в процессе накладываются по очереди.

Все замечательно, но все равно притормаживает, а undo выполняется вообще неимоверно долго.

Шаг третий, вместо списка операций храним список "снимков" экрана. Сжатие в PNG позволяет нам хранить довольно большое их количество, делаем это во вспомогательном потоке с меньшим приоритетом, запускаем его когда текущая операция завершается и возникает естественная пауза, пока пользователь не ведет стилусом (пальцем, языком или чем там еще) по экрану, а переставляет его на новое место.

Undo вписалось в приемлемые рамки, теперь мы просто достаем предпоследний снимок, а последний удаляем. Ну а чего же оно тормозит-то ТЕПЕРЬ?

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

Шаг четвертый. Делаем статический класс Trash, который будет у нас содержать коллекцию объектов, которые надо бы удалить, но не хочется вызвать этим появление на сцене сборщика мусора. И конечно у него будет метод освобождающий весь этот хлам - в тот момент, когда мы надеемся на лучшее ничем важным не заняты. Это не гарантирует, что сборщик вызовется именно в этот момент, но скорей всего так и произойдет, особенно если подсказать системе вызовом System.gc()

Шаг пятый, шерстим весь код и избавляемся от лишнего создания/удаления объектов. Т.е. если объект можно использовать повторно, то лучше его повторно использовать. В частности не зачем для каждой операции создавать новый Bitmap+Canvas, лучше просто почистить тот, что остался от предыдущей.

 

И конечно лучше об этом думать сразу.

среда, 7 декабря 2011 г.

Ошибка резидента MotionEvent & SurfaceView

После пары дней пыхтения и потения над отладкой тривиального графического редактора, обкусав ногти до локтей и выщипав все волосы в разных местах выяснил два момента. Может кому пригодится если сумеет найти данный текст.

Первая проблема известна в англоязычных энторнетах как SurfaceView flickering. Так вот если у вас картинка моргает, то почти сто процентов уверенности, что вы накосячили с синхронизацией потоков. Т.е. рисуете вы в одном потоке, а готовите картинку в другом и где-то лопухнулись. Вдумчиво изучите как у вас происходит синхронизация, прокол может быть совершенно неочевидным. Второй вариант - вынести все действия по подготовке картинки в один поток, тот же, что занимается рисованием. Тут нас может подстерегать другая проблема, названия ей я не нашел, но крови она мне попортила изрядно.

Итак, есть у нас обработчик кликов:

boolean onTouchEvent(MotionEvent e)

и есть желание эту обработку перенести в тот же поток, где у нас происходит формирование картинки. К слову сказать, SurfaceView был разработан в частности для того, чтобы эту обработку разнести по разным потокам :), но иногда это все таки может быть нужно. Так вот, делаем мы это элементарно, вариантов много, но все они сводятся к тому чтобы поместить событие в очередь, а в другом потоке эту очередь опросить и обработать. Так вот внимание, сюрприз:

 

Объект MotionEvent e, который получает наш обработчик передавать в другой поток для отложенной обработки НЕЛЬЗЯ. Потому что он существует в единственном экземпляре и в наш обработчик передается лишь ссылка на этот экземпляр. И как только вы выйдете из обработчика (положив эту ссылку в очередь), система с чистой совестью может поместить туда новое значение. В результате, если обработка у нас запаздывает, и мы успели  положить к себе в очередь три события MotionEvent, то все три будут указывать на одно и то же событие.

В результате мы одновременно а) теряем события и б) они у нас дублируются. Я плакаль...

Хрен вы это где найдете в документации. Это наглядный пример результатов телепатической связи между разработчиками Android SDK (которые полагают подобное поведение очевидным) и конечными его пользователями.

Вывод: верить можно только себе, и то, с осторожностью. Т.е. любой системный объект (в данном случае MotionEvent) должен быть обработан там, где он получен  и не следует рассчитывать что он сохранит своё состояние после выхода из обработчика. Если нам нужно сделать "ленивую обработку", то нужно получить "твердую копию", т.е. объект состояние которого полностью контролируется нашей программой, а не системой.

понедельник, 5 декабря 2011 г.

Андроид. Вести с полей

Наконец-то я плотно занялся этим девайсом. Делаю несложный графический редактор, узнаю много нового и интересного.

 

Моя первая попытка сделать его с использованием OpenGL ES1.0 потерпела полнейшее фиаско. Для редактора пиксельной графики OpenGL (по крайней мере ES1.0) подходит как квадратные колеса для велосипеда. Т.е. как-то приспособить может быть и можно, и даже едет, но совершенно не тривиально и с жутким скрыпом.

 

Вторая попытка была более удачной. Обычный Canvas заточен как раз под пиксельную графику, без всяких заморочек с 3D. Осложняет задачу то что я с ним не работал никогда (в отличие от OpenGL) и многие проблемы и их решение для меня неочевидны. А от некоторых спецэффектов, появившихся в процессе работы у меня волосы встали дыбом во всех местах, я так и не понял как такое возможно. Скажем появился у меня объект класса Picture, который по разному рисуется при четном и нечетном вызове процедуры рисования. 

 

Пришлось сразу задуматься об оптимизации. Если рисовать всю картинку каждый раз заново - быстродействие падает стремительными темпами. Хранить все действия в Picture - возможный вариант, но не столь тривиальный, как хотелось бы. В результате мучительных поисков, решение было найдено и оказалось совершенно тривиальным, более того, о нем вскользь упомянуто в гугловской документации. Их документация это вообще отдельная тема. Полнотой и подробностью она не страдает. 

 

Если кому интересно - нужно создать Bitmap, обернуть его в Canvas и спокойно рисовать туда. А когда дело доходит до обновления экрана - рисовать этот Bitmap в Canvas экрана. В этом случае время отрисовки стабильно и достаточно невелико.

 

P.S. Всплыл интересный факт - размер сенсорной области у меня меньше видимой области экрана. Если разместить какой-то мелкий элемент на краю экрана, то для нажатия он будет недоступен. Практический пример: ползунок (SeekBar) шириной во весь экран невозможно довести до крайних положений.

пятница, 2 декабря 2011 г.

Ограничения OpenGLES 1.0

Оказывается мой девайс не поддерживает OpenGL ES 2.0. Что более неприятно - эмулятор тоже его не поддерживет, так что возможности отлаживать приложения под 'эту версию я лишен начисто. Придется осваивать 1.0. Сразу понятно, что некоторые вещи ориентированные на удобство программиста отсутствуют в принципе, но есть и другие нюансы. Этот пост будет постоянно пополнятся по мере вникания в тему

glPointSize не работает. Размер точки задаваемый этой функцией игнорируется напрочь. Точка выводится как точка, вне зависимости от заданного размера.

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

 

 

Бардак на корабле:

есть glClearColor(float,float,float,float) и glClearColorx(int,int,int,int);

и есть glClearDepthf(float) и glClearDepthх(int);

прозевали? 

четверг, 1 декабря 2011 г.

Multi-touch которого нет

Как ведет себя Android без поддержки MultiTouch технологии, когда вы касаетесь экрана в нескольких местах сразу? Прямо скажем - неважно. Может проскочить событие с координатами  одной точки, может проскочить с координатами другой - поскольку ничего одновременного в этом мире не бывает. А в основном идет нечто среднее  - результирующая точка где-то посередине между двумя настоящими.

 

Интерфейс программы должен это учитывать и не провоцировать пользователя хвататься за экран всеми лапами сразу. А если интерфейс как бы намекает на возможность multi-touch, то должна быть фильрация ошибочных нажатий. Подозреваю что немалая часть нареканий к управлению играми связана с тем что разработчики об этом не подумали. Пользователь в азарте вцепляется в девайс и зажимает какие-то области на экране -  программа получает левые координаты нажатий и либо ведет себя некорректно, либо просто игнорирует. Отсюда вывод второй - хорошим тоном является показать пользователю куда он нажал. В том числе если он нажал "в никуда".

 

Еще один момент - чем пользователь нажимает. Можно нажимать пальцем, можно нажимать стилусом. Если предполагаются нажатия пальцем, то элементы управления должны иметь соответствующий размер и при проверке попадания нажатия в элемент. должна контролироваться соответствующих размеров область. Если стилусом - их можно сделать меньше, но возможны проблемы с дешевыми аппаратами: если сенсорный экран плохо откалиброван, то в мелкий элемент вы не попадете. Отсюда еще раз можно подчеркнуть - нужно показывать нажатия "в никуда"

понедельник, 28 ноября 2011 г.

Блокировка при записи в ObjectOutputStream

Получил сегодня такой интересный эффект. Разбирательство показало, что добавился класс, которому не задан интерфейс Serializable. При этом сам он в явном виде в поток не пишется, а может оказаться в значении поля внутри другого класса, который имеет Serializable и пишется в поток.

 

Но самая главная пакость заключается в другом. На тестовых примерах такая ошибка вызывает java.io.NotSerializableException, но в данном конкретном случае этого почему-то не происходит. Оладка показала, что ObjectOutputStream обнаруживает проблемы, но вместо того чтобы тихо мирно вывалить эксепшн, пытается записать его в выходной поток (writeFatalException)! Немножко поразмыслив понял что это правильно - на другой стороне в процессе приема объекта вместо очередного поля обнаружится эксепшен и вывалится с ним, а иначе мы рискуем получить блокировку уже там (поскольку объект целиком никогда не прийдет.) В моем случа это не важно, но универсальность должна быть универсальной, хуже от этого быть не должно... Однако стало и по прежнему неясно, где же приключился затык.

 

Смотрим дальше... затык происходит при вызове slotDesc.invokeWriteObject(obj, this) ,  где obj это экземпляр NotSerializableException. Теоретически должен вызваться метод writeObject этого класса.

NotSerializableException и его родитель, ObjectStreamException, IOException, Exception  нужного метода не содержат, обнаруживается он только в Throwable и тут мы снова возвращаемся к ObjectOutputStream, метод defaultWriteObject(). Все что я могу тут сказать - мы туда приходим и впадаем в ступор где-то на выводе состояния стека. Что там такого криминального обнаружилось я право не знаю. Отладка становится трудоемкой и слабоосмысленной без каких-нибудь специальных инструментов, которыми я не владею. 

воскресенье, 27 ноября 2011 г.

Кто тут еще мне расскажет про супердостоинства Open Source?

Скачал, поставил и запустил LAMPP. Точно также как я это делал десятки раз за последние лет 5. И все всегда работало и не требовало никаких мысленных усилий (поправил конфиги и вперед). А тут на тебе - MySQL не запустился. Хорошо он мне сейчас не нужен и разбираться кто чего и где поломал мне не интересно.  Просто это типичная ситуация в этой среде. Все работало, потом бац! - перестало. Иногда это бац происходит при очередном обновлении и как откатится назад для простого пользователя не очевидно.

 

Еще до кучи:

у видеокарты ATI Radeon X1600PRO - проблемы с драйверами. Регулярно падает blender, странно себя ведет jogl

у встроенной видеокарты - тоже проблемы с драйверами. Другие. Иногда ее клинит и на экране появляются разные странные артефакты при выделении.

И чаво? Еще одну видюху покупать в надежде что с ней все будет ОК?