Part IV: SurfaceView and TextureView
In this final installment we will cover the more advanced SurfaceView and TextureView animation and then experiment at the end by combining techniques together that we have seen throughout the series to create more visually interesting animations. If you haven’t already read part three of this series and are unfamiliar with the Android* canvas, please read that now: Part 3
Table of Contents
Figure 1: Restaurant App with the little chef in middle of the SurfaceView animation
SurfaceView
SurfaceView uses a canvas and is designed to be used with a secondary thread to handle all the drawing logic. Like in the previous canvas method in part three, your canvas will retain what was previously drawn on it. To interact with the surface, it requires a SurfaceHolder which provides a built in lock for the thread to utilize when accessing the canvas. This type of animation is more complex than all the previous types because while it hands over more control, it also hands over more responsibility. You must handle what to do on creation and destruction, in addition to handling locking the canvas and accounting for threading synchronization. If you repeatedly lock and unlock the canvas with the thread running un-interrupted, the locking time itself takes about 16-17 milliseconds in this case giving us a frame rate of about 55 frames per second max. To make sure that the frame rate stays consistent and to eliminate the lock time barrier, we will sleep the thread to make it a standard 30 frames per second; otherwise we are at the mercy of the speed of our device’s CPU. We could even go further if we wanted to account for slower devices by dynamically adjusting the frame rate. If we want to speed up our animation we can sacrifice some visual smoothness to make up for performance by increasing the factor by which the bitmap is translated each time.
Note that the SurfaceView is actually separate from the rest of your view and is drawn behind all your windows. It creates a hole through them to the top in order to be seen. Thus if your image has transparent parts, they are going to show up as black since there is nothing behind them. You can change this by making the SurfaceView be drawn on top of everything else and making the pixel format transparent. Another consequence of its separateness is that it does not behave well when placed in a scrollable container or when you try to animate the view itself. It will lag behind and jump around instead of smoothly moving (a view animation will have no effect). In order to handle these situations, it is recommended that you use a TextureView instead.
For the SurfaceView animation there are 4 steps: (Please refer to part one for more information about our restaurant app setup)
- Change the ImageView to our new ExampleSurfaceView type in the MenuGridFragment.java
ExampleSurfaceView mLittleChefDraw;
Remember to update it in the onCreateView method as well.
mLittleChefDraw= (ExampleSurfaceView)rootView.findViewById(R.id.imageView1);
- Update the layout file to use your custom class. Note that we can no longer wrap content to just the little chef image, if we did then the whole screen will be filled with your canvas. So we have restricted it to the height and width of the image.
<com.example.restaurant.ExampleSurfaceView android:id="@+id/imageView1" android:layout_width="85dp" android:layout_height="90dp" />
- Add the method call in
public void doSurfaceView(){ mLittleChefDraw.startChef(); }
- The custom class itself
package com.example.restaurant; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.util.AttributeSet; import android.view.SurfaceHolder; import android.view.SurfaceView; /* * ExampleSurfaceView is a custom SurfaceView class that also implements * the SurfaceHolder callbacks to handle the changes to the SurfaceView. * It will control the animation of the littleChef icon. */ class ExampleSurfaceView extends SurfaceView implements SurfaceHolder.Callback { Bitmap bitmap= BitmapFactory.decodeResource(getResources(), R.drawable.dish_special); Bitmap starBitmap= BitmapFactory.decodeResource(getResources(), R.drawable.star); int mShortAnimationDuration= 4000; int framesPerSec= 30; //states for our animation int STATE_CURRENT; static final int STATE_INITIALIZE = -1; static final int STATE_LEFT = 0; static final int STATE_RIGHT = 1; static final int STATE_PAUSE = 2; //thread that will drive the animation mThread thread; public ExampleSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); //set up the holder to interact with the canvas of our SurfaceView SurfaceHolder holder = getHolder(); holder.addCallback(this); //put the SurfaceView on top of your window, otherwise it will be behind the view and have a black background setZOrderOnTop(true); //set the pixels to transparent, so we can see our view below the SurfaceView where our image is transparent holder.setFormat(PixelFormat.TRANSPARENT); setFocusable(true); }
The next three methods in the class are the SurfaceHolder callbacks that we need to use to control the changes our SurfaceView goes through. As the SurfaceView is not yet ready after being created with a call to its constructor, we need to wait till we get the callback from the SurfaceHolder before we start using it. Hence we instantiate the thread here instead of the constructor so we don’t try to draw before it is available. This will also handle the case of when the application goes from onPause to onResume, as this way we will always have a thread. Then on destroy, we have to stop all drawing to it by cleaning up our thread. In our case, the surface isn’t going to change format or size so we will leave the method empty.
/* * Callback when the surface has been created and is ready to be drawn on. * We should wait till the surface is ready before drawing, hence we * start our thread here. */ public void surfaceCreated(SurfaceHolder holder) { thread= new mThread(holder); thread.setRunning(true); thread.start(); } /* * Callback when the surface is being destroyed. Need to stop our thread * and make sure we do not touch the surface at all after this method * is called. */ public void surfaceDestroyed(SurfaceHolder holder) { thread.setRunning(false); STATE_CURRENT = STATE_PAUSE; synchronized(thread.mWaitLock){ thread.mWaitLock.notify(); } boolean retry = true; while (retry) { try { thread.join(); retry = false; } catch (InterruptedException e) { e.printStackTrace(); } } } /* * Callback when the surface changes format or size. */ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { //Do nothing. }
This is the method we call from outside the class to start our animation.
//start the animation public void startChef() { //make sure that the animation isn't already running if(STATE_CURRENT == STATE_PAUSE){ STATE_CURRENT= STATE_LEFT; synchronized(thread.mWaitLock){ thread.mWaitLock.notify(); } } }
Below is the nested thread class within our SurfaceView class. For the animations states in this case compared to the custom canvas, we do not want to lock the canvas if we are not going to be doing any drawing on it as this will consume resources. Hence we won’t do anything if our animation is in STATE_PAUSE and we will do all the animation calculations outside of the canvas lock. We will also need a STATE_INITIALIZE now to handle the initial drawing of our view otherwise we won’t see our little chef until the animation starts as we can no longer link the view to a drawable resource in the layout file.
class mThread extends Thread { private SurfaceHolder mSurfaceHolder; private boolean mRun = false; private final Object mRunLock = new Object(); private final Object mWaitLock = new Object(); float mX= 0f; public mThread(SurfaceHolder holder) { this.mSurfaceHolder= holder; STATE_CURRENT=STATE_INITIALIZE; } private void doDraw(Canvas canvas) { //clear the canvas canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); canvas.drawBitmap(bitmap, 0, 0, null); canvas.translate(mX + canvas.getWidth(), 0); canvas.drawBitmap(starBitmap, 0, 0, null); } public void run() { long startTime; long drawTime; //milliseconds per frame long mspf= 1000/framesPerSec; float distance=getWidth(); //calculate the amount to move the stars based on frame rate and animation duration float distanceInterpolatedStar= (distance*2)/(framesPerSec*mShortAnimationDuration/1000); //round to an int distanceInterpolatedStar= Math.round(distanceInterpolatedStar); while (mRun) { if (STATE_CURRENT != STATE_PAUSE){ startTime = System.currentTimeMillis(); //do our calculations up front switch (STATE_CURRENT){ //initially draw our littleChef in the view case STATE_INITIALIZE: STATE_CURRENT= STATE_PAUSE; break; //moving to the left case STATE_LEFT: if (mX>-distance){ mX=mX-distanceInterpolatedStar; }else{ STATE_CURRENT= STATE_RIGHT; } break; //moving to the right case STATE_RIGHT: if (mX<0){ mX=mX+distanceInterpolatedStar; }else{ STATE_CURRENT= STATE_PAUSE; } break; } //draw to our canvas Canvas c = null; try { c = mSurfaceHolder.lockCanvas(null); if(c!=null){ doDraw(c); } } finally { if (c != null) { mSurfaceHolder.unlockCanvasAndPost(c); } } //make the frame rate consistent drawTime = (System.currentTimeMillis() - startTime); if (drawTime <= mspf){ try { sleep(mspf-drawTime); } catch (InterruptedException e) { e.printStackTrace(); } } }else{ //have the thread wait to start again so we aren't doing busy work try { synchronized(mWaitLock){ mWaitLock.wait(); } } catch (InterruptedException e) { e.printStackTrace(); } } } } public void setRunning(boolean b) { synchronized (mRunLock) { mRun = b; } } } }
TextureView
Designed to have the control of a SurfaceView but not its limitations, a TextureView acts more like a view and uses hardware acceleration to improve performance (note that all drawing is now hardware accelerated by default in Android now, but TextureView requires it). It can be used to hold a variety of streaming content, but for our drawing purposes, we will use a canvas. You control the content using a SurfaceTextureListener like the SurfaceHolder for SurfaceView by driving the drawing with a thread, but lock the canvas using the TextureView itself. It can also be animated like a normal view and behaves in a scrollable container. For our TextureView, the canvas lock takes 0-1 milliseconds and hence we need a delay unless we want our star to be a mere blur on the screen, so we will add in the same frame rate control as we did in SurfaceView.
A TextureView can be defined and created right in the main activity class, but for code simplicity it is shown here as its own class.
For the TextureView animation there are 4 steps: (Please refer to part one for more information about our restaurant app setup).
- Change the ImageView to our new ExampleTextureView type in the MenuGridFragment.java
ExampleTextureView mLittleChefTexture;
Remember to update it in the onCreateView method as well.
mLittleChefTexture= (ExampleTextureView)rootView.findViewById(R.id.imageView1);
- Update the layout file to use your custom class. Note that we can no longer wrap content to just the little chef image, if we did then the whole screen will be filled with your canvas. So we have restricted it to the height and width of the image.
<com.example.restaurant.ExampleTextureView android:id="@+id/imageView1" android:layout_width="85dp" android:layout_height="90dp" />
- Add the method call in
public void doTextureView(){ mLittleChefTexture.startTextureView(); }
- Add the custom class itself
package com.example.restaurant; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.SurfaceTexture; import android.util.AttributeSet; import android.view.TextureView; import android.view.TextureView.SurfaceTextureListener; /* * ExampleTextureView is a custom TextureView class that also implements * the SurfaceTextureListener to handle the changes to the TextureView. * It will control the animation of the littleChef icon. */ public class ExampleTextureView extends TextureView implements SurfaceTextureListener { Bitmap bitmap= BitmapFactory.decodeResource(getResources(), R.drawable.dish_special); Bitmap starBitmap= BitmapFactory.decodeResource(getResources(), R.drawable.star); int mShortAnimationDuration= 4000; int framesPerSec= 30; //states for our animation int STATE_CURRENT; static final int STATE_INITIALIZE = -1; static final int STATE_LEFT = 0; static final int STATE_RIGHT = 1; static final int STATE_PAUSE = 2; //thread that will drive the animation mThread thread; public ExampleTextureView(Context context, AttributeSet attrs) { super(context, attrs); setSurfaceTextureListener(this); setOpaque(false); }
Like our SurfaceView’s SurfaceHolder, we need the next four methods for our SurfaceTextureListener in order to handle the availability of our texture.
/* * Listener tells us when the texture has been created and is ready to be drawn on. */ @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { thread = new mThread(); thread.setRunning(true); thread.start(); } /* * Listener tells us when the texture is being destroyed. Need to stop our thread * and make sure we do not touch the texture at all after this method * is called. */ @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { thread.setRunning(false); synchronized(thread.mWaitLock){ thread.mWaitLock.notify(); } STATE_CURRENT = STATE_PAUSE; boolean retry = true; while (retry) { try { thread.join(); retry = false; } catch (InterruptedException e) { e.printStackTrace(); } } return true; } /* * Listener calls when the texture changes buffer size. */ @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { //Do nothing. } /* * Listener calls when the texture is updated by updateTexImage() */ @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { //Do nothing. }
Here is the method we call from outside the class to start our animation.
//start the animation public void startTextureView(){ if(STATE_CURRENT== STATE_PAUSE){ STATE_CURRENT= STATE_LEFT; synchronized(thread.mWaitLock){ thread.mWaitLock.notify(); } } }
Below is our nested thread class within our TextureView class. The main difference here is that we are no longer using the SurfaceHolder to lock and unlock the canvas, as that functionality is built right into the texture itself.
class mThread extends Thread { private boolean mRun = false; private final Object mRunLock = new Object(); private final Object mWaitLock = new Object(); Float mX= 0f; public mThread() { STATE_CURRENT= STATE_INITIALIZE; } private void doDraw(Canvas canvas) { //clear the canvas canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); canvas.drawBitmap(bitmap, 0, 0, null); canvas.translate(mX + getWidth(), 0); canvas.drawBitmap(starBitmap, 0, 0, null); } public void run() { long startTime; long drawTime; //milliseconds per frame long mspf= 1000/framesPerSec; float distance=getWidth(); //calculate the amount to move the star based on frame rate and animation duration float distanceInterpolatedStar= (distance*2)/(framesPerSec*mShortAnimationDuration/1000); //round to an int distanceInterpolatedStar= Math.round(distanceInterpolatedStar); while (mRun) { if (STATE_CURRENT != STATE_PAUSE){ startTime = System.currentTimeMillis(); //do calculations up front synchronized (this) { switch (STATE_CURRENT){ //initially draw our littleChef in the view case STATE_INITIALIZE: STATE_CURRENT= STATE_PAUSE; break; //moving to the left case STATE_LEFT: if (mX>-distance){ mX=mX-distanceInterpolatedStar; }else{ STATE_CURRENT= STATE_RIGHT; } break; //moving to the right case STATE_RIGHT: if (mX<0){ mX=mX+distanceInterpolatedStar; }else{ STATE_CURRENT= STATE_PAUSE; } break; } } //draw to our canvas Canvas c = null; try { c = lockCanvas(null); if(c!=null){ doDraw(c); } } finally { if (c != null) { unlockCanvasAndPost(c); } } //make the frame rate consistent drawTime = (System.currentTimeMillis() - startTime); if (drawTime <= mspf && STATE_CURRENT!=STATE_INITIALIZE){ try { sleep(mspf-drawTime); } catch (InterruptedException e) { } } }else{ //have the thread wait to start again so we aren't doing busy work try { synchronized(mWaitLock){ mWaitLock.wait(); } } catch (InterruptedException e) { e.printStackTrace(); } } } } public void setRunning(boolean b) { synchronized (mRunLock) { mRun = b; } } } }
Combining Techniques
You can take some of these techniques and combine them to create more complex and interesting animations.
Here is a combination of the view animation’s translate and scale in a set combine with a drawable animation:
public void doCombo(){ TranslateAnimation transTo= new TranslateAnimation(0, -mLittleChef.getX()/2, 0, 0); transTo.setRepeatCount(1); transTo.setRepeatMode(Animation.REVERSE); transTo.setDuration(mShortAnimationDuration/2); ScaleAnimation scaleTo= new ScaleAnimation(1, 0.8f, 1, 0.8f, mLittleChef.getWidth()/2, mLittleChef.getHeight()); scaleTo.setRepeatCount(3); scaleTo.setRepeatMode(Animation.REVERSE); scaleTo.setDuration(mShortAnimationDuration/4); AnimationSet transSet= new AnimationSet(true); transSet.setInterpolator(new LinearInterpolator()); transSet.addAnimation(transTo); transSet.addAnimation(scaleTo); transSet.setAnimationListener(new Animation.AnimationListener(){ @Override public void onAnimationEnd(Animation animation) { simpleLock= false; } @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } }); mLittleChef.setBackgroundResource(R.drawable.drawable_animation); AnimationDrawable drawAnimation= (AnimationDrawable) mLittleChef.getBackground(); drawAnimation.stop(); drawAnimation.start(); mLittleChef.startAnimation(transSet); }
And the drawable_animation.xml in the res/drawable:
<?xml version="1.0" encoding="utf-8"?><animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="true"><item android:drawable="@drawable/dish_special" android:duration="500" /><item android:drawable="@drawable/dish_special1" android:duration="500" /><item android:drawable="@drawable/dish_special" android:duration="500" /><item android:drawable="@drawable/dish_special1" android:duration="500" /><item android:drawable="@drawable/dish_special" android:duration="500" /><item android:drawable="@drawable/dish_special1" android:duration="500" /><item android:drawable="@drawable/dish_special" android:duration="500" /><item android:drawable="@drawable/dish_special1" android:duration="500" /><item android:drawable="@drawable/dish_special" android:duration="0" /></animation-list>
Another combination we will do is use the TextureView and animate the little chef shrinking and growing with rotating stars flying by while he moves across the screen! When using a SurfaceView or TextureView combine with a view or property animation, synchronization can be tricky. It is better to build in a translate animation into your TextureView itself. Here we have done that and it requires notifying the main UI thread that our view should be re-drawn in a new position. We do this by creating a handler to talk to the main lopper and send it a message with the new x location. Also, as we are drawing multiple things in different locations and orientations all together, we need to use canvas.save() before doing the matrix transformation and canvas.restore() after we draw the bitmap and are ready to move onto the next drawing.
package com.example.restaurant; import java.math.BigDecimal; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.SurfaceTexture; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.AttributeSet; import android.view.TextureView; import android.view.TextureView.SurfaceTextureListener; /* * ExampleTextureView is a custom TextureView class that also implements * the SurfaceTextureListener to handle the changes to the TextureView. * It will control the animation of the littleChef icon. */ public class ExampleTextureView extends TextureView implements SurfaceTextureListener { Bitmap bitmap= BitmapFactory.decodeResource(getResources(), R.drawable.dish_special); Bitmap starBitmap= BitmapFactory.decodeResource(getResources(), R.drawable.star); float mShortAnimationDuration= 4000; int framesPerSec= 30; //states for our animation int STATE_CURRENT; static final int STATE_INITIALIZE = -1; static final int STATE_LEFT = 0; static final int STATE_RIGHT = 1; static final int STATE_PAUSE = 2;
Below is the handler to send messages to the main UI thread to update the x of our TextureView in order to animate it moving across the screen.
/* * Handler to communicate to the main UI thread. * It gets a x value to set the view to. */ Handler mHandler = new Handler(Looper.getMainLooper()) { /* * handleMessage() defines the operations to perform when * the Handler receives a new Message to process. */ @Override public void handleMessage(Message inputMessage) { // Gets the image task from the incoming Message object. float x = (Float) inputMessage.obj; setX(x); } }; //thread that will drive the animation mThread thread; //original position of our animation; needed to reset our animation on resume float originalX=0; public ExampleTextureView(Context context, AttributeSet attrs) { super(context, attrs); setSurfaceTextureListener(this); setOpaque(false); } /* * Listener tells us when the texture has been created and is ready to be drawn on. */ @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { if(originalX==0){ //orignalX has not been set yet, need to set it originalX= getX(); }else{ //make sure littleChef is at the position it should be Message input= new Message(); input.obj= originalX; mHandler.sendMessage(input); } //start the thread thread = new mThread(); thread.setRunning(true); thread.start(); } /* * Listener tells us when the texture is being destroyed. Need to stop our thread * and make sure we do not touch the texture at all after this method * is called. */ @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { thread.setRunning(false); //wake up the sleeping thread synchronized(thread.mWaitLock){ thread.mWaitLock.notify(); } STATE_CURRENT = STATE_PAUSE; boolean retry = true; while (retry) { try { thread.join(); retry = false; } catch (InterruptedException e) { e.printStackTrace(); } } return true; } /* * Listener calls when the texture changes buffer size. */ @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { //Do nothing. } /* * Listener calls when the texture is updated */ @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { //Do nothing. } //start the animation if not already started public void startTextureView(){ if(STATE_CURRENT== STATE_PAUSE){ STATE_CURRENT= STATE_LEFT; synchronized(thread.mWaitLock){ thread.mWaitLock.notify(); } } } class mThread extends Thread { private boolean mRun = false; private final Object mRunLock = new Object(); private final Object mWaitLock = new Object(); float mX= 0f; BigDecimal mS= new BigDecimal(1); public mThread() { STATE_CURRENT= STATE_INITIALIZE; }
Our onDraw method is more detailed now. We start with scaling the little chef image down followed by doing the star’s rotation and translation. As each bitmap needs its own unique transformation applied to it, we need to save off a default version of our canvas and do a restore after we are finished drawing our transformed bitmap.
private void doDraw(Canvas canvas) { //clear the canvas canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); canvas.save(); canvas.scale(mS.floatValue(), mS.floatValue(), getWidth()/2, getHeight()); canvas.drawBitmap(bitmap, 0, 0, null); canvas.restore(); canvas.save(); canvas.translate(mX + getWidth(), 0); canvas.rotate(mX, starBitmap.getWidth()/2, starBitmap.getHeight()/2); canvas.drawBitmap(starBitmap, 0, 0, null); canvas.restore(); canvas.translate(mX + getWidth(), getHeight()-starBitmap.getHeight()); canvas.rotate(mX, starBitmap.getWidth()/2, starBitmap.getHeight()/2); canvas.drawBitmap(starBitmap, 0, 0, null); } public void run() { long startTime; long drawTime; //milliseconds per frame long mspf= 1000/framesPerSec; float distance=getWidth(); //calculate the amount to move the stars based on frame rate and animation duration float distanceInterpolatedStar= (distance*2)/(framesPerSec*mShortAnimationDuration/1000); //round to an int distanceInterpolatedStar= Math.round(distanceInterpolatedStar); //do the same for littleChef float distanceInterpolatedChef= (getX())/(framesPerSec*mShortAnimationDuration/1000); distanceInterpolatedChef= Math.round(distanceInterpolatedChef); //same for scale BigDecimal scaleSmooth= new BigDecimal((0.2)/(framesPerSec*mShortAnimationDuration/4/1000)); BigDecimal ceiling= new BigDecimal(1); while (mRun) { if (STATE_CURRENT != STATE_PAUSE){ startTime = System.currentTimeMillis(); //do the calculations up front switch (STATE_CURRENT){ //initially draw our littleChef in the view case STATE_INITIALIZE: STATE_CURRENT= STATE_PAUSE; break; //moving to the left case STATE_LEFT: //send the new x coordinate of the view to the main thread Message input= new Message(); input.obj= getX()-distanceInterpolatedChef; mHandler.sendMessage(input); //calculate for the star and scale if (mX>-distance){ mX=mX-distanceInterpolatedStar; if (mX>-distance/2){ mS= mS.subtract(scaleSmooth); }else{ mS= mS.add(scaleSmooth); //skip the last few frames so our scale stays below 1 if(mS.compareTo(ceiling)==1){ mS=ceiling; } } }else{ STATE_CURRENT= STATE_RIGHT; } break; //moving to the right case STATE_RIGHT: Message input2= new Message(); input2.obj= getX()+distanceInterpolatedChef; mHandler.sendMessage(input2); if (mX<0){ mX=mX+distanceInterpolatedStar; if (mX<-distance/2){ mS= mS.subtract(scaleSmooth); }else{ mS= mS.add(scaleSmooth); if(mS.compareTo(ceiling)==1){ mS=ceiling; } } }else{ STATE_CURRENT= STATE_PAUSE; } break; } //draw to the canvas Canvas c = null; try { c = lockCanvas(null); if(c!=null){ doDraw(c); } } finally { if (c != null) { unlockCanvasAndPost(c); } } //make the frame rate consistent drawTime = (System.currentTimeMillis() - startTime); if (drawTime <= mspf && STATE_CURRENT!=STATE_INITIALIZE){ try { sleep(mspf-drawTime); } catch (InterruptedException e) { } } }else{ //have the thread wait to start again so we aren't doing busy work try { synchronized(mWaitLock){ mWaitLock.wait(); } } catch (InterruptedException e) { e.printStackTrace(); } } } } public void setRunning(boolean b) { synchronized (mRunLock) { mRun = b; } } } }
Series Conclusion
We have covered a variety of different animations that you can do in Android from property animations all the way to TextureView animations. You should have a better idea of their different advantages and disadvantages, and which one will work for your purposes. Animation will add a lot to your application visually and the user is bound to have a better experience.
References
http://developer.android.com/guide/topics/graphics/index.html
Part I on Property Animations
Part II on View Animations
Part III on Drawable and Canvas
About the Author
Whitney Foster is a software engineer at Intel in the Software Solutions Group working on scale enabling projects for Android applications.
*Other names and brands may be claimed as the property of others.
**This sample source code is released under the Intel Sample Source Code License Agreement