Идея проста. Делаем виджет. При нажатии на виджет появляется активити. Активити полностью прозрачна и целиком перекрывает рабочий стол. В том месте, где на рабочем столе расположен исходный виджет, на активити рисуем такой же "виджет". И уже этот "виджет" мы можем сделать анимированным.
Практика показала, что такой подход действительно работает. Но в нем много нюансов. О них и пойдет речь.
Тестовый проект AniClick
Рассмотрим простейший пример такого анимированного виджета. Для простоты вновь используем ту же анимацию, что в прошлый раз - крутящееся колесо. Анимация колеса состоит из 10 кадров. Статический виджет показывает колесо - один (по умолчанию, самый первый) кадр анимации. При нажатии на виджет колесо на экране начинает крутиться. При повторном нажатии колесо останавливается. Теперь виджет показывает другой кадр анимации - тот, на котором произошел клик. Если снова кликнуть виджет, колесо вновь начнет крутиться. И т.д.
Проект называется AniClick. В проекте 5 основных исходных файлов:
AniWidgetProvider.java
- реализация провайдера виджета. Содержит код для вызова активити и обновления виджета после завершения работы активити.DataStorage.java
- отвечает за сохранение и загрузку данных (номер кадра) каждого экземпляра виджета вSharedPreferences
.MainActivity.java
- прозрачная активити, которая перекрывает экран при нажатии на виджет. Внутренний классScreenView
обеспечивает немедленную перерисовку экрана и позволяет достичь приемлемых значений FPS.ScreenManager.java
- эмулирует анимацию (перебирает кадр за кадром) и отрисовывет текущий кадр на переданныйcanvas
.Utils.java
- вспомогательный класс, позволяющий конвертироватьRect
в строку и обратно.
Теперь переходим к особенностям реализации.
Вызов активити при нажатии на виджет
Первая проблема - получение координат исходного виджета. Нам они необходимы, чтобы мы могли нарисовать на активити картинку точно поверх исходного виджета.
//class AniWidgetProvider ... public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); if (action.equals(ACTION_WIDGET_CONTROL)) { Uri uri = intent.getData(); Rect widget_pos = intent.getSourceBounds(); } }Все просто. Но код работает только под API 2.1. В более ранних версиях API функции getSourceBounds нет и координаты кликнутного виджета узнать невозможно (?).
Вызов activity из виджета выполняется следующим образом:
private void make_widget_action(Context context, int appWidgetId, Rect widgetPos) { Intent start = new Intent(context, MainActivity.class); start.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); start.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); start.putExtra(PARAM_WIDGET_ID, appWidgetId); start.putExtra(PARAM_WIDGET_POSITION, Utils.RectToStr(widgetPos)); context.startActivity(start); }
Прозрачная activity
Наша активити должна быть прозрачной. Как делать прозрачные activity известно - нужно использовать тему
@android:style/Theme.Translucent
. Беда в том, что в этой теме свойство стиля android:windowNoTitle
выключено. Поэтому, при нажатии на виджет, сверху будет появляться заголовок activity. Отключить заголовок можно, создав собственную тему:<!-- res/values/TranslucentNoTitleBarTheme.xml --> <?xml version="1.0" encoding="utf-8"?> <resources> <style name="TranslucentNoTitleBarTheme" parent="@android:style/Theme.Translucent"> <item name="android:windowNoTitle">false</item> </style> </resources>и применив ее в манефесте к нашей activity:
<activity android:name=".MainActivity" android:theme="@style/TranslucentNoTitleBarTheme" android:label="@string/app_name"> </activity>
Обновление виджета после завершения активити
Ткнули на крутящееся колесо, колесо остановилось, activity закрывается. Запомнили кадр, который показывался в момент клика. Теперь нужно сообщить виджету о том, что его картинку следует обновить. Это делается следующим образом. В activity даем команду виджиту:
public class MainActivity extends Activity { .... @Override public boolean onTouchEvent(MotionEvent event) { //any click on the animation stops it if (event.getAction() == MotionEvent.ACTION_DOWN) { ... Intent active = new Intent(this, AniWidgetProvider.class); active.setAction(AniWidgetProvider.ACTION_UPDATE_WIDGET_CONTEXT); Uri uri = Uri.parse(AniWidgetProvider.URI_SCHEME + "://widget#"); uri = uri.buildUpon() .appendQueryParameter("widget_id", String.valueOf(m_WidgetId)) .build(); active.setData(uri); this.sendBroadcast(active); //сообщаем виджету об обновлении ... } return super.onTouchEvent(event); } ... }В провайдере виджета принимаем и выполняем эту команду:
//class AniWidgetProvider public static final String ACTION_UPDATE_WIDGET_CONTEXT = "com.rammus.aniclick.UPDATE_WIDGET_CONTEXT"; ... @Override public void onReceive(Context context, Intent intent) { ... if (action.equals(ACTION_UPDATE_WIDGET_CONTEXT)) { Uri uri = intent.getData(); int widget_id = Integer.parseInt(uri.getQueryParameter("widget_id")); if (widget_id != AppWidgetManager.INVALID_APPWIDGET_ID) { //перерисовывем картинку WidgetInstanceManager wim = get_instance(widget_id); wim.ReinitializeWidgetBitmap(context); //даем команду виджету обновить картинку RemoteViews remote_views = new RemoteViews(context.getPackageName() , R.layout.widget); update_widget_view(AppWidgetManager.getInstance(context) , widget_id , remote_views); } else super.onReceive(context, intent); } } private void update_widget_view(AppWidgetManager appWidgetManager , int appWidgetId, RemoteViews remoteViews) { WidgetInstanceManager wim = get_instance(appWidgetId); remoteViews.setImageViewBitmap(R.id.widget_image_view, wim.GetBitmap() ); appWidgetManager.updateAppWidget(appWidgetId, remoteViews); }
View для рисования на activity.
Чтобы можно было рисовать на активити, нам нужен View. Изначально я пошел следующим путем:
public class ActivityMain extends Activity { private ScreenView m_MainView; ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); m_MainView = new ScreenView(this); setContentView(m_MainView); ... } private class ScreenView extends View { public ScreenView(Context context){ super(context); } //рисуем текущий фрейм анимации на активити @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); RedrawScreen(); } } //ScreenView private class AnimationThread extends TimerTask { @Override public void run() { //запускается 25 раз в секунду //перейти к следующему кадру анимации MakeAnimationStep(); //перерисовать активити ActivityMain.this.m_MainView.postInvalidate(); } } }У нас два вспомагательных класса.
ScreenView
- это View, на котором рисуется очередной фрейм анимации. AnimationThread
- поток, который 25 раз в секунду увеличивает номер текущего кадра анимации и дает команду перерисовать активити. Все работает, но анимация рисуется рывками - часть кадров пропускается.Причина пропусков очевидна. Команда invalidate не обновляет экран мгновенно - она лишь помещает команду redraw в очередь. У нас команды redraw генерируются с заданным FPS - 25 раз в секунду. А слишком частые redraw приводят к тому, что OS объединяет несколько redraw в одну команду (для оптимизации). Для покадрового вывода анимации такое поведение не подходит.
SurfaceView вместо View
При разработке игр вместо View используют SurfaceView. Основное отличие - в SurfaceView можно рисовать из фоновых потоков, что идеально подходит в нашем случае. Вот как выглядит код с SurfaceView:
class ScreenView extends SurfaceView implements SurfaceHolder.Callback { private final String TAG_LOG = "com.rammus.flw.ScreenView"; SurfaceHolder m_SurfaceHolder; public ScreenView(Context context){ super(context); m_SurfaceHolder = this.getHolder(); //ставим формат TRANSLUCENT чтобы получить //прозрачную activity m_SurfaceHolder.setFormat(PixelFormat.TRANSLUCENT ); m_SurfaceHolder.addCallback(this); } public void surfaceCreated(SurfaceHolder holder) { //необходимо реализовать интерфейс SurfaceHolder.Callback //и вызвать Repaint() при создании поверхности. //Иначе мы получим моргание экрана в момент запуска activity. Repaint(); } public void surfaceDestroyed(SurfaceHolder holder) { } public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } private void paint(Canvas canvas) { RedrawScreen(); } public void Repaint() { //доступ к lockCanvas должен быть синхронизирован //чтобы функцию Repaint можно было вызывать из //разных потоков synchronized (m_SurfaceHolder) { Canvas c = m_SurfaceHolder.lockCanvas(); if (c != null) { try { super.draw(c); paint(c); } finally { m_SurfaceHolder.unlockCanvasAndPost(c); } } else { Log.e(TAG_LOG, "c is null!"); } } } //Repaint } //ScreenView public class ActivityMain extends Activity { private ScreenView m_MainView; ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); m_MainView = new ScreenView(this); setContentView(m_MainView); ... } private class AnimationThread extends TimerTask { @Override public void run() { m_ScreenManager.MakeAnimationStep(); //перерисовка экрана вызывается из ФОНОВОГО потока. m_MainView.Repaint(); } } }Вместо
invalidate
мы вызываем функцию Repaint
, которая рисует картинку на SurfaceView
.Здесь хочу обратить внимание на несколько важных моментов.
- Для того, чтобы наша поверхность была прозрачной, необходимо вызвать
setFormat(PixelFormat.TRANSLUCENT)
По умолчанию установлен форматPixelFormat.OPAQUE
, это не то. - Перерисовка экрана вызывается из фонового потока
AnimationThread
. Чтобы перерисовка работа корректно, вызовы функцийlockCanvas
иunlockCanvasAndPost
должны быть синхронизированы. Отсюда synchronize в коде. - ScnreenView должен реализовывать интерфейс
SurfaceHolder.Callback
и обязательно вызывать функцию отрисовки при создании поверхности в функцииsurfaceCreated
. Если этого не сделать, то при запуске activity экран на короткое мгновение будет становиться черным. - Функция
lockCanvas
корректно отрабатывает только после полного создания activity. Если вызвать функцию Repaint вActivityMain.OnCreate
, то мыlockCanvas
вернет null и мы получим ошибку "c is null!" в логе. Таким образом МГНОВЕННО запустить анимацию не получится - задержка равная времени создания activity неизбежна.
Выводы
Подход с псевдоанимированным виджетом оказался вполне рабочим. Конечно, есть определенные проблемы - под активити видно исходный виджет, запуск анимации происходит не мгновенно, а лишь после создания активити, анимация в виджете работает только по клику на виджет и т.д. Однако, существует ряд приложений, в которых такой анимации более чем достаточно. Для них главное, чтобы значение FPS получалось пристойным - а этого можно добиться.
Одно из таких приложений мы сейчас разрабатываем. Надеюсь, уже скоро оно появится на маркете и тогда данный подход пройдет проверку боем. Там посмотрим.
Исходные коды проекта
Просмотреть исходные коды, скачать исходные коды, скачать apk. Short description in English is here.
Комментариев нет:
Отправить комментарий