Третья часть из цикла статей о разработке своего говорящего
питомца под Android. В
этой статье мы рассмотрим, как «оживить» нашего питомца. Рассмотрим, как
создать анимацию в Android-приложениях
и использовать SurfaceView для постоянной отрисовки анимационных кадров.
Что использовалось
Для реализации покадровой анимации будет использоваться
структура, содержащая в себе последовательной анимации из кадров, определенных
как серия объектов изображений, которая могут быть использованы в качестве фона
объекта. При вызове метода для воспроизведения, данный объект будет в течение
заданного времени поочередно отображать анимационные изображения.
Самым простым способом создать покадровую анимацию является
определение анимации в XML файле. Данный файл содержит в себе название
анимации, возможность повторять анимацию несколько раз, и сами кадры анимации.
Поскольку в приложении используется несколько различных
анимаций, то для структуризации кода и для управления всеми ресурсами анимации
будет использоваться отдельно разработанный класс.
Для отрисовки кадров в приложении мы будем использовать SurfaceView. SurfaceView обеспечивает
выделенную поверхность рисования внутри иерархии View. Вы можете управлять форматом этой
поверхности и, если хотите, ее размером; SurfaceView заботится о размещении surface в нужном месте на экране.
Доступ к surface осуществляется через интерфейс SurfaceHolder, который можно получить, вызвав
метод getHolder().
Анимационный кадр
Один кадр анимации будет состоять из ресурса изображения и
времени, которое это изображение должно рисоваться на экране.
/**
* Структура анимационного кадра
*/
public class Frame {
private int id; // id ресурса с изображением
private int time; // продолжительность кадра
public int getFrame() {
return id;
}
public void setFrame(int id) {
this.id = id;
}
public int getTime() {
return time;
}
public void setTime(int time) {
this.time = time;
}
public Frame()
{
this(0,0);
}
public Frame(int id, int time) {
this.id = id;
this.time = time;
}
}
Класс для Анимации
Анимация для конкретного движения персонажа (xml) будет храниться в классе
ArrayListAnimation. Этот класс содержит в себе список кадров для анимации,
общее время анимации и проверку на цикличность анимации.
/**
* Анимация
*/
public class ArrayListAnimation {
private ArrayList animation = new ArrayList(); // список кадров в анимации
private boolean isOneShot = false; // Цикличность анимации
private int time; // общее время анимации
public Frame getFrame(int index)
{
return this.animation.get(index);
}
public int getSize()
{
return this.animation.size();
}
public boolean isOneShot() {
return this.isOneShot;
}
public void setOneShot(boolean isOneShot) {
this.isOneShot = isOneShot;
}
public int getTime() {
return this.time;
}
public void setOneShot(int time) {
this.time = time;
}
public ArrayListAnimation(ArrayList animation, boolean isOneShot,int time)
{
this.animation = animation;
this.isOneShot = isOneShot;
this.time = time;
}
}
SurfaceView дла анимации
Теперь необходимо разработать View для
отрисовки нашей анимации. Нам необходимо наследоваться от класса SurfaceView и
имплементировать интерфейс SurfaceHolder.Callback.
Важным объектом это класса будет поток AnimationThread. Этот
поток будет постоянно рисовать текущий кадр анимации с помощью метода doDraw(). Вся логика
рисования анимации будет содержаться в методе run() потока.
/**
* Поток для отрисовки анимационного кадра
*/
public class AnimationThread extends Thread {
private SurfaceHolder mSurfaceHolder;
private boolean mRun = false; // остановка потока
private boolean mDrawing = false; // остановка отрисовки
private int mTaskIntervalInMillis = 100; // время одного кадра в потоке
Resources mRes; // ресурсы приложения
// размеры канвы для рисования
private int mCanvasHeight = 1;
private int mCanvasWidth = 1;
private Bitmap background; // фон
private int currentIndex = 0; // текущий кадр в анимации
private int currentTime = 0; // время кадра
private int currentId = R.anim.anim_spok; // текущая анимация
public AnimationThread(SurfaceHolder surfaceHolder) {
mSurfaceHolder = surfaceHolder;
mRes = context.getResources();
}
/**
* Отрисовка одного кадра
*/
private void doDraw(Canvas canvas, int id) {
canvas.drawBitmap(background, 0, 0, null);
Bitmap bitmap = null;
bitmap = BitmapFactory.decodeResource(mRes, id, null);
canvas.drawBitmap(bitmap, canvas.getWidth() / 2 - bitmap.getWidth() / 2, canvas.getHeight() / 2 - bitmap.getHeight() / 2, null);
}
/**
* Установка анимации
*/
public void setAnimation(int id) {
this.currentId = id;
this.currentIndex = 0;
this.currentTime = 0;
}
/**
* Установка фона
*/
public void setBackground(int id) {
this.background = Bitmap.createScaledBitmap(BitmapFactory
.decodeResource(mRes, id, null), mCanvasWidth, mCanvasHeight,
true);
}
/**
* Выполнение потока с рисованием
*/
public void run() {
while (mRun) {
if (mDrawing) {
Canvas c = null;
try {
// здесь происходит зацикливание анимации
if (currentIndex == hmAnimations.get(currentId).getSize())
// в случае, когда персонаж слушает, необходимо по окончанию первой анимации воспроизводить последний кадр
if (currentId==R.anim.anim_listen)
{
currentIndex = hmAnimations.get(currentId).getSize()-1;
}
else if (!hmAnimations.get(currentId).isOneShot())
currentIndex = 0;
// получаем кадр анимации
Frame frame = null;
try
{
frame = hmAnimations.get(currentId).getFrame(currentIndex);
}
catch (Exception e) {
currentIndex=0;
frame = hmAnimations.get(currentId).getFrame(currentIndex);
}
// если прошло времени больше, чем продолжительность кадра, то переходим к следующему кадру
if (currentTime >= frame.getTime()) {
frame = hmAnimations.get(currentId).getFrame(currentIndex);
currentTime = 0;
currentIndex++;
}
currentTime += mTaskIntervalInMillis;
// рисуем кадр
if (currentTime==mTaskIntervalInMillis)
{
c = mSurfaceHolder.lockCanvas(null);
int id = frame.getFrame();
doDraw(c, id);
}
else
{
try
{
sleep(70);
}catch (Exception e) {
}
}
} finally {
if (c != null) {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
}
public void setSurfaceSize(int width, int height) {
synchronized (mSurfaceHolder) {
background = Bitmap.createScaledBitmap(background, width,
height, true);
mCanvasWidth = width;
mCanvasHeight = height;
}
}
public void setRunning(boolean isRun) {
mRun = isRun;
}
public void setDrawing(boolean isDraw) {
mDrawing = isDraw;
}
}
Кроме этого потока наш SurfaceView будет содержать хеш-таблицу
со всей анимацией для персонажа.
/**
* View для отрисовки анимации
*/
public class AnimationView extends SurfaceView implements
SurfaceHolder.Callback {
public static final String TAG = AnimationView.class.getSimpleName();
private AnimationThread thread; // Поток для рисования
private HashMap hmAnimations = new HashMap(); // список анимаций
private Context context;
public AnimationView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
}
public void setAnimations(HashMap hmAnimations)
{
this.hmAnimations = hmAnimations;
}
/**
* Создание потока для отрисовки
*/
public void createThread() {
SurfaceHolder holder = getHolder();
holder.addCallback(this);
thread = new AnimationThread(holder);
}
/**
* Изменение размеров View
*/
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
thread.setSurfaceSize(width, height);
thread.setDrawing(true);
thread.setRunning(true);
}
/**
* Создание View
*/
public void surfaceCreated(SurfaceHolder holder) {
thread.start();
}
/**
* Уничтожение View
*/
public void surfaceDestroyed(SurfaceHolder holder) {
boolean retry = true;
thread.setRunning(false);
thread.setDrawing(false);
while (retry) {
try {
thread.join();
retry = false;
} catch (Exception e) {
}
}
}
public AnimationThread getThread() {
return this.thread;
}
}
Ресурсы для анимации
Теперь нам необходимо нарисовать анимацию для персонажа и
создать xml-файлы для
этой анимации.
Пример анимационных изображений можно скачать отсюда.
Для каждой анимации необходимо создать свой xml-файл и поместить их в
папку «res/anim».
Например, для анимации спокойного состояния этот файл будет
выглядеть так:
<?xml
version="1.0" encoding="utf-8"?>
<animation-list
xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:duration="550"
android:drawable="@drawable/spok_1" />
<item
android:duration="250"
android:drawable="@drawable/spok_2" />
<item
android:duration="250"
android:drawable="@drawable/spok_3" />
<item
android:duration="250"
android:drawable="@drawable/spok_2" />
<item
android:duration="250"
android:drawable="@drawable/spok_1" />
</animation-list>
Все примеры xml-ресурсов
можно скачать отсюда.
Теперь нам необходимо изменить 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"
android:orientation="vertical"
>
<org.snowpard.projects.one.animations.AnimationView
android:id="@+id/animation_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</LinearLayout>
Изменения в MainActivity
Последним шагом к работающему приложению будет доработка
нашего Activity. Нам
необходимо добавить объекты для анимации, получить анимацию из ресурсов и
изменить наш Handler.
Для получения анимации из файлов, необходимо разработать
парсер xml-файлов. Как
пример, можно использовать следующую реализацию:
/**
* Парсер xml файлов с анимацией
*/
private ArrayListAnimation parceXml(int id) {
boolean isOneShot = false;
ArrayList array = new ArrayList();
XmlPullParser xpp = getResources().getXml(id);
int timeSummary = 0;
try {
int eventType = xpp.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG) {
if (xpp.getName().equals("animation-list")) {
isOneShot = Boolean.parseBoolean(xpp
.getAttributeValue(0));
} else if (xpp.getName().equals("item")) {
int idResource = Integer.parseInt(xpp
.getAttributeValue(1).substring(1));
int time = Integer.parseInt(xpp.getAttributeValue(0));
timeSummary += time;
array.add(new Frame(idResource, time));
}
}
eventType = xpp.next();
}
} catch (Exception e) {
e.printStackTrace();
}
return new ArrayListAnimation(array, isOneShot, timeSummary);
}
Теперь внесем все нужные изменения в наш MainActivity
public class MainActivity extends Activity {
public static final String TAG = MainActivity.class.getSimpleName();
private PowerManager.WakeLock wl; // для вкл/откл экрана
private Record record = null; // для записи речи
private Playback playback = null; // для воспроизведения речи
private AnimationThread thread; // поток для отрисовки анимации
// список анимаций
private HashMap hm = new HashMap();
private boolean isPause = false; // отслеживать, когда приложение уйдет в паузу
// обработка сообщений от объектов
private Handler handler = new Handler() {
public void handleMessage(Message msg) {
DebugLog.i(TAG, "msg.what = " + msg.what);
if (isPause)
return;
switch (msg.what) {
case Constants.MSG_RECORD: // запись речи
if ((record != null) && (!record.isRec()))
record.startRecord();
thread.setAnimation(R.anim.anim_spok); // персонаж спокоен
break;
case Constants.MSG_PLAYBACK: // воспроизведение речи
thread.setAnimation(R.anim.anim_speak); // персонаж говорит
playback = new Playback(handler);
int size = msg.getData().getInt(Constants.DATA_SIZE);
byte[] array = msg.getData().getByteArray(Constants.DATA_ARRAY);
playback.setData(size, array);
playback.start();
break;
case Constants.MSG_LISTEN: // персонаж слушает
thread.setAnimation(R.anim.anim_listen);
break;
}
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
DebugLog.i(TAG, "onCreate()");
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
this.wl = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK
| PowerManager.ACQUIRE_CAUSES_WAKEUP, "TAG");
loadAnimation();
AnimationView view = (AnimationView) findViewById(R.id.animation_view);
view.setAnimations(this.hm);
view.createThread();
this.thread = view.getThread();
this.thread.setBackground(R.drawable.bg);
}
/**
* Загрузка анимации из ресурсов
*/
private void loadAnimation()
{
hm.put(R.anim.anim_listen, parceXml(R.anim.anim_listen));
hm.put(R.anim.anim_speak, parceXml(R.anim.anim_speak));
hm.put(R.anim.anim_spok, parceXml(R.anim.anim_spok));
}
protected void onPause() {
super.onPause();
DebugLog.i(TAG, "onPause()");
thread.setRunning(false);
this.wl.release();
isPause = true;
if ((this.playback != null) && (this.playback.isPlay())) {
this.playback.stopPlay();
}
// останавливаем запись
if (this.record != null)
{
this.record.stopRecord();
this.record.close();
}
}
protected void onResume(){
super.onResume();
DebugLog.i(TAG, "onResume()");
this.thread.setRunning(true);
this.wl.acquire();
isPause = false;
// запускаем поток для записи речи
this.record = new Record(handler);
this.record.start();
handler.sendEmptyMessage(Constants.MSG_RECORD);
}
/**
* Парсер xml файлов с анимацией
*/
private ArrayListAnimation parceXml(int id) {
boolean isOneShot = false;
ArrayList array = new ArrayList();
XmlPullParser xpp = getResources().getXml(id);
int timeSummary = 0;
try {
int eventType = xpp.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG) {
if (xpp.getName().equals("animation-list")) {
isOneShot = Boolean.parseBoolean(xpp
.getAttributeValue(0));
} else if (xpp.getName().equals("item")) {
int idResource = Integer.parseInt(xpp
.getAttributeValue(1).substring(1));
int time = Integer.parseInt(xpp.getAttributeValue(0));
timeSummary += time;
array.add(new Frame(idResource, time));
}
}
eventType = xpp.next();
}
} catch (Exception e) {
e.printStackTrace();
}
return new ArrayListAnimation(array, isOneShot, timeSummary);
}
}
Важные замечания:
- Предложенный пример разработки анимации может использоваться в небольших приложениях, где не очень много ресурсов и они не очень большие. В игре «Тарабаня» мы отказались от такого метода и переделали все под игровой движок, использующий OpenGL.
- Для корректного отображения изображений во View необходимо разработать несколько наборов картинок для разных разрешений.

Комментариев нет:
Отправить комментарий