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 HashMaphmAnimations = 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 HashMaphm = 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:
- 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.
- For correctly display of images in View to develop several sets of pictures for different resolutions.
No comments:
Post a Comment