четверг, 20 сентября 2012 г.

Talking Pets: Разработка питомца под Android – часть 3

Talking Pets: Разработка питомца под Android – часть 3
Третья часть из цикла статей о разработке своего говорящего питомца под 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);
 }

}
Важные замечания:
  1. Предложенный пример разработки анимации может использоваться в небольших приложениях, где не очень много ресурсов и они не очень большие. В игре «Тарабаня» мы отказались от такого метода и переделали все под игровой движок, использующий OpenGL.
  2. Для корректного отображения изображений во View необходимо разработать несколько наборов картинок для разных разрешений.
Talking Pets: Разработка питомца под Android – Результат

Ссылки

  • Исходные коды данного проекта можно скачать отсюда: zip
  • Пример говорящего питомца под AndroidТарабаня
  • Talking Pets: Разработка питомца под Android – часть 1часть 2

Комментариев нет:

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