среда, 17 ноября 2010 г.

Android widget, part 0

Зуд под коленкой не дает уснуть, требует выплеснуть полученный экспириенс, так что я попробую на пальцах рассказать, как сделать виджет-часы для андроида; на пальцах – потому что (как я до сего времени любил говорить про Си) про яву я не прочитал ни одной книжки, поэтому предметом особо не владею – так, понахватался понемногу отовсюду.

Результатом должно стать вот это:

Результат

При добавлении виджета должен появляться экран настроек, где можно указать – какую программу запускать после тапа по области с датой и часами и как (12/24) выводить время:

Настройки

Начинать надо с начала – с дезигна.

Для начала надо прочитать ту часть документации, где обьясняется, как рассчитать размер будущего виджета; К.О. утверджает, что это должен быть виджет 4*1, т.е. высотой в 1 “клетку” и шириной в 4; тогда по формуле из документации ширина нашего виджета = (number of cells * 74) - 2 = 294 пиксела и высота = 72; эти значения нужны для описания виджета, но сначала они понадобятся для рисования эскиза в фотошопе или гимпе.

Для натуралистичности рисовать прототип лучше всего поверх настоящей картинки, легко и просто взять ее с эмулятора (ежу понятно, что к этому моменту надо установить эклипсу или идею, скачать сдк и понастраивать все, вот чего-чего, а детальных прошождений этого процесса в инетах тыща и маленькая тележка). После установки сдк в папке диск:\путь_к_сдк\tools есть куча полезных программок, сейчас нужна ddms.bat (юные помошники К.О. дружно идут в сад, декламируя, что .bat - это не программа и все такое); после ее запуска появляется окно “Davlik Debug Monitor”, в котором видим подключенные для отладки телефоны или эмулятор и кучу полезных окошек с разной инфой; сейчас нам надо скриншот из эмулятора – выбираем эмулятор из списка (если вдруг там есть еще что-то.. или выбираем устройство, в общем то разницы никакой) и жмем Ctrl-S – появляется окошко со скриншотом:

Скриншот Сохраняем в файл (или копируем) и открываем в графическом редакторе.

В виджете (в отличие от “обычных” программ) могут использоваться только ограниченный набор контролов – важно то, что нельзя использовать кастомные контролы (а я сдуру был сделал кастомный контрол, отладил его и только потом выяснил, что зря старался.. ну не то, чтоб зря, но в общем начал все заново); фактически виджет больше похож на сверстанную страничку, где можно использовать готовые изображения, текст и несколько специализированных контролов – я бы допер до этого намного раньше, если бы использовал еще один инструмент из сдк – hierarchyviewer.bat – он показывает, что на самом деле рисуется на экране телефона, кто из кого растет и все такое.. очень полезная программка.. не очень интуитивно понятная и удобная, но полезная:

Иерархия Вот например так устроены мои часы. Видно, что по идее тут таблица из одного столбца и 3-х рядов – строка с днями недели, текущий из которых выделен (сам текст белый, выделение черное – хорошо читается на любом фоне), потом строка с крупными датой и временем, и еще строка с месяцами (принцип выделения тот же). Дни недели и месяцы – текст (локализующийся автоматически в зависимости от региональных настроек телефона), дата и время - “пререндеренные” цифры, крупные, белые с темной обводкой (по той же причине – читаемость на любом фоне).

Собственно сначала я нарисовал картинку, поместив ее в рассчитанный выше прямоугольник 294*72 пикселя, подбирая шрифты и разглядывая так и сяк (в т.ч. и в телефоне, загрузив картинку и положив ее как обоину); смысл возни был в том, чтобы дата и время читались безнапряжно, опыт пользования в течение 2-х недель вроде показывает, что все ок:

Разметка

Выделение текущего дня недели или месяца на самом деле состоит из: маленького полукруга-картинки и текста с черным фоном; т.о. первая строка состоит на самом деле из 4-х “столбцов”: левый полукруг, прямоугольник с текстом, правый полукруг, прямоугольник с текстом; 3-я строка собственно строится так же

Все это похоже на таблицы вложенные: есть таблица с единственной ячейкой – весь виджет; в этой ячейке таблица с 3-я рядками и одним столбцом – вот эти самые 3 строчки; 2-я строчка (с датой и временем) состоит на самом деле еще из 2-х таблиц – для даты и для времени; сделано так (может и не совсем правильно) для того, чтобы дату выровнять по левому краю, а время – по правому. В ячейке для даты - (сюрприз-сюрприз) еще одна таблица, с 2-я столбцами, для каждой цифры свой, то же самое с ячейкой для времени – там таблица с 5-ю столбцами, цифры+разделитель “:”. Собственно если присмотреться, то на картинке с иерархией выше все это видно.

Так вот, к чему все это: с днями/месяцами понятно, картинка-полукруг, текст, картинка, текст; а дата/время формируются из отдельных цифр, значит надо подготовить эти цифры; собственно тут все очень просто – пишем “0”, обрезаем картинку вменяемо (скажем, оставляя по 1-пиксельному зазору слева-справа, по высоте она должна равняться 72 – 2 * высота_текстовых_областей, по моей картинке это 36 пикселей), сохраняем в файл скажем с именем d0.png (у моих цифр обводка полупрозрачная получилась, поэтому сохранять надо в png-24 с сохранением прозрачности), и так еще 9 раз с остальными цифрами, так же надо сохранить и картинки-полукруги, должно получится вагон и маленькая тележка картинок:

d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 l r

Собственно, тут дизайн и закончился; теперь начинается макетирование.

Вот те “таблицы”, про которые я только что написал – это конечно непрофессионально. Правильно говорить.. не знаю как, LAYOUTs, все, что видно на экране телефона, описывается этими лайаутами, и виджет тоже. Уже нужна ide, или эклипса, или idea (которая последняя, комьюнити эдишн, она знает про андроид, не требует денег и хоть и тормозная слегка, но имхо удобнее эклипсы)

Запускаем, создаем проект SimpleClockWidget, packageName “info.hamster.simpleclock.widget”, если предложат – не надо никаких активити со старта. Idea создает структуру папок при создании проекта, не помню, как эклипса – факт в том, что все эти картинки надо положить в папку res/drawable проекта.

Теперь создаем еще один ресурс, на этот раз – описание разметки нашего виджета: File-New-Android resource file, resource type – layout, resource name – main, появляется новый файл main.xml с заготовкой кода:

<?xml version="1.0" encoding="utf-8"?> 

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"> 

</LinearLayout>

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

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent">

</LinearLayout>

Кроме того, здесь задается ширина и высота “ячейки” – по размерам родителя. Внутри наши 3 ряда, 2 потоньше и один потолще, с цифрами, так что

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent">

    <LinearLayout
            android:layout_alignParentTop="true"
            android:orientation="horizontal"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            >

    </LinearLayout>

    <LinearLayout
            android:orientation="horizontal"
            android:layout_width="fill_parent"
            android:layout_height="36dip"
            android:layout_centerVertical="true"
            android:layout_centerInParent="true"
            >

    </LinearLayout>

    <LinearLayout
            android:layout_alignParentBottom="true"
            android:orientation="horizontal"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            >

    </LinearLayout>

</LinearLayout>

Они располагаются горизонтально, по ширине заполняют все доступное пространство, по высоте лайоут с датой имеет фиксированное значение 38 пикселей, а лайауты с днем/месяцем – по 18 пикселей. Содержимое лайаута с днями недели:

        <ImageView
                android:src="@drawable/l"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                />
        <TextView
                android:id="@+id/cur_day"
                android:text="empty_day"
                android:textSize="18sp"
                android:textColor="#FFFFFF"
                android:gravity="center_vertical|left"
                android:layout_gravity="center_vertical"
                android:singleLine="true"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/blackbg"
                />
        <ImageView
                android:src="@drawable/r"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                />
        <TextView
                android:text=""
                android:id="@+id/days"
                android:textSize="16sp"
                android:textColor="#989898"
                android:gravity="center_vertical|left"
                android:layout_gravity="center_vertical"
                android:singleLine="true"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                />

Здесь много всего: сначала идет картинка, которая берется из файла l.png из папки с ресурсами, потом идет текстовый контрол, на который я буду ссылаться из кода по id cur_day, здесь фигурирует еще один графический ресурс, о котором я забыл – изображение 1*1 пиксел, полностью залитое черным цветом (по другому я пока не догадался сделать фон теста), для отладки сразу задаю значение empty_day; дальше снова картинка и снова текст, теперь серый. Параметры всех тегов подбирались в бОльшей степени методом тыка, так что я даже не очень могу внятно обьяснить, почему так все, а не иначе :)

Точно так же выглядит лайаут с месяцами, другие id только естественно; лайоут с часами/датой формируется из отдельных цифр, для отладки пока достаточно одной:

        <ImageView
                android:src="@drawable/l"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                />
        <TextView
                android:id="@+id/cur_day"
                android:text="empty_day"
                android:textSize="18sp"
                android:textColor="#FFFFFF"
                android:gravity="center_vertical|left"
                android:layout_gravity="center_vertical"
                android:singleLine="true"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/blackbg"
                />
        <ImageView
                android:src="@drawable/r"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                />
        <TextView
                android:text=""
                android:id="@+id/days"
                android:textSize="16sp"
                android:textColor="#989898"
                android:gravity="center_vertical|left"
                android:layout_gravity="center_vertical"
                android:singleLine="true"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                />

Чтобы посмотреть, как это все выглядит "в живую", надо сделать следующее: создать активити с любым именем, указать, что это "стартовое" активити (идеа все это делает сама - создает файл, добавляет строки нужные в манифест), в onCreate добавить загрузку разметки активити:

        <ImageView
                android:src="@drawable/l"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                />
        <TextView
                android:id="@+id/cur_day"
                android:text="empty_day"
                android:textSize="18sp"
                android:textColor="#FFFFFF"
                android:gravity="center_vertical|left"
                android:layout_gravity="center_vertical"
                android:singleLine="true"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/blackbg"
                />
        <ImageView
                android:src="@drawable/r"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                />
        <TextView
                android:text=""
                android:id="@+id/days"
                android:textSize="16sp"
                android:textColor="#989898"
                android:gravity="center_vertical|left"
                android:layout_gravity="center_vertical"
                android:singleLine="true"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                />

Для отладки можно еще указать точно высоту основного лайоута (74 пикселя) и добавить файл для фона, потому что черное на черном не очень то смотрится - получим что-то вот такое:

т.е. как будто бы все вписывается. Дальше будет средняя часть с цифрами.

Исходники проекта в текущем состоянии - здесь. Достаточно распаоквать куда-то, в идеа создать новый проект из существующих исходников - дальше она все сама сделает

6 комментариев:

  1. Забавная статья, я как раз тем же и занимаюсь, вот только simple clock оказалось совсем не просто сделать... Часики заработали, а вот функцию запуска по тапу так и не удаётся до сих пор сделать.. Может у тебя уже есть какие мысли по этому поводу?

    ОтветитьУдалить
  2. дык смотрим дальше, part 0.1, 0.2 и так далее.. там это есть

    ОтветитьУдалить
  3. Так как на счёт кастомных контролов на виджет ?
    На сколько я понимаю, их всё же можно сделать. Вроде видел виджеты, которые изменяют свой вид по мере работы....

    ОтветитьУдалить
  4. кастомные контролы использовать нельзя - и точка.. доступ к контролу в виджете идет через remoteview, а тот понимает очень ограниченное количество контролов.

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

    по-моему как-то так..

    ОтветитьУдалить
  5. Вот нашёл. Подробненько описано и более менее понятно. Вдруг кому-то понадобится http://derevyanko.blogspot.com/2010/10/android-how-to.html

    ОтветитьУдалить
  6. ну так да - используя картинку и обновляя виджет можно получить подобие анимации..

    бегущую строку я погорячился - в принципе и ее сделать можно по такому же принципу (хе-хе, тавтология) - текст обновлять, он будет бежать..но как грузиться система будет - имхо только даром батарейку тратить :)

    ОтветитьУдалить