UPDATE: The code for this is open-sourced. 

This is heavy. So the Android API itself simply want to stop you to do this.

But do we have to stop here? Not really, we still need some sort of animation happens in our widget, with standard home screen supported (ie no need custom home).

Recently I pop out with an idea of making a Mario Coin Block Widget, if you click (or should I say, TAP?) on it, a coin will come out. Without animation, this is nothing. Although it sounds easy (move the block up and down, shows a moving & rotating coin) and hey, it’s only 80s NES animation, there’s no help from the android system. The remote view (setInt) not even let me change the layout setting on my views! Correction: While SetInt cannot, there something called SetBundle in RemoteViews that accepts multiple parameter and thus, you can call setPadding() of view from there.

Since you have the only way to interact/control your widget is with RemoteViews, we have to consider what can be done there, and the entry point of this, will be the SetImageViewBitmap(int, Bitmap). This function allows you to put a Bitmap class object to it and together with simple Handler, you can make 80s animation back to life!

This is how I do this. (NB: I am much more comfortable to use C# like naming convention for classes/objects)

public interface IAnimatable
{
  public abstract boolean AnimationFinished();
  public abstract void Draw(Bitmap canvas);
}

IAnimatable is the interface that let your custom animation do the work. Note that the AnimationFinished() is here for the parent to know if it can still need to poll the redraw regularly. Let me remind you that the redraw is so heavy that you shouldn’t do too much, so it is stopped immediately after it is done. Next will be my concrete implementation on the animating coin.

public class coinAnimation implements IAnimatable {
 private static int[][] coinSprite;
 private static int swidth;
 private static int sheight;
 private int currentSprite = 0;
 private int step;

 // We need the context to get the resource to Bitmap
 public coinAnimation(Context context, CoinBlockView parent)
 {
   step = 10;
   currentSprite = 0;
   prepareSprite(context, R.drawable.money_sprites);
 }

  // Read the sprite from res folder, strip it into sprites,
  // we are saving int[] here as it is most native to Bitmap.setPixels
  private void prepareSprite(Context context, int srpiteResId)
  {
     if (coinSprite !=null) return;
     BitmapFactory.Options options = new BitmapFactory.Options();
     options.inScaled = false;
     Bitmap coin = BitmapFactory.decodeResource(context.getResources(), srpiteResId, options);
     swidth = coin.getWidth()/4;
     sheight = coin.getHeight();
     coinSprite = new int[4][swidth*sheight];
     for(int i=0; i<4; i++)
     {
       coin.getPixels(coinSprite[i], 0, swidth, swidth * i, 0, swidth, sheight);
     }
  }
  // Is animation done?
  public boolean AnimationFinished()
  {
    return step==0;
  }

  // Parent passing the Bitmap as the canvas to draw on it (it's pretty much like most GUI APIs).
  public void Draw(Bitmap canvas)
  {
   // Draw sprite
     canvas.setPixels(coinSprite[currentSprite],
         0, swidth,
         (canvas.getWidth() - swidth) /2 ,
         step * 2, swidth, sheight);
     step --;
     currentSprite++;
     currentSprite %= 4;
   }
}

So, everytime the parent call draw, it will try to draw different sprite on the “canvas” Bitmap. And you can even have multiple children drawing different things! Finally, how I do the parent.


// warning: code snippet only!
// This is called when parent need to redraw.
public void onRedraw(Context context)
	{
                // Look for the widget in RemoteViews and create a Blank canvas
		RemoteViews rviews = new RemoteViews(context.getPackageName(), R.layout.coin_block_widget);
		Bitmap canvas = Bitmap.createBitmap(72, 72, Bitmap.Config.ARGB_8888);
		// Loop through child objects.
                // Since we need to remove from the set, we have to convert it to array first
		IAnimatable [] child = new IAnimatable[Children.size()];
		Children.toArray(child);
		for(int i=0; i<child.length; i++)
		{
			child[i].Draw(canvas);
                        // Remove or not, up to your parent implementation
			if (child[i].AnimationFinished())
				Children.remove(child[i]);
		}

		state.Draw(canvas);
		rviews.setImageViewBitmap(R.id.block, canvas);
		AppWidgetManager.getInstance(context).updateAppWidget(id, rviews);
		// This is important! Your owner view should consider (and think careful)
                // That whether it still need to redraw on regular basis
		if (state.NeedRedraw() || Children.size() > 0)
			setTimedRedraw(REFRESH_RATE);
	}

        // Poll a handler timer to redraw
        private void setTimedRedraw(int timeMillis)
	{
		Handler timer = new Handler();
		timer.postDelayed(
				new Runnable(){
					@Override
					public void run() {
						onRedraw(CoinBlockWidgetApp.getApplication());
					}
				}
				, timeMillis);
	}

As a conclusion, what you could do is some old-school sprite-based animation, with some simple movements of objects. The bottom line for this would be, keep your animation small and short enough!

Advertisements