Thursday, September 20, 2012

Talking Pets: Development Pet for Android - Part 3


Talking Pets: Development Pet for Android - Part 3
The third part of a series of articles on the development of a talking pet for Android. In this article we will discuss how to "revive" our pet. Look at how to create an animation in Android-applications and use SurfaceView for constant rendering animation frames. 


What was used

To implement frame animation will be used a structure, which contains the animation sequence of frames are defined as a series of images of objects that can be used as a background object. When you call to play, the object will be within the specified time serially display animations.
The easiest way to create a frame animation is to define the animation in XML file. This file contains the name of the animation, the ability to repeat the animation a few times, and animation frames.
Since the application uses several different animations, for structuring the code and to control all the resources of animation will be used a designed class.
To draw a frame in the application, we will use SurfaceView. SurfaceView provides a dedicated drawing surface within the hierarchy View. You can control the format of the surface and, if you like, its size; SurfaceView takes care of placing the surface in the right place on the screen.
Access to the surface through the interface SurfaceHolder, which can be obtained by calling getHolder().


Animation Frame

One frame of the animation will consist of an image resource and time that this image should be drawn on the screen.
/**
 * Structure of the animation frame
 */
public class Frame {

 private int id; // id resource with image
 private int time; // frame duration
 
 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;
 }
 
}


Animation class

Animation for a specific character's movement (xml) will be stored in a class ArrayListAnimation. This class contains a list of frames for the animation, total time the animation and checking the animation loop.
/**
 * Animation
 */
public class ArrayListAnimation {
 
 private ArrayList animation = new ArrayList(); // list of frames in the animation
 private boolean isOneShot = false; // animation will loop
 private int time; // total time animation
 
 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 for animation

Now you need to develop a View to render our animation. We need inherit from SurfaceView and implement the interface SurfaceHolder.Callback.
An important object of this class will flow AnimationThread. This thread will constantly draw current animation frame by using the doDraw(). The whole logic of drawing animation will be contained in the method run().
 /**
  * Thread for rendering animation frames
  */
 public class AnimationThread extends Thread {
  
  private SurfaceHolder mSurfaceHolder;
  
  private boolean mRun = false; // stopping the thread
  private boolean mDrawing = false; // stopping the render
  
  private int mTaskIntervalInMillis = 100; // time of one frame in the thread
  Resources mRes; // application resources
  
  // size canvas for drawing
  private int mCanvasHeight = 1; 
  private int mCanvasWidth = 1;
  
  private Bitmap background; // background
  
  private int currentIndex = 0; // current frame in the animation
  private int currentTime = 0; // time frame
  private int currentId = R.anim.anim_spok; // current animation

  public AnimationThread(SurfaceHolder surfaceHolder) {
   mSurfaceHolder = surfaceHolder;
   mRes = context.getResources();
  }

  /**
   * Rendering one frame
   */
  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);
  }

  /**
   * Setting animation
   */
  public void setAnimation(int id) {
   this.currentId = id;
   this.currentIndex = 0;
   this.currentTime = 0;
  }

  /**
   * Setting background
   */
  public void setBackground(int id) {
    this.background = Bitmap.createScaledBitmap(BitmapFactory
      .decodeResource(mRes, id, null), mCanvasWidth, mCanvasHeight,
      true);
  }

  /**
   * Running thread with drawing
   */
  public void run() {
   while (mRun) {
    if (mDrawing) {
     Canvas c = null;
     try {
      // Here is a loop animation
      if (currentIndex == hmAnimations.get(currentId).getSize())
       // when the character is listening to at the end of the first animation to play the last frame
       if (currentId==R.anim.anim_listen)
       {
        currentIndex = hmAnimations.get(currentId).getSize()-1;
       }
       else if (!hmAnimations.get(currentId).isOneShot())
        currentIndex = 0;
      // get a frame of animation
      Frame frame = null;
      try
      {
       frame = hmAnimations.get(currentId).getFrame(currentIndex);
      }
      catch (Exception e) {
       currentIndex=0;
       frame = hmAnimations.get(currentId).getFrame(currentIndex);
      }
      // If it's been more time than the duration of a frame, then go to the next frame
      if (currentTime >= frame.getTime()) {
       frame = hmAnimations.get(currentId).getFrame(currentIndex);
       currentTime = 0;
       currentIndex++;
      }
      currentTime += mTaskIntervalInMillis; 
      // draw a frame
      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;
  }
 }
Besides our SurfaceView thread will contain a hash table with all the animation for the character.
/**
 * View to render the animation
 */
public class AnimationView extends SurfaceView implements
  SurfaceHolder.Callback {

 public static final String TAG = AnimationView.class.getSimpleName();

 private AnimationThread thread; // Thread for drawing
 private HashMap hmAnimations = new HashMap(); // list of animations
 private Context context; 
 
 public AnimationView(Context context, AttributeSet attrs) {
  super(context, attrs);
  this.context = context; 
 }
 

 public void setAnimations(HashMap hmAnimations)
 {
  this.hmAnimations = hmAnimations;
 }
 
 /**
  * Creating a thread to draw
  */
 public void createThread() {
  SurfaceHolder holder = getHolder();
  holder.addCallback(this);
  thread = new AnimationThread(holder);  
 }
 
 /**
  * Resizing the View
  */
 public void surfaceChanged(SurfaceHolder holder, int format, int width,
   int height) {
  thread.setSurfaceSize(width, height);
  thread.setDrawing(true);
  thread.setRunning(true);
 }

 /**
  * Creating the View
  */
 public void surfaceCreated(SurfaceHolder holder) {  
  thread.start();
 }

 /**
  * Destroy the 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;
 }
}


Resources for animation


Now we need to draw the animation for the character and create a xml-files for this animation.
Example animations can be downloaded here.
For each animation to create the xml-file and put them in a folder «res/anim».
For example, to animate a calm state, this would look like:
<?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>
All examples xml-resources can be downloaded from here.
Now we need to change our application 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>

Changes in MainActivity

The final step to a running application will rework our Activity. We need to add objects for animation, get the animation of resources and change our Handler.
To get the animation of the files necessary to develop a parser xml-files. As an example, you can use the following implementation:
    /**
     * Parser xml file with animation
     */
    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);
 }
Now, make any necessary changes to our MainActivity
public class MainActivity extends Activity {

 public static final String TAG = MainActivity.class.getSimpleName();
 
 private PowerManager.WakeLock wl;  // for on / off screen
 private Record record = null;   // for voice recording
 private Playback playback = null; // for voice reproduction
 private AnimationThread thread;     // thread to rendering animation
 // list of animations
 private HashMap hm = new HashMap();
 
 private boolean isPause = false; // monitor when the application goes to pause

 // processing messages from objects
 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:  // voice recording
     if ((record != null) && (!record.isRec())) 
      record.startRecord();    
     thread.setAnimation(R.anim.anim_spok); // character is calm
    break;
    case Constants.MSG_PLAYBACK: // voice reproduction
     thread.setAnimation(R.anim.anim_speak); // character says
     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:   // character listens
     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);  
    }
    
    /**
     * Loading animation from resources
     */
    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();
  }
  // stop recording
  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;     
     // runnable threads for voice recording
     this.record = new Record(handler);   
     this.record.start();
  handler.sendEmptyMessage(Constants.MSG_RECORD);
    }
    
    /**
     * Parser xml file with animation
     */
    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);
 }

}
Important Notes:
  1. Suggested example of development animation can be used in small applications where not a lot of resources and they are not very big. In the game "clatter" we abandoned this method and changed all for a game engine that uses OpenGL.
  2. For correctly display of images in View to develop several sets of pictures for different resolutions.
Talking Pets: Development Pet for Android - Result

Links

  • The source codes of this project can be downloaded here: zip