Pull to refresh

Анимация под Android, или спроси у Google

Рассказ о том, как мне захотелось написать простенький интерфейс под андроид и сколько неожиданных препятствий мне пришлось преодолеть. Акцент здесь сделан не на слове “препятствий”, а слове “неожиданных”.

Но обо всём по порядку.

Решил я начать с GUI. Имея за плечами изрядный опыт работы с Java и GUI (очень разными — от MFC до Qt — но, как выяснилось, такими одинаковыми) понаделся прорваться на халяву. В качестве усложняющего фактора решил навесить немного анимации на кнопки.

Итак задача — сделать двигающиеся и расширяющиеся кнопки. Т.е. при нажатии на кнопку она должна расширяться, а остальные соответсвенно сдвигаться/сжиматься.


Сразу же нашёл в (так называемой) документации масштабирование. Но оно работает с видами (view – именно так называются все окна и контролы в андроиде), как с картинками. Не годится — расширяются также границы кнопки — выглядит убого.

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

Начинаю расставлять кусочки картинки по нужным местам. LinearLayout как раз для такого случая – собираются сначала горизонтальные строки – затем складываются друг на друга. Порядок определить просто, а размер задаётся через setWidth() и setHeight(). А вот нет – эти функции несмотря на всю очевидность не работают ни разу. Что на это говорит документация? – Должны работать (пруф ). Начинаем копать почему. Логика работы layout довольно хитрая. Работает всё в несколько заходов с вызовом protected функций, которые необходимо переопределить… Если короче, то правильный ответ – это магия — надо использовать setLayoutParams(new LinearLayout.LayoutParams(width, height)). Зато логика работы layout проявляется в том, что в момент создания высота и ширина не известны. Поэтому попытка растянуть картинку при инициализации обречена – новая ширина и высота зависит от старой. Короче – ждём onWindowFocusChanged() и в нём устанавливаем размер.

Приступаем к анимации. После путешествия в дебри андроида использовать родную анимацию желание пропадает. Будем проще – создаём отдельный поток и двигаем всё, что надо. Не работает – из другого потока трогать объекты интерфейса нельзя (это даже не документация – это Exception). Не вопрос – запускаем таймер – двигаем по таймеру. Не работает – таймер запускается в отдельном потоке (это правда только документация – в реальности может быть как с setWidth() – т.е. как угодно – я не решился проверять). Возвращаемся к родной анимации.

Итак – родная анимация умеет масштабировать и двигать. Нам большего не нужно. (А ведь был соблазн обойтись только масштабированием — остальные кнопки должны естественным образом сдвигаться. Но нет — при масштабировании контейнер не изменяет размер. Приходится двигать и внимательно следить за взаимным положением всех объектов на экране). Средние части масштабируются, нижние или верхние двигаются. Как синхронизировать анимацию? Очевидно. Задать одинаковое время старта. Результат тоже очевиден. Задать в точности один и тот же объект анимации всем окнам. Результат тот же. Правильный ответ (с точностью до видимых эффектов на конкретном телефоне) – задавать разные, но с одинаковыми параметрами, объекты анимации для всех частей. Почему? Магия. (Кстати – clone() для создания таких одинаковых объектов существует, но он protected – видимо, из вредности).

Кульминация. Необходимо сделать что-нибудь вроде нескольких последовательных анимаций (расширить / сжать). Варианта два:

  • Отменить анимацию с одновременным изменением размеров частей. Следующую анимацию применять к «чистому» (неискаженному объекту). Считается, что так правильно (Кем считается? Официальной документацией).
  • Не отменять анимацию, а придумывать следующую с учётом текущего искажения объекта.


Первый вариант отметается достаточно легко. Одновременно отменить анимацию и изменить размер части нельзя. Оно обязательно мигнёт. Очень заметно мигнёт. Не помогает даже запрещение функции onDraw() (а именно она отрисовывает вид на экран) рисовать в момент переключения. Ничего не помогает.

Второй вариант отметается сложнее. Полностью отсутствует внятная документация, каким образом различные преобразования совмещаются. Со сдвигом то всё просто – его можно применять в любом порядке и независимо по каждому параметру. С масштабированием всё гораздо сложнее. Масштабирование сдвинутого и не сдвинутого изображения сильно отличается. А ведь ещё можно (и нужно) задавать центр масштабирования, который после первой же трансформации начинает обитать в неизвестной системе координат (Как то мне пришлось работать с системой, в которой физически существовало 6 различных динамических систем координат – так что я очень отчетливо представляю себе, что такое обитание в неизвестной СК – второй раз я на такое не пойду). Более того – даже если картинка кнопки переместилась, область обработки события нажатия – нет – так что придётся еще и её пересчитывать и специальным образом обрабатывать нажатия.

Короче: ищем третий вариант. Его нет – используем первый. Испробовал N-е количество способов — всех и не упомнишь — ничего не помогает. После танца с бубном получается приемлемый вариант (почти без миганий) путём изменения порядка расположения окон (окно, которое меньше всех склонно к миганию отрисовывается сверху).

Последний штрих. Отрисовка текста на кнопке. В тексте главное – граница — а точнее, чтобы он не рисовался за границей кнопки. Кладём на нашу кнопку еще один вид. Забиваем на масштабирование картинки. Тут почему-то трудностей не оказалось — canvas.getMatrix().getValues(m)[4] = 1.0 в onDraw() (если кто программировал одновременно 6 систем координат сразу пойдёт, что здесь изменять нужно именно 4 элемент матрицы — кто не программировал — может попробовать все варианты — их всего 9) и в том же onDraw() рисуем текст. Границы текста определяются границами вида, который масштабируется – всё хорошо (Внимательный читатель и тут наверняка заметил магию — границы кнопки меняются, а границы LinearLayout — нет). Только чуть чуть мигает. Всякие жалкие попытки исправить мигание (например, путём скрывания трансформированной надписи с одновременным отображением другого вида, выглядящего точно также) обречены. Мигает. Но не сильно – на быстрых устройствах практически не заметно.

Оргвыводы. Писать под андроид – весьма увлекательное занятие. По пунктам:

  • Качество документации. Оно отвратительно. Большинство функций задокументированы путём расстановки пробелов в названии функций (пруф). При чтении некоторых в голову почему-то приходят слова “надмозг” и “расовый индийский” (пруф). Ну а в некоторых случаях документация вообще отсутствует. В качестве упражнения повышенной сложности попробуйте в runtime (не через XML) создать ProgressBar в виде полоски, а не кругляшка.
  • Сообщество гораздо слабее, чем в других областях (я имею ввиду как минимум Java, C/C++ и Ruby). Пишущих под андроид физически гораздо меньше. На многие вопросы ответа в интернете просто нет.
  • Сильно нестандартная логика работы. Для людей, выросших на MFC/VCL/Qt/wxWidgets/WinForms/AWT/итд этот опыт почти не пригождается – здесь многое по другому. Постоянно приходится преодолевать неожиданные препятствия. Наверняка гугль придумал свою систему гораздо лучше, чем люди до него, но моим мозгам, испорченным всем выше перечисленным, пришлось трудновато приобщаться к прекрасному.
  • Совет «спроси у гугля» выглядит попросту издевательством.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.