Идея проста. Делаем виджет. При нажатии на виджет появляется активити. Активити полностью прозрачна и целиком перекрывает рабочий стол. В том месте, где на рабочем столе расположен исходный виджет, на активити рисуем такой же "виджет". И уже этот "виджет" мы можем сделать анимированным.
Практика показала, что такой подход действительно работает. Но в нем много нюансов. О них и пойдет речь.
Тестовый проект 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.
Комментариев нет:
Отправить комментарий