Рассказ о том, как мне захотелось написать простенький интерфейс под андроид и сколько неожиданных препятствий мне пришлось преодолеть. Акцент здесь сделан не на слове “препятствий”, а слове “неожиданных”.
Но обо всём по порядку.
Решил я начать с 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/итд этот опыт почти не пригождается – здесь многое по другому. Постоянно приходится преодолевать неожиданные препятствия. Наверняка гугль придумал свою систему гораздо лучше, чем люди до него, но моим мозгам, испорченным всем выше перечисленным, пришлось трудновато приобщаться к прекрасному.
- Совет «спроси у гугля» выглядит попросту издевательством.