Анимированный виджет для Android


Не секрет, что виджетов для платформы Android с динамичной анимацией не так много. Основная причина проста - так как виджет выполняется в потоке менеджера главного экрана то особо нагруженный виджет тормозит работу всего остального. Кроме того, из-за архитектуры работы с виджетом, доступ напрямую получить к нему нельзя, а попытка прокачивать большой поток данных приводит к ошибкам пересылки. Но не все так плохо, при небольшой частоте кадров и малом размере анимации все получается достаточно неплохо. Реализации анимированного виджета и посвящена данная статья. Реализация хорошо работает на устройствах с системой версии 2.1 и выше, все примеры можно скачатьздесь.

Для начала соберем простой виджет отображающий статичную картинку, он не только послужит базой для анимированного виджета, но и станет неплохим шаблоном. Откроем Eclipse и создадим новый проект с именем WidgetAnimate, имя пакета com.shadowsshot.widget_animate, используем API седьмой версии, нам нужен только виджет, поэтому галочку Create Activity снимаем. Нажимаем на кнопку Finish и получаем пустой проект.

Теперь разберемся с ресурсами, изначально в проекте Android создается три папки с графическими ресурсами для разного разрешения экрана с именами drawable-hdpi drawable-ldpi drawable-mdpi, для нас эта возможность излишня, поэтому создадим в папке res папку drawable, куда положим наше изображение. В качестве картинки я взял простое красное сердце.



Добавим его в папку resdrawable с именем heart0.png. Теперь зайдем в папку layout и создадим файл разметки виджета widget_layout.xml. Разметка включает в себя только один компонент ImageView растянутый на всю площадь виджета, рамку мы пока рисовать не будем. Уже существующий файл разметки main.xml можно удалить - он нам не понадобится.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <ImageView
        android:id="@ id/WidgetImage"
        android:src="/@drawable/heart0"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_gravity="center">
        </ImageView>
</LinearLayout>

Далее создадим файл описывающий виджет, для этого создадим в ресурсах папку xml и в ней создадим файл разметки виджета. Проще всего создавать разметку, воспользовавшись мастером, для этого щелкаем правой кнопкой по папке xml и выбираем из меню New-Android XML File



Закончив создание шаблона, задаем параметры виджета, для этого открыв файл widget_info.xml в режиме структуры заполняем соответствующие поля. В Min width и Min height задается размер виджета, в Update period millis задается скорость обновления виджета. Далее необходимо указать разметку, задав в поле Initial layout ссылку на предварительно нами созданную разметку из ресурса widget_layout. Параметр Configure позволяет задать форму настроек виджета - оставим его пустым.



Пришла пора добавить исходный код обработчика WidgetProvider, создадим класс AniWidgetProvider, наследуемый от AppWidgetProvider, внутри которого реализуем необходимые методы. При создании или изменении виджета вызывается метод onUpdate, в котором отдаются команды по изменению содержимого виджета. Необходимо учесть, что виджетов на экране может быть несколько, поэтому на вход методу передается массив идентификаторов appWidgetIds, каждый элемент которого содержит уникальный идентификатор виджета. Для доступа к содержимому разметки используется класс RemoteViews, инициализировав который, мы получаем набор методов передачи данных. В примере мы перебираем все экранные виджеты и устанавливаем там одно и то же изображение, а значит не делам ничего как и положено шаблону.

Для проверки виджета на устройстве осталось добавить информацию о его существовании в AndroidManifest.xml, проще всего сделать это добавив в его текст следующие строки в раздел application

    <receiver android:name="AniWidgetProvider" android:label="@string/app_name">
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>
        <meta-data android:name="android.appwidget.provider"
            android:resource="@xml/widget_info" />
        </receiver>

Этим мы информируем систему о существовании нового виджета и задаем сервис, выполняющий его обновление. Осталось собрать проект и запустить его на устройстве. После этого можно открыть список виджетов и добавить один из них на экран. Получится достаточно симпатичное, хотя и немного размытое сердечко абсолютно ничего не делающее.  Итак, мы получили шаблон статичного виджета. Исходный текст проекта лежит в папке StaticTemplateархива.

Переходим к анимации. До версии 3.0 единственный способом создать анимацию - это создать сервис, принудительно обновляющий виджет с определенной скоростью. Из-за наличия широкой базы устройств совместимых с API 2.1 в примере будет применен именно этот способ. Итак, для начала нам понадобится сервис с заданной периодичностью вызывающий обновление виджета.

Создадим класс UpdateWidget унаследованный от Service, переопределим в нем методы onStart, onDestroy и onBind для реализации взаимодействия с виджетом.  Нам необходимо создать периодическую смену кадров, для этого реализуем в нем  класс обработчика события от таймера UpdateTimeTask, внутри которого переопределим метод run, из которого будем вызывать обработчик обновления виджета updateWidget(). Для того, чтобы инициализировать таймер, в начале метода  onStart запустим его выполнение с периодом, заданным отношением 1сек/FPS_MAX - этим мы привяжем желаемую частоту кадров к константе FPS_MAX.

         if(timer == null){
            timer = new Timer();
            timer.schedule(new UpdateTimeTask(), 0, 1000 / FPS_MAX);
        }   

Так как сервис является синглтоном, то сколько бы мы ни запускали виджетов, каждый из них будет обрабатываться единственным сервисом, это позволяет системе не тратить лишние ресурсы, но приводит к проблеме потери контекста при добавлении еще одного экземпляра виджета. Это значит, что анимирован будет только последний виджет. Нас это не устраивает, поэтому в класс сервиса мы добавим список идентификаторов widgets, к которому будем добавлять новый элемент после добавления каждого виджета.

    static List<int[]> widgets = new ArrayList<int[]>();

Теперь добавим в ресурсы остальные кадры анимации, чтобы нам было что рисовать, в папку drawable я добавил изображения heart1.png - heart4.png  содержащие анимацию сердца. Теперь его можно анимировать в методе updateWidget(). В отличии от первого примера, здесь добавляется вложенный цикл по списку widgets, перебирающий существующие экранные виджеты, добавлена переменная current_frame, содержащая номер текущего кадра и добавлен выбор кадра из ресурсов по его номеру.

    void updateWidget(){
        // Обновляем номер текущего кадра
        current_frame ;
        if(current_frame > FRAME_MAX)current_frame = 0;
        // Создаем ссылку на разметку виджета           
        RemoteViews remoteViews = new RemoteViews(getPackageName(),    R.layout.widget_layout);               
        for (int j = 0; j < widgets.size(); j ){
            int[] current = widgets.get(j);               
            // Определяем количество виджетов на экране
            int widget_max = current.length;
            // Обновляем содержимое каждого виджета
            for (int i=0; i < widget_max; i ) {               
                // Задаем виджету изображение из ресурсов
                switch(current_frame){
                    case(0):{remoteViews.setImageViewResource(R.id.WidgetImage, R.drawable.heart0);}break;
                    case(1):{remoteViews.setImageViewResource(R.id.WidgetImage, R.drawable.heart1);}break;
                    case(2):{remoteViews.setImageViewResource(R.id.WidgetImage, R.drawable.heart2);}break;
                    case(3):{remoteViews.setImageViewResource(R.id.WidgetImage, R.drawable.heart3);}break;
                    case(4):{remoteViews.setImageViewResource(R.id.WidgetImage, R.drawable.heart4);}break;                   
                }                           
                // Определяем идентификатор виджета
                int widgetId = current[i];
                // Обновляем изображение виджета
                appWidgetManager.updateAppWidget(widgetId, remoteViews);                 
            }
        }       
    }

Для упрощения кадры загружаются из ресурсов по их идентификатору. Число кадров анимации задается в константе FRAME_MAX. Если загружать все изображения в виде битмапов и держать в буфере, быстрее выводить их все равно не получается, для анимаций в 5-30 кадров работа с ресурсами это самый простой путь. Для задач, где изображение генерируется динамически ресурсы не нужны, но такой пример бы значительно усложнил код. Сервис написан,  осталось добавить информацию о нем в манифест

    <service
        android:label="@string/app_name" android:name="UpdateWidget">       
    </service>   

Так как всю обработку обновления виджета мы перенесли в сервис, код класса AniWidgetProvider значительно упростится.  Если виджет добавляется на экран нам необходимо стартовать сервис вызвав context.startService(intent), а при удалении передать идентификатор виджета в метод onDestroy(). Правда здесь есть одна маленькая проблема, если для удаления виджета вызывать сервис методом stopService - то прочитать идентификатор виджета из Intent не получится, так как метод onDestroy() не передает в себя параметры. Мы выйдем из этой ситуации в лоб, все управление сервисом сосредоточив в методе onStart(). В классе AniWidgetProvider добавим константы-идентификаторы параметров для передачи в сервис,  управляющий параметр будет строковый и содержит всего две команды - виджет добавлен и виджет удален.

    public static final String SERVICE_PARAM = "parameters";
    public static final String SERVICE_ADD = "add";
    public static final String SERVICE_DEL = "del";

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

        // Если виджет удален       
        if(operation.equals(AniWidgetProvider.SERVICE_DEL)){
            // Удаляем из списка заданный идентификатор               
            widgets.remove(appWidgetIds);
            // Виджетов нет - останавливаем сервис
            if(widgets.size() == 0){
                timer.cancel();           
                stopSelf();
            }               
        }

После внесения всех изменений мы получим результат - анимированный виджет. Взять готовый пример можно в папке AnimateTemplate архива. Количество виджетов на экране не ограничено, но их большое количество может вызвать падение скорости работы интерфейса, а то и его зависание, поэтому не стоит увлекаться запуском множества экземпляров виджета ;( В реальных приложениях можно ввести ограничение на количество виджетов или допустить запуск только одного экземпляра. Два анимированных сердца выглядят примерно так:



В качестве резюме, стоит еще раз упомянуть об особенностях и недостатках анимации выполненной подобным образом. Не стоит гнаться за большой частотой кадров, для большинства медленных устройств на перерисовку изображения на одном виджете уходит около 150 мс, что дает предельную скорость 6 кадров в секунду. Для того чтобы виджет не пропускал кадры во время активности пользователя этот параметр необходимо уменьшить вдвое. В примере частота кадров равна трем. Этого вполне достаточно для динамичной индикации событий или рисовании графика загруженности, но будет мало для видео. Поэтому основное назначение такой анимации - анимированные индикаторы событий, всевозможные переключатели и семафоры..

Второе ограничение - невозможность быстро перебросить заданный объем данных из ресурса в виджет, эта величина составляет около 40 кб сырых данных. Превышение этого лимита приводит к появлениям ошибок синхронизации, кадры не будут успевать выводиться и реальная скорость вывода сильно упадет. Максимальный ориентировочный размер зависит от производительности устройства и упирается в картинку примерно 128х128 пикселей для медленных устройств. Третим ограничением на использование подобных виджетов может стать использование батареи. При большой частоте обновления пользователи могут начать жаловаться на потерю 1-2 часов работы устройства ;).

Необходимо отметить, что в версии 3.0 Android API возможности виджетов были значительно расширены. Появилась возможность создавать виджет с фреймовой анимацией  на базе класса ViewFlipper, без необходимости создания сервиса и меньшей потерей производительности. Для тех кто пишет для новых устройств это будет гораздо удобнее и проще.

АвторShadowsshot



Наши соцсети

Подписаться Facebook Подписаться Вконтакте Подписаться Twitter Подписаться Google Подписаться Telegram

Популярное

Ссылки

Новости [1] [2] [3]... Android/ iOS/ J2ME[1] [2] [3]) Android / Архив

Рейтинг@Mail.ru Яндекс.Метрика
MobiLab.ru © 2005-2018
При использовании материалов сайта ссылка на www.mobilab.ru обязательна