Третья часть из цикла статей о разработке своего говорящего
питомца под 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 HashMaphmAnimations = 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 HashMaphm = 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 необходимо разработать несколько наборов картинок для разных разрешений.
Комментариев нет:
Отправить комментарий