2D Animation for Android* Series: Comparing and Contrasting Different Ways of Doing the Same Animation Part III

Part III: Drawable and Canvas

This is part three of our series, so it’s time to change things up with some slightly different techniques using Drawables and Canvas. Neither animation method supports translating from one place to another in the same way that we did before, so we’ll be doing some different animations that show off their capacities. Canvas will be used more in part four of this series, so make sure you don’t skip it!

Table of Contents

  1. Drawable
  2. Canvas


Figure 1: Restaurant App with the little chef in middle of the Drawables animation

Drawables

Drawable animation is designed for cycling through static images and not for moving your view around. The sequence of images and the time between them is defined in an XML file that goes in res/drawable. This animation is a good candidate to combine with another animation method that will move the view around, like if you want to animate a character walking across the screen. A word of caution though, the drawable will load all images into memory at once, most likely to prevent lag during the animation. So this is best to use with a few small images, otherwise you may be looking at an out of memory exception. In this example, the little chef will move back and forth with stars flashing around him. Note that if you define your view with an image in the layout file, the drawable animation images will be drawn behind it. Also it is only going to play once or repeat forever, and will end on the last image defined in the XML. The play once also refers to when the animation is triggered again, meaning that it is only going to play the first time. However, if you call stop on the animation before it starts, you can make it trigger every time.

public void doDrawables(){
	     mLittleChef.setBackgroundResource(R.drawable.drawable_animation);
	     AnimationDrawable drawAnimation= (AnimationDrawable)    mLittleChef.getBackground();
	     //Reset the animation's oneshot play, so we can start it again
	     drawAnimation.stop();
         drawAnimation.start();
  	     //Let's move the view around at the same time
	     doViewPropertyAnimator();
}

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> 

Canvas

A canvas is very true to its name as it is your drawing board within the view itself. You define the canvas, and then transform it to how you want your image bitmap or shape to be drawn. So when you use canvas.translate, rotate, or scale, you are changing how your object is going to be drawn in the view, not how the view is going to be drawn in the layout. As the canvas is bound within the view itself (it will disappear when it goes out of bounds), I will animate a star flying across our little chef image instead to better illustrate its functionalities. You could animate the canvas itself with a property or view animation; see part one and part two of this series for more details on how to do those animations.

When implementing the canvas you have (at least) two ways to go about it.

In the first way, you can create a bitmap of your image, then link it to a canvas, and finally set it as your view’s bitmap; this allows you to put the canvas into your existing image view. I choose to use a value animator as the mechanism to drive the animation. This simplifies our job as it will do the interpolation logic for us, has a built in onAnimationUpdate listener, and also creates its own handler. When the animation value is updated, I update the image view’s value and then call invalidate on the view to trigger the onDraw method for the canvas to start doing its drawing. If your animation requires a lot of computation this is probably not the best choice. Note that you must paint the canvas clear if you wish to start anew. In our case we want a blank canvas every time otherwise there will a line of stars drawn across our view as it animates. We must also call canvas.save() before drawing and canvas.restore() after drawing to reset the canvas back to the original position before we start drawing again.  

Linking the canvas into your existing image view:

 
public void doCanvas(){
     //Create our resources
	     Bitmap bitmap = Bitmap.createBitmap(mLittleChef.getWidth(), mLittleChef.getHeight(), Bitmap.Config.ARGB_8888);
         final Canvas canvas = new Canvas(bitmap);
	     final Bitmap chefBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.dish_special);
	     final Bitmap starBitmap= BitmapFactory.decodeResource(getResources(),R.drawable.star);

	     //Link the canvas to our ImageView
	     mLittleChef.setImageBitmap(bitmap);

	     ValueAnimator animation= ValueAnimator.ofInt(canvas.getWidth(),0,canvas.getWidth());
	     animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
		          @Override
		          public void onAnimationUpdate(ValueAnimator animation) {
			               int value = (Integer) animation.getAnimatedValue();
			               //Clear the canvas
			               canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
			               canvas.drawBitmap(chefBitmap, 0, 0, null);
			               canvas.save();
			               canvas.translate(value,0);
                                                 canvas.drawBitmap(starBitmap, 0, 0, null);
			               canvas.restore();
			               //Need to manually call invalidate to redraw the view
			               mLittleChef.invalidate();
		           }
	 });
 	         animation.addListener(new AnimatorListenerAdapter(){
		               @Override
		               public void onAnimationEnd(Animator animation) {	
			                        simpleLock= false;
		           }

	 });
	 animation.setInterpolator(new LinearInterpolator());
	 animation.setDuration(mShortAnimationDuration);
	 animation.start();
}

The other way would be to create your own class that extends the view class and write a custom onDraw() method.  This will require you to update the view type in the layout file to use your class. Also your previous canvas will not be retained and you start each time with the original blank canvas, hence why we also no longer need to save or restore our canvas nor do we need to paint it clear. To drive this animation, you could use a valueAnimator as well, but the best BKM is to have all the computation done in its own thread separate from the UI thread. I choose to control the animation by using different states to indicate what our thread should be doing (STATE_PAUSE, STATE_LEFT, STATE_RIGHT). This will also be used to lock our animation instead of the Boolean simpleLock variable like in the previous parts of this series. Remember not to initialize objects or load bitmaps from your resources in your onDraw() draw method or in the run loop, as it will slow down your app as it is called over and over again in quick succession.

For the custom canvas class there are 4 steps: (Please refer to part one for more information about our restaurant app setup)

  1. Change the ImageView to our new ExampleDrawView type in the MenuGridFragment.java

    ExampleDrawView mLittleChefDraw;

    Remember to update it in the onCreateView method as well.

    mLittleChefDraw= (ExampleDrawView)rootView.findViewById(R.id.imageView1);

  2. 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 will restrict it to the height and width of the image.

     
    <com.example.restaurant.ExampleDrawView
              android:id="@+id/imageView1"
              android:layout_width="85dp"
              android:layout_height="90dp" />
    
  3. Add the method call in
    public void doCanvasCustomView(){ 
          mLittleChefDraw.startDrawing();
    
  4. 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.os.Handler;
    import android.util.AttributeSet;
    import android.view.View;
    
    public class ExampleDrawView extends View {
    
         Bitmap bitmap;
         Bitmap starBitmap;
         Float myX= 0f;
         Handler h;
         int mShortAnimationDuration;
         //State variables
         final int STATE_LEFT = 0;
         final int STATE_RIGHT = 1;
         final int STATE_PAUSE = 2;
         int STATE_CURRENT;
    
    
    public ExampleDrawView(Context context, AttributeSet attrs) {
         super(context, attrs);
         h = new Handler();
         bitmap= BitmapFactory.decodeResource(getResources(), R.drawable.dish_special);
         starBitmap= BitmapFactory.decodeResource(getResources(), R.drawable.star);
         mShortAnimationDuration= 4000;
         STATE_CURRENT= STATE_PAUSE;
    }
    
    Runnable move = new Runnable() {
         @Override
         public void run() {
         switch (STATE_CURRENT){
         case STATE_LEFT:
              if (myX>-getWidth()){
              myX=myX-1;
              invalidate();
         }else{
              STATE_CURRENT= STATE_RIGHT;
         }
         h.postDelayed(move, 20);
         break;
    case STATE_RIGHT:
         if (myX<0){
              myX=myX+1;
              invalidate();
              h.postDelayed(move, 20);
         }else{
              STATE_CURRENT= STATE_PAUSE;
         }
         break;
         } 
      }
    
    };
    
    public void startDrawing(){
         //Equivalent of the simpleLock in the other animations
         if(STATE_CURRENT == STATE_PAUSE){
              STATE_CURRENT= STATE_LEFT;
              h.postDelayed(move, 20);
         }
    
    }
    
      @Override
      protected void onDraw(Canvas canvas){
              super.onDraw(canvas);
                    canvas.drawBitmap(bitmap, 0, 0, null);
                    //Transform where the canvas will draw
                    canvas.translate(myX + getWidth(), 0);
                    canvas.drawBitmap(starBitmap, 0, 0, null);
                    //Can also change top left corner position instead of the transform matrix
                    //canvas.drawBitmap(starBitmap, myX + canvas.getWidth(), myY, null);
    
         }
    }
    

Summary

Drawables create a frame by frame animation by cycling through static images, while a canvas gives you a finely controlled way to draw within your view. You will see canvas used again in part four with SurfaceView and TextureView.

References

http://developer.android.com/guide/topics/graphics/index.html

Part I on Property Animations

https://software.intel.com/en-us/android/articles/2d-animation-for-android-series-comparing-and-contrasting-different-ways-of-doing-the-same

Part II on View Animations

https://software.intel.com/en-us/android/articles/2d-animation-for-android-series-comparing-and-contrasting-different-ways-of-doing-the-same-part-II

About the Author

Whitney Foster is a software engineer at Intel in the Software Solutions Group working on scale enabling projects for Android applications.

Copyright © 2014 Intel Corporation. All rights reserved.
*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 

For more complete information about compiler optimizations, see our Optimization Notice.