Android LayerDrawable and Drawable.Callback

LayerDrawable is a Drawable that manages an array of Drawables, where each Drawable is a layer of it. It can be used to compose fancy visual effects. However, used incorrectly, it can also introduce difficult to catch bugs. This post discusses a bug that can be caused by the Callback mechanism of Drawable.

Chain of Callbacks

In LayerDrawable, every layer/drawable registers the LayerDrawable as its Drawable.callback. This allows the layer to inform the LayerDrawable when it needs to redraw. As shown in the invalidateSelf method from Drawable.java, the DrawableCallback.invalidateDrawable is called to inform the client (the LayerDrawable, in this case) the drawable needs to redraw.

public void invalidateSelf() {
    final Callback callback = getCallback();
    if (callback != null) {
        callback.invalidateDrawable(this);
    }
}

 

Also, a View registers itself as Drawable.callback, so when the drawable needs to redraw, the View can be informed and invalidated. So if we set background of a View to be LayeredDrawable, we have a chain of DrawableCallbacks. This is illustrated in the figure below.

1.jpg

Figure 1. Chain of Callbacks when Setting LayerDrawable as View’s Background

When the Drawable needs to redraw, it calls its callback.invalidateDrawable, which in turns calls LayerDrawable’s callback.invalidateDrawable, which causes the View to redraw the corresponding region if necessary.

View Breaks Callback of Old Background Drawable

So far all work well, but the setBackgroundDrawable of View.java has the following code snippet.

public void setBackgroundDrawable(Drawable background) {
    ...

    /*
     * Regardless of whether we're setting a new background or not, we want
     * to clear the previous drawable.
     */
    if (mBackground != null) {
        mBackground.setCallback(null);
        unscheduleDrawable(mBackground);
    }

    …
    if (background != null) {
        background.setCallback(this);
    }
    ...
}

This means setting a new background will break the old background drawable’s callback by setting it to null, regardless whether the callback is still set to the View.

The Bug

With all the knowledge above, we can “fabricate” a bug by follow the steps below.

  1. Set a Drawable A as a background of a View V. The A.callback is set to V.

  2. Create a LayerDrawable L with A as one layer. Now A.callback is set to L.

  3. Now set another Drawable (or just null) as background of V. V will set callback of its old background (A in our example) to null, which breaks the link between A and L.

  4. Updates A won’t trigger L to update.

The solution to this issue is to update the background of V before creating L. This is illustrated in the code example here.

In the example, we tried animating the actionbar background. The background consists of two layers, the first one is a green ColorDrawable, and second a bitmap drawable. We want to animate the alpha value of the bitmap drawable, so it appears the image is faded in.

The code contains two methods, one works and the other doesn’t due to the bug we described. Each method can be triggered by a button. Below we extract the key part of the code, one can refer to the link for more details.


Button btn1 = (Button) findViewById(R.id.button1);
btn1.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    // 1. This sets launcherIconDrawable.callback to actionBar
    actionBar.setBackgroundDrawable(launcherIconDrawable);
    animateActionBarWorking();
  }
});
Button btn2 = (Button) findViewById(R.id.button2);
btn2.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    // 1. This sets launcherIconDrawable.callback to actionBar
    actionBar.setBackgroundDrawable(launcherIconDrawable);
    animateActionBarNotWorking();
  }
});

private void animateActionBarNotWorking() {
	Drawable[] layers = new Drawable[] { colorLayer, launcherIconDrawable };
	// 2. This sets launcherIconDrawable.callback to layerDrawable
	LayerDrawable layerDrawable = new LayerDrawable(layers);
	// 3. This sets launcherIconDrawable.callback to null
	actionBar.setBackgroundDrawable(layerDrawable);
	ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 255);
	valueAnimator.setDuration(1000);
	valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
	  @Override
	  public void onAnimationUpdate(ValueAnimator animation) {
		// 4. Updates launcherIconDrawable will not trigger action bar background to update
		// as launcherIconDrawable.callback is null
		launcherIconDrawable.setAlpha((Integer) animation.getAnimatedValue());
	  }
	});
	valueAnimator.start();
}

private void animateActionBarWorking() {
	actionBar.setBackgroundDrawable(null);
	animateActionBarNotWorking();
}

 

References:

  1. LayerDrawable: http://developer.android.com/reference/android/graphics/drawable/LayerDrawable.html

Android Animation Playback: Inline Animation with TextView

TextView allows us to attach and detach markup objects to a range of text using Spannable. We can use this to implement inline animation within TextView.

0. AnimatedImageSpan

We implement a AnimatedImageSpan, which extends DynamicDrawableSpan. The object will accept a set of image frames and keep track of the frame to display. A handler and a runnable are implemented to update the frame to display.

Note that AnimatedImageSpan doesn’t update the screen by itself. It calls AnimatedImageUpdateHandler.updateFrame to refresh the entire TextView, which subsequently redraw the AnimatedImageSpan with new image.

public class AnimatedImageSpan extends DynamicDrawableSpan {

    private AnimationAssetsSet mGifAssets;

    private int mCurrentFrameIdx;

    private Context mContext;

    

    private SimpleImageMemCache mImageCache;

    private AnimatedImageUpdateHandler mImageUpdater;

    

    private final Handler handler = new Handler();

    

    public AnimatedImageSpan(Context context) {

        mContext = context;

    }

    

    public void setImageCache(SimpleImageMemCache pImageCache) {

        mImageCache = pImageCache;

    }

    

    public void setAnimationAssets(AnimationAssetsSet pGifAssets) {

        mGifAssets = pGifAssets;

    }

    

    private Runnable mRunnable;

    private int mPlaybackTimes;

    private boolean mPlaying;

    public void playGif(final AnimationSettings pGifSettings, AnimatedImageUpdateHandler pListener) {

        mPlaying = true;

        mImageUpdater = pListener;

        mPlaybackTimes = 0;

        mRunnable = new Runnable() {

            public void run() {

                mCurrentFrameIdx = (mCurrentFrameIdx + 1)%mGifAssets.getNumOfFrames();

//                Logger.d(this, "current frame " + mCurrentFrameIdx);

                handler.postDelayed(this, pGifSettings.mDelay);

                if (null != mImageUpdater) {

//                    Logger.d(this, "update frame using listener " + mImageUpdater.getId());

                    mImageUpdater.updateFrame();

                }

                if (mCurrentFrameIdx == mGifAssets.getNumOfFrames() - 1) {

                    if (pGifSettings.mPlaybackTimes == 0) {

                        //repeat forever

                    } else {

                        mPlaybackTimes++;

                        if (mPlaybackTimes == pGifSettings.mPlaybackTimes) {

                            stopRendering();

                        }

                    }

                }

            }

        };

        handler.post(mRunnable);

    }

    

    public boolean isPlaying() {

        return mPlaying;

    }

    

    public void stopRendering() {

        handler.removeCallbacks(mRunnable);

        mPlaying = false;

    }

    

    @Override

    public Drawable getDrawable() {

        Bitmap bitmap = mImageCache.loadBitmap(mContext, mGifAssets.getGifFramePath(mCurrentFrameIdx));

        BitmapDrawable drawable = new BitmapDrawable(mContext.getResources(), bitmap);

        int width = drawable.getIntrinsicWidth();

        int height = drawable.getIntrinsicHeight();

        drawable.setBounds(0, 0, width > 0 ? width : 0, height > 0 ? height : 0);

        return drawable;

    }

 

    @Override

    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {

//        Logger.d(this, "draw " + mCurrentFrameIdx);

        Drawable b = getDrawable();

        canvas.save();

 

        int transY = bottom - b.getBounds().bottom;

        if (mVerticalAlignment == ALIGN_BASELINE) {

            transY -= paint.getFontMetricsInt().descent;

        }

 

        canvas.translate(x, transY);

        b.draw(canvas);

        canvas.restore();

    }

}

1. Detect Click Events

We want the animation to start playing when the area is clicked, therefore detecting clicking is necessary. We’ll extend ClickableSpan as below.

private static class AnimationClickableSpan extends ClickableSpan {

        AnimatedImageSpan mAnimatedImage;

        AnimationSettings mSettings;

        AnimatedImageUpdateHandler mHandler;

        AnimationClickableSpan(MyTextView pView, AnimatedImageSpan pSpan, AnimationSettings pSettings) {

            mAnimatedImage = pSpan;

            mSettings = pSettings;

            mHandler = new AnimatedImageUpdateHandler(pView);

        }

        

        @Override

        public void onClick(View widget) {

            MyTextView view = (MyTextView) widget;

            if (mAnimatedImage.isPlaying()) {

                mAnimatedImage.stopRendering();

            } else {

                mAnimatedImage.playGif(mSettings, mHandler);

            }

        }

    }

When the click event is detected, we start the animation playback, if it’s clicked while the animation is playing, we stop the playback.

2. MyTextView

We glue everything together in the MyTextView as below.

public class MyTextView extends TextView {

    private SpannableStringBuilder mSb = new SpannableStringBuilder();

    private String dummyText = "dummy " + System.currentTimeMillis();

    private Context mContext;

    private SimpleImageMemCache mImageCache;

    private ArrayList<AnimatedImageSpan> mAnimatedImages = new ArrayList<AnimatedImageSpan>();

 

    public MyTextView(Context context) {

        super(context);

        mContext = context;

    }

    

    public MyTextView(Context context, AttributeSet attrs) {

        super(context, attrs);

        mContext = context;

    }

    

    public MyTextView(Context context, AttributeSet attrs, int defStyle) {

        super(context, attrs, defStyle);

        mContext = context;

    }

    

    public void setImageCache(SimpleImageMemCache pImageCache) {

        mImageCache = pImageCache;

    }

 

    @Override

    protected void onDetachedFromWindow() {

        super.onDetachedFromWindow();

        Log.d(this.getClass().getName(), "onDetachedFromWindow ");

        for (AnimatedImageSpan ais : mAnimatedImages) {

            Log.d(this.getClass().getName(), "animation playing " + ais.isPlaying());

            if (ais.isPlaying()) {

                ais.stopRendering();

            }

        }

        mAnimatedImages.clear();

        mSb.clearSpans();

        mSb.clear();

    }

    

    public void appendText(String pStr) {

        mSb.append(pStr);

    }

    

    public void appendAnimation(AnimationAssetsSet pAsset, AnimationSettings pSettings) {

        mSb.append(dummyText);

        AnimatedImageSpan ais = new AnimatedImageSpan(mContext);

        ais.setImageCache(mImageCache);

        ais.setAnimationAssets(pAsset);

        mSb.setSpan(ais, mSb.length() - dummyText.length(), mSb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        AnimationClickableSpan clickSpan = new AnimationClickableSpan(this, ais, pSettings);

        mSb.setSpan(clickSpan, mSb.length() - dummyText.length(), mSb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        mAnimatedImages.add(ais);

    }

    

    public void finishAddingContent() {

        this.setText(mSb);

        this.setMovementMethod(LinkMovementMethod.getInstance());

    }

    

    private static class AnimationClickableSpan extends ClickableSpan {

        AnimatedImageSpan mAnimatedImage;

        AnimationSettings mSettings;

        AnimatedImageUpdateHandler mHandler;

        AnimationClickableSpan(MyTextView pView, AnimatedImageSpan pSpan, AnimationSettings pSettings) {

            mAnimatedImage = pSpan;

            mSettings = pSettings;

            mHandler = new AnimatedImageUpdateHandler(pView);

        }

        

        @Override

        public void onClick(View widget) {

            MyTextView view = (MyTextView) widget;

            if (mAnimatedImage.isPlaying()) {

                mAnimatedImage.stopRendering();

            } else {

                mAnimatedImage.playGif(mSettings, mHandler);

            }

        }

    }

}

3. The Complete Source Code

The complete source code can found at github repo.

Android Animation Playback: Frame Animation

Android provides AnimationDrawable object to create frame animation as a series of Drawable objects. The details can be found at Drawable Animation guideline at http://developer.android.com/guide/topics/graphics/drawable-animation.html.

This post discusses a different approach where we extends an ImageView and update the image displayed on the ImageView to render an animation.

0. Decode Animation Frame

Decoding frames is relatively computation intensive and therefore it’s better to do it in a separate thread. This can be implemented in code below.

class MyThread extends Thread {

    boolean mIsPlayingGif;

    AnimationSettings mGifSettings;

    MyThread(AnimationSettings pGifSettings) {

        mIsPlayingGif = true;

        mGifSettings = AnimationSettings.newCopy(pGifSettings);

    }

    @Override

    public void run() {

        int repetitionCounter = 0;

        do {

            for (int i = 0; i < mGifAssets.getNumOfFrames(); ++i) {

                if (!mIsPlayingGif) {

                    break;

                }

                Log.d(this.getName(), FrameAnimationView.this.getWidth()

                        + ":" + FrameAnimationView.this.getHeight());

                switch (mLoadMethod) {

                case ASSETS:

                    mTmpBitmap = mImageCache.loadBitmap(mContext, mGifAssets.getGifFramePath(i));

                    break;

                case FILES:

                    mTmpBitmap = mImageCache.loadBitmap(mGifFrames.get(i));

                    break;

                case RESOURCES:

                    //TODO

                    break;

                }

                mHandler.post(mUpdateResults);

                try {

                    Thread.sleep(mGifSettings.mDelay);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

            if(0 != mGifSettings.mPlaybackTimes) {

                repetitionCounter++;

            }

        } while (mIsPlayingGif && repetitionCounter <= mGifSettings.mPlaybackTimes);

    }

}

1. Update Frame

Update the ImageView is required to be done in UI thread. Therefore we need to post a Runnable to a Handler object. The runnable is as below.

final Runnable mUpdateResults = new Runnable() {

    public void run() {

        if (mTmpBitmap != null && !mTmpBitmap.isRecycled()) {

            FrameAnimationView.this.setImageBitmap(mTmpBitmap);

        }

    }

};

2. Test the Animation

To test the animation, we can declare FrameAnimationView in an XML layout as below.

<;RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:paddingBottom="@dimen/activity_vertical_margin"

    android:paddingLeft="@dimen/activity_horizontal_margin"

    android:paddingRight="@dimen/activity_horizontal_margin"

    android:paddingTop="@dimen/activity_vertical_margin"

    tools:context=".FrameAnimationActivity" >;

    <roman10.tutorial.frameanimation.FrameAnimationView

        android:id="@+id/gifPreviewOne"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:layout_marginTop="100dp" />;

</RelativeLayout>

Suppose we have all frames of an animation under assets/1/ folder, to test the animation, we can code an activity as below.

public class FrameAnimationActivity extends Activity {

    private FrameAnimationView mFrameAniView;

    private SimpleImageMemCache mImageCache;

    private Context mContext;

    

    public int convertDpToPixel(int dp) {

        Resources r = mContext.getResources();

        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics()));

    }

    

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        mContext = this;

        setContentView(R.layout.activity_frame_animation);

        mFrameAniView = (FrameAnimationView) this.findViewById(R.id.gifPreviewOne);

        mImageCache = new SimpleImageMemCache(0.20f, convertDpToPixel(320), convertDpToPixel(200));

        mFrameAniView.setImageCache(mImageCache);

        mFrameAniView.setAnimationAssets(new AnimationAssetsSet(this, "1"));

    }

 

    @Override

    public void onResume() {

        super.onResume();

        mFrameAniView.playGif(new AnimationSettings());

    }

 

    @Override

    public void onPause() {

        super.onPause();

        mFrameAniView.stopRendering();

    }

    

    @Override

    public void onDestroy() {

        super.onDestroy();

        mFrameAniView.clearBitmap();

        mImageCache.clearCache();

    }

}

3. The Complete Source Code

The complete source code can be found at github repo. The code includes a simple image memory cache and a few bitmap utilities to optimize image decoding.

Android Animation Playback: Display GIF Animation in WebView

Android doesn’t support GIF natively. However, there’re a few different approaches to display GIF-like animation. In this and subsequently three blogs, we’ll cover two different approaches to display animation, including WebView and Frame Animation. We’ll start with GIF Animation playback in WebView.

The idea of using WebView to display GIF is simply embed the GIF into a few lines of HTML code as below.

<html>

    <head>

    <style type='text/css'>body{margin:auto auto;text-align:center;} img{width:100%25;} </style>

    </head>

    <body>

    <img src="1.gif" width="100%" />

    </body>

</html>

We can create a custom view which extends the WebView to display GIF animation as below.

public class GifWebView extends WebView {

    

    public GifWebView(Context context) {

        super(context);

    }

    

    public GifWebView(Context context, AttributeSet attrs) {

        super(context, attrs);

    }

        

    public void setGifAssetPath(String pPath) {

        String baseUrl = pPath.substring(0, pPath.lastIndexOf("/") + 1);

        String fileName = pPath.substring(pPath.lastIndexOf("/")+1);

        StringBuilder strBuilder = new StringBuilder();

        strBuilder.append("<html><head><style type='text/css'>body{margin:auto auto;text-align:center;} img{width:100%25;} </style>");

        strBuilder.append("</head><body>");

        strBuilder.append("<img src="" + fileName + "" width="100%" /></body></html>");

        String data = strBuilder.toString();

        Log.d(this.getClass().getName(), "data: " + data);

        Log.d(this.getClass().getName(), "base url: " + baseUrl);

        Log.d(this.getClass().getName(), "file name: " + fileName);

        loadDataWithBaseURL(baseUrl, data, "text/html", "utf-8", null);

    }

}

We can declare a XML layout with the custom view.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:paddingBottom="@dimen/activity_vertical_margin"

    android:paddingLeft="@dimen/activity_horizontal_margin"

    android:paddingRight="@dimen/activity_horizontal_margin"

    android:paddingTop="@dimen/activity_vertical_margin"

    tools:context=".GifWebviewDisplayActivity" >

 

    <roman10.tutorial.gifinwebview.GifWebView

        android:id="@+id/gif_view"

        android:layout_height="match_parent"

        android:layout_width="match_parent">

    </roman10.tutorial.gifinwebview.GifWebView>

 

</RelativeLayout>

Suppose we have an gif animation file 1.gif under assets folder, we can display the gif file in an activity as below.

public class GifWebviewDisplayActivity extends Activity {

    private GifWebView gifView;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_gif_webview_display);

        gifView = (GifWebView) findViewById(R.id.gif_view);

        gifView.setGifAssetPath("file:///android_asset/1.gif");

    }

}

The complete source code can be found at github repo.

References:

1. WebView Android doc: http://developer.android.com/reference/android/webkit/WebView.html

Android Property Animation — ValueAnimator

This is the second post on Android Property Animation. Previous post can be found below.

Android Property Animation Overview.

ValueAnimator is the core of the Property Animation system. It provides a timing engine which calculates the animation values. It also allows us to get notified at every animation frame through ValueAnimator.AnimatorUpdateListener interface.

Animation Handler

All animations created by Property Animation system share a single timing pulse, which is maintained by a custom static handler AnimationHandler at ValueAnimator class. Below is the code extracted from NineOldAndroids library ValueAnimator class.

/**

 * This custom, static handler handles the timing pulse that is shared by

 * all active animations. This approach ensures that the setting of animation

 * values will happen on the UI thread and that all animations will share

 * the same times for calculating their values, which makes synchronizing

 * animations possible.

 *

 */

private static class AnimationHandler extends Handler {

    /**

     * There are only two messages that we care about: ANIMATION_START and

     * ANIMATION_FRAME. The START message is sent when an animation's start()

     * method is called. It cannot start synchronously when start() is called

     * because the call may be on the wrong thread, and it would also not be

     * synchronized with other animations because it would not start on a common

     * timing pulse. So each animation sends a START message to the handler, which

     * causes the handler to place the animation on the active animations queue and

     * start processing frames for that animation.

     * The FRAME message is the one that is sent over and over while there are any

     * active animations to process.

     */

    @Override

    public void handleMessage(Message msg) {

        boolean callAgain = true;

        ArrayList<ValueAnimator> animations = sAnimations.get();

        ArrayList<ValueAnimator> delayedAnims = sDelayedAnims.get();

        switch (msg.what) {

            // TODO: should we avoid sending frame message when starting if we

            // were already running?

            case ANIMATION_START:

                ArrayList<ValueAnimator> pendingAnimations = sPendingAnimations.get();

                if (animations.size() > 0 || delayedAnims.size() > 0) {

                    callAgain = false;

                }

                // pendingAnims holds any animations that have requested to be started

                // We're going to clear sPendingAnimations, but starting animation may

                // cause more to be added to the pending list (for example, if one animation

                // starting triggers another starting). So we loop until sPendingAnimations

                // is empty.

                while (pendingAnimations.size() > 0) {

                    ArrayList<ValueAnimator> pendingCopy =

                            (ArrayList<ValueAnimator>) pendingAnimations.clone();

                    pendingAnimations.clear();

                    int count = pendingCopy.size();

                    for (int i = 0; i < count; ++i) {

                        ValueAnimator anim = pendingCopy.get(i);

                        // If the animation has a startDelay, place it on the delayed list

                        if (anim.mStartDelay == 0) {

                            anim.startAnimation();

                        } else {

                            delayedAnims.add(anim);

                        }

                    }

                }

                // fall through to process first frame of new animations

            case ANIMATION_FRAME:

                // currentTime holds the common time for all animations processed

                // during this frame

                long currentTime = AnimationUtils.currentAnimationTimeMillis();

                ArrayList<ValueAnimator> readyAnims = sReadyAnims.get();

                ArrayList<ValueAnimator> endingAnims = sEndingAnims.get();


                // First, process animations currently sitting on the delayed queue, adding

                // them to the active animations if they are ready

                int numDelayedAnims = delayedAnims.size();

                for (int i = 0; i < numDelayedAnims; ++i) {

                    ValueAnimator anim = delayedAnims.get(i);

                    if (anim.delayedAnimationFrame(currentTime)) {

                        readyAnims.add(anim);

                    }

                }

                int numReadyAnims = readyAnims.size();

                if (numReadyAnims > 0) {

                    for (int i = 0; i < numReadyAnims; ++i) {

                        ValueAnimator anim = readyAnims.get(i);

                        anim.startAnimation();

                        anim.mRunning = true;

                        delayedAnims.remove(anim);

                    }

                    readyAnims.clear();

                }


                // Now process all active animations. The return value from animationFrame()

                // tells the handler whether it should now be ended

                int numAnims = animations.size();

                int i = 0;

                while (i < numAnims) {

                    ValueAnimator anim = animations.get(i);

                    if (anim.animationFrame(currentTime)) {

                        endingAnims.add(anim);

                    }

                    if (animations.size() == numAnims) {

                        ++i;

                    } else {

                        // An animation might be canceled or ended by client code

                        // during the animation frame. Check to see if this happened by

                        // seeing whether the current index is the same as it was before

                        // calling animationFrame(). Another approach would be to copy

                        // animations to a temporary list and process that list instead,

                        // but that entails garbage and processing overhead that would

                        // be nice to avoid.

                        --numAnims;

                        endingAnims.remove(anim);

                    }

                }

                if (endingAnims.size() > 0) {

                    for (i = 0; i < endingAnims.size(); ++i) {

                        endingAnims.get(i).endAnimation();

                    }

                    endingAnims.clear();

                }


                // If there are still active or delayed animations, call the handler again

                // after the frameDelay

                if (callAgain && (!animations.isEmpty() || !delayedAnims.isEmpty())) {

                    sendEmptyMessageDelayed(ANIMATION_FRAME, Math.max(0, sFrameDelay -

                        (AnimationUtils.currentAnimationTimeMillis() - currentTime)));

                }

                break;

        }

    }

}

The handler only handles two types of messages, including ANIMATION_START and ANIMATION_FRAME.

ANIMATION_START message is generated when an animation starts, the handler will receive the message and put the animation into delay queue. Next the handler decide if the animations from the delay queue should be started immediately (add to ready queue). The handler will then process all active animations, which includes those in the ready queue.

ANIMATION_FRAME message is sent by the handler itself to schedule next animation frame update. The handler checks if any animation from the delay queue should be started and then process all active animations. (The code to handle ANIMATION_FRAME is actually part of the code for handling ANIMATION_START).

The handler also checks if an animation should be ended or not in the handler.

By using this static handler, all animations are synchronized on a frame by frame basis.

Using ValueAnimator

Animating a single value with ValueAnimator is straightforward. ValueAnimator provides static method like ofInt and ofFloat to animation between integer and floating point values. We will skip that and discuss how to animation multiple values.

Suppose we need to animate an ImageView of a red square from bottom left to top right as shown in the screenshots below.

 

Figure 1. Animation Bottom Left to Top Right

It is obvious that we need to update both the x and y coordinates. The naive approach is to create two animations and play them simultaneously, but it is not efficient. We can use PropertyValuesHolder to combine multiple values. Alternatively, we can use custom objects as animation values. We’ll explore both approaches.

We will use the layout as indicated by the xml file below. We want to move the ImageView inside the FrameLayout with id “container”.

 

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    tools:context=".MainActivity" >


    <FrameLayout

        android:id="@+id/container"

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:layout_alignParentTop="true">

    </FrameLayout>

    <LinearLayout

        android:id="@+id/controlBtns"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:orientation="horizontal"

        android:layout_alignParentBottom="true">

        <Button

            android:id="@+id/btnStart"

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:text="Start">

        </Button>

        <Button

            android:id="@+id/btnReset"

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:text="Reset">

        </Button>

    </LinearLayout>

    <ImageView

        android:id="@+id/image"

        android:layout_width="50dp"

        android:layout_height="50dp"

        android:src="#ffff4444"

        android:layout_above="@id/controlBtns">

    </ImageView>

</RelativeLayout>

Using PropertyValuesHolder

PropertyValuesHolder contains information about a property and its values during animation. It is used with ValueAnimator or ObjectAnimator to animate multiple properties at the same time.

In order to support property animation in pre-3.0 Android devices, NineOldAndroids provide a class AnimatorProxy to wrap a view and handles drawing of animated views. We need to wrap the ImageView in our code as below.

ImageView mImageView = (ImageView) this.findViewById(R.id.image);

mImageAnimatorProxy = AnimatorProxy.wrap(mImageView);

The code below creates an animation which animates two values which represent X and Y position of the image view with respective to the frame layout.

PropertyValuesHolder widthPropertyHolder = PropertyValuesHolder.ofFloat("posX", mImageAnimatorProxy.getX(), container.getWidth() - mImageView.getWidth());

PropertyValuesHolder heightPropertyHolder = PropertyValuesHolder.ofFloat("posY", mImageAnimatorProxy.getY(), 0);

ValueAnimator mTranslationAnimator = ValueAnimator.ofPropertyValuesHolder(widthPropertyHolder, heightPropertyHolder);

mTranslationAnimator.addUpdateListener(ValueAnimatorDemo.this);

mTranslationAnimator.setDuration(1000);

mTranslationAnimator.start();

We use two PropertyValuesHolder to animate two values named posX and posY. We then created the animation using ValueAnimator. We registered the listener, set the duration and start the animation. The listener interface is implemented by overriding the onAnimationUpdate callback function as below.

@Override

public void onAnimationUpdate(ValueAnimator arg0) {

    float posX = (Float) arg0.getAnimatedValue("posX");

    float posY = (Float) arg0.getAnimatedValue("posY");

    mImageAnimatorProxy.setX(posX);

    mImageAnimatorProxy.setY(posY);

}

We retrieve the animated values posX and posY at every animation frame and update it.

Using Custom Objects as Animation Values

ValueAnimator can be used to animate custom objects also. We’ll need to specify a TypeEvaluator to tell the ValueAnimator how the values should be calculated.

In our example, we are moving the position of the ImageView, so we can create a custom object to represent the position of the ImageView and animate it. The code below creates a class Position to represent the position of the ImageView.

private class Position {

    private float posX;

    private float posY;


    public float getPosX() {

        return posX;

    }


    public float getPosY() {

        return posY;

    }


    Position(float pPosX, float pPosY) {

        posX = pPosX;

        posY = pPosY;

    }

}

We also provide to TypeEvaluator by implementing the android.animation.TypeEvaluator<T> interface.

private class PositionTypeEvaluator implements TypeEvaluator<Position> {

    @Override

    public Position evaluate(float fraction, Position startValue, Position endValue) {

        float posX = startValue.getPosX() + (endValue.getPosX() - startValue.getPosX()) * fraction;

        float posY = startValue.getPosY() + (endValue.getPosY() - startValue.getPosY()) * fraction;

        return new Position(posX, posY);

    }

}

At every animation frame, the callback function evaluate will be triggered with three arguments, including the elapsed time so far, the animation start position and end position. We can calculate the current position should be based on the three input arguments and returns it.

With the custom object and TypeEvaluator in place, the animation can be easily created as below.

ValueAnimator mAnimator = ValueAnimator.ofObject(new PositionTypeEvaluator(), new Position(mOriX, mOriY),

                        new Position(container.getWidth() - mImageView.getWidth(), 0));

mAnimator.addUpdateListener(ValueAnimatorDemo2.this);

mAnimator.setDuration(1000);

mAnimator.start();

Of course, we still need to implement the ValueAnimator.AnimatorUpdateListener interface.

@Override

public void onAnimationUpdate(ValueAnimator pAnimator) {

    Position currentPos = (Position) pAnimator.getAnimatedValue();

    mImageAnimatorProxy.setX(currentPos.getPosX());

    mImageAnimatorProxy.setY(currentPos.getPosY());

}

Note that we can also use ViewPropertyAnimator, which I’ll cover in another post if I got time.

You can get the full source code here: https://github.com/roman10/roman10-android-tutorial/tree/master/PropertyAnimation

References:

Android ValueAnimator doc: http://developer.android.com/reference/android/animation/ValueAnimator.html

Android Property Animation Overview

Property animation is introduced in Android 3.0 Honeycomb. A whole new set of APIs comes with the android.animation package (The view animation is exposed through android.view.animation package), which makes animation in Android more flexible and powerful.

1. Limitation of View Animation System

The property animation system is introduced to tackle the limitations of the view animation systems in pre-3.0 Android as described below.

Firstly, we can only animate views. This is enough most of the times since animation manipulates the GUI, which consists of views. However, there are times we want to animate non-view objects or properties, then we are on our own. For example, if we have a custom view implemented by drawing a few drawables on canvas and we want to animate the individual drawable, we won’t be able to use the view animation system.

Secondly, the animation is constrained to position, size, rotation etc. Properties like background color is not supported by the view animation system.

Thirdly, the view animation system only updates where the view is drawn, but not the view itself. This can cause issues sometimes. For example, if we animate a button to move outside of current screen, after the animation ends, the button is not shown on screen, but we can still click the button’s original location to trigger click event. This is because the location of the button is not changed, though its not drawn on the screen. In this case, we’ll need to write additional code to make sure the button behave properly.

2. The android.animation package

The property animation system APIs are exposed by the android.animation package. The hierarchy of the main classes in the package is as below.

Fig 1. android.animation class hierarchy

ValueAnimator: it is the core the property animation system. There’re two steps to animate a property, including calculating the animated values and assign the values to the property of the animated object. The ValueAnimator can handle the first part but not the second.

Fig 2. ValueAnimator class

The class allows us to specify a time interpolator to control the speed and type evaluator to control the animation values. We’ll cover more about those in future posts.

ObjectAnimator: it’s a subclass of ValueAnimator, which handles both steps of the property animation for us. In other words, it updates the target property value when calculating the animated values.

AnimatorSet: it provides interfaces to group animations together. We can play animations together, sequentially, one after another, etc.

TimeAnimator: This class is introduced in API level 16 (Android 4.1 Jelly Bean). It provides a simple callback mechanism with the TimeAnimator.TimeListener interface. All animators in the system is synchronized by Android system animation frame event. TimeAnimator allows us to get notified with the animation frame event through the TimeListener interface. The method onTimeUpdate will be triggered with the TimeAnimator instance, the total animation time elapsed since the animator started and the time elapsed since preview frame, in milliseconds.

3. NineOldAndroids Library

NineOldAndroids is an amazing library which enables property animation all the way back to Android 1.0. Well, not everything of property animation, but most of it. (e.g. layout animation is left out.)

In order to support the property animation, the library use a wrapper class named AnimatorProxy to wrap a view. The wrapper class facilitates modification of post-3.0 view properties on pre-3.0 platforms and draw the view property.

References:

1. NineOldAndroids: http://nineoldandroids.com/

2. Google Developer Blog, Animation in Honeycomb: http://android-developers.blogspot.sg/2011/02/animation-in-honeycomb.html

3. Android API Guides, Property Animation: http://developer.android.com/guide/topics/graphics/prop-animation.html

Android Custom Notification with Progress Bar

When your android app is doing something that user needs to wait, it’s better to tell the user how much has been done and how long user needs to wait. Progress bar comes into handy. Sometimes user goes to other app, but still wants to check out the task your app is doing, then a custom notification with progress bar is your choice.

It’s used in Android Market downloading. When you’re downloading an app, a notification is post to the status bar with progress bar indicates the progress. This post is how to make your own custom notification with progress bar and what you should avoid. The screenshot is as below,

Figure 1. Custom Notification with Status Bar

If you simply want the code, go to the end of the post and download it. Android doc (reference 1) provides a good tutorial about how to post notification and create custom notification in a step-by-step manner. It’s not repeated here.

The Code

The code to create the notification bar is as below,

nm = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);

CharSequence tickerText = "hello";

long when = System.currentTimeMillis();

noti = new Notification(R.drawable.ic_launcher, tickerText, when);

context = this.getApplicationContext();

Intent notiIntent = new Intent(context, ProgressBarNotificationActivity.class);

PendingIntent pi = PendingIntent.getService(context, 0, notiIntent, 0);

noti.flags |= Notification.FLAG_AUTO_CANCEL;

CharSequence title = "Downloading initializing...";

RemoteViews contentView = new RemoteViews(getPackageName(), R.layout.noti);

contentView.setImageViewResource(R.id.status_icon, R.drawable.ic_launcher);

contentView.setTextViewText(R.id.status_text, title);

contentView.setProgressBar(R.id.status_progress, 100, 0, false);

noti.contentView = contentView;

noti.contentIntent = pi;

nm.notify(STATUS_BAR_NOTIFICATION, noti);

You’ll need to update the progress bar using a background thread. The code is as below,

new Thread(new Runnable() {

   public void run() {

        int mCount = 0;

        mRun = true;

        while (mRun) {

        ++mCount;

        SystemClock.sleep(1000);

        CharSequence title = "Downloading: " + mCount%100 + "%";

        noti.contentView.setTextViewText(R.id.status_text, title);

        noti.contentView.setProgressBar(R.id.status_progress, 100, mCount%100, false);

        nm.notify(STATUS_BAR_NOTIFICATION, noti);

    }

   }

}).start();

The XML layout is noti.xml,

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="fill_parent" 

    android:layout_height="fill_parent"

    android:padding="5dp">

    <ImageView android:id="@+id/status_icon"

        android:layout_width="wrap_content" 

        android:layout_height="fill_parent"

        android:layout_alignParentLeft="true" 

        android:layout_marginRight="10dp"/>

 

    <RelativeLayout android:layout_width="fill_parent"

        android:layout_height="fill_parent" 

        android:layout_toRightOf="@id/status_icon">

 

        <TextView android:id="@+id/status_text" 

            android:layout_width="fill_parent"

            android:layout_height="wrap_content" 

            android:layout_alignParentTop="true" 

            style="@style/NotificationTitle"/>

        <ProgressBar android:id="@+id/status_progress"

            android:layout_width="fill_parent" 

            android:layout_height="wrap_content"

            android:layout_below="@id/status_text"

            android:indeterminate="false" 

            android:indeterminateOnly="false"

            style="?android:attr/progressBarStyleHorizontal"  />

 

 

    </RelativeLayout>

 

</RelativeLayout>

What to Avoid

If your task is long running, DO NOT update the progress very frequently. Frequent custom notification consumes lots of CPU and may hurt your app performance. A post here describes the detail.

Download

You can download the source file here or get it from my github.

References:
1. Android Developer Doc: Status Bar Notification, available at http://developer.android.com/guide/topics/ui/notifiers/notifications.html

Frequent Custom Status Bar Notification is Evil on Android

The Story Behind

In my recent app, video converter for Android, I add a notification progress report on the status bar. So users can view the video conversion progress any time. I also did several other changes, and then start running the final test before release.

Then I found the app is much slower compared with previous version. I couldn’t think of why. I turned on Power Tutor app, and found that the system process is consuming a lot of CPU. Notification bar is called through NotificationManager, which a system service. I guess it’s because I’m updating the status bar progress every second.

In the end, I changed the status progress update interval and app works as normal. But just to demonstrate frequent custom status bar notification is really evil, I programmed a simple testing app does nothing but status bar notification update.

Why I Say it’s Evil

The key part of the code is as below (you can get the entire code at the end of the post),

SystemClock.sleep(1000);

CharSequence title = "Freq noti is evil: " + mCount;

CharSequence content = "Freq notification update takes too much CPU";

if (CUSTOM_NOTI) {

    noti.contentView.setTextViewText(R.id.status_text, title);

    noti.contentView.setProgressBar(R.id.status_progress, 100, mCount%100, false);

} else {

    Intent notiIntent = new Intent(context, StatusBarNotificationActivity.class);

    PendingIntent pi = PendingIntent.getService(context, 0, notiIntent, 0);

    noti.setLatestEventInfo(context, title, content, pi);

}

//nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

//nm = (NotificationManager) getApplicationContext().getSystemService(getApplicationContext().NOTIFICATION_SERVICE);

nm.notify(STATUS_BAR_NOTIFICATION, noti);

The testing app supports both custom and default status notification. The custom status bar notification contains an icon, a text view and a progress bar. It updates the notification once every second. 

The Power Tutor CPU power consumption curve is as below for default status notification,

Figure 1. CPU Power Consumption with Frequent Default Status Bar Notification Update

The left one is before testing app starts, the right one is when testing app is running. There’s almost no difference.

The Power Tutor CPU power consumption curve is as below for custom status notification,

 

Figure 2. CPU Power Consumption with Frequent Custom Status Bar Notification

The first one is before testing app starts, the second one is when testing app is running, and the third one is after testing app is finished. Clearly frequent update of the custom notification status consumes lots of CPU resources.

I also tried without updating the progress bar (so only the title text of the notification is updated), the consumption is less, but still much higher than default one.

Figure 3. Custom Notification without Progress bar

You can download the testing app here or from my github. Note that you might want to run the testing app for a while to see the results. At some of my tests, the CPU consumption is low initially, but raise high after a while.

The testing is done on Nexus One device with Android 2.3, the android sdk used for development is 2.1 and 4.0.

Android Tutorial–Customized Radio Button List

Radio buttons are almost always used to represent single choice from a list of items. Android is no exception to that.

Android provides two ways to use radio buttons. At a higher level, it allows one to use radio button list in ListActivity by setting the choice mode, as indicated in the code below,

public class List10 extends ListActivity {

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_single_choice, CONTENT));

        final ListView listView = getListView();

        listView.setItemsCanFocus(false);

        listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);

    }

    private static final String[] CONTENT = new String[] {

        "Action", "Adventure", "Animation", "Children", "Comedy", "Documentary", "Drama",

        "Foreign", "History", "Independent", "Romance", "Sci-Fi", "Television", "Thriller"

    };

}

The effect is as below,

sc0

At a lower level, one can create radio button list by using RadioGroup and RadioButton. One can find the detailed code in the RadioGroup1.java source file downloadable at the end of this post. The effect of this method is as below,

sc1

However, the lower level default radio button only allows one to place the text on the right of the button, and the higher level method is also difficult to customize. But one can always build a radio button list from scratch.

The method is to place a radio button and a text view in a linear layout to form a single item and then use it as the element to build list view. The effect is as below,sc2

The complete code can be downloaded from here.

Android Tutorial–TabMenu Based on Customized PopupWindow

The Android standard menu can be limited sometimes, especially when you have more than 6 items. Some apps have dozens of menu and the menu starts to look very crowded. For instance, the Opera browser has the menu like below,

1

Figure 1. Opera Menu Panel

This tutorial introduces a tab-based menu (we call it TabMenu from this point onwrads) shown as below,

tabmenu

Figure 2. TabMenu

This types of menu have been used in apps like GoSMS, UC Web Browser, and it’s pretty neat, isn’t it? Smile

Design Idea

The menu is actually a PopupWindow with two grid layout. The first layout contains the TextView, indicating the menu titles; the second grid has a list of ImageView and TextView, forming the menu items. The two grids stack together to form the pop up window.

Source Code

You can download the source code package for the sample app shown in Figure 2 at here or my github repo here. The TabMenu.java and Main.java source files are identical to the source code provided by Reference 1. The PopupWindow.java is a modified version of the Android PopupWindow.

Why Customized PopupWindow

The Android PopupWindow is suppoed to be a modal window, which requires user to interact with it first before proceeding. Some Android popup dialogs are also built in this way. Therefore it is understandable that the Menu key press doesn’t work with the PopupWindow.

It’s fine for most case, but we’re building a menu here, so users will expect the menu key work. In order to meet the user expectation, the code is modified.

The PopupWindow is a view that put in FrameLayout. The FrameLayout part contains a method that dispatchKeyEvent.

The original source looks as below,

@Override

        public boolean dispatchKeyEvent(KeyEvent event) {

            if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {

                if (event.getAction() == KeyEvent.ACTION_DOWN

                        && event.getRepeatCount() == 0) {

                    getKeyDispatcherState().startTracking(event, this );

                    return true;

                } else if (event.getAction() == KeyEvent.ACTION_UP

                        && getKeyDispatcherState().isTracking(event)

                        && !event.isCanceled()) {

                    dismiss();

                    return true;

                }

                return super .dispatchKeyEvent(event);

            } else {

                return super .dispatchKeyEvent(event);

            }

        }

To make the menu key press to dismiss the popup window, we simply add the KEYCODE_MENU to the first if condition, and the code becomes,

@Override

        public boolean dispatchKeyEvent(KeyEvent event) {

            if (event.getKeyCode() == KeyEvent.KEYCODE_BACK || event.getKeyCode() == KeyEvent.KEYCODE_MENU) {

                if (event.getAction() == KeyEvent.ACTION_DOWN

                        && event.getRepeatCount() == 0) {

                    getKeyDispatcherState().startTracking(event, this );

                    return true;

                } else if (event.getAction() == KeyEvent.ACTION_UP

                        && getKeyDispatcherState().isTracking(event)

                        && !event.isCanceled()) {

                    dismiss();

                    return true;

                }

                return super .dispatchKeyEvent(event);

            } else {

                return super .dispatchKeyEvent(event);

            }

        }

This will make the menu key press work, and now the TabMenu behaves like a menu instead of a PopupWindow.

Reference:

1. Android Customized TabMenu: http://blog.csdn.net/hellogv/article/details/6168439

2. Android PopupWindow Source Code