View Animation with Full Layout Support

Herein lies a tale of frustration with View animations and through necessity, the invention of a ViewGroup that supports smooth animation of views through the animation of the underlying view layout.

For those interested in getting directly at the code, you can find it over at github: DynamicLayoutViewGroup.

Recently I found myself working on an application that required some View animations.  I set about coding up a set of views that would rearrange themselves based on which one was selected.  After some serious code wrangling and some quality time spend reading through the android.view.animation.* source code, I came to the realization that the 2D animation support had two shortfalls that were making my project difficult.

First, the animations don't actually change the position of the views.  It is left as an exercise for the programmer to deal with actually getting the view positions updated at the end of the animation run.  This was even confirmed by Google framework coding guru Romain Guy, who said:

A translate animation does not actually change the position of the view. It changes where it is drawn, not its position within the layout. As such, the values returned by left/top/right/bottom are still valid. Updating the view's coordinate (and more generally its whole geometry) is already planned for a future release as part as the support for static transformations. This is however considered as a low priority for now.

This was not in and of itself a show stopper.   However, the second problem was.  The only animation available to change the size of a view is the ScaleAnimation.  However, it does just that, changes the scale.  The view does not get to redraw itself to fit the new size as it moves, rather a snapshot of the view is scaled up.  Particularly vexing is that this causes 9 patch images to scale up as though they were simple bitmaps.  Not acceptable for my needs.

It turns out that both of these issues could be addressed if instead of animating the View itself, we were to animate the View's layout.   If for each step of a translation and resize, the View were to have its onLayout and onDraw called, we could achieve truly smooth View animations.  The images would not stretch in awkward ways and the content of the view could reshape to fit the new layout.

Fast forward through some furious coding and we arrive at the DynamicLayoutViewGroup.

The specific type of layout that this ViewGroup is designed to manage is one where the selection of a child view causes the position of the views to change.  Most common use is a menu of items that keeps the selected item centered on the screen.  In this kind of layout, when a new child is selected, the views will move so that the new selected child is centered.  In my specific use case, we also wanted the selected child to be larger than the other items.

On to the solution. The concept is simple.  There is a user supplied LayoutModel which is used to indicate a Rect for the bounds of each child view.

public interface LayoutModel {
    /\*\*
    \* called when the parent view size changes
    \*/
    public void onSizeChanged(int width, int height, int oldw, int oldh);
    /\*\*
    \* called for each child (by index) to get a layout Rect
    \*/
    public Rect getLayoutRect(int pos, int selected);
}

When the child views are laid out, the Rect returned is used in the call to the child.layout() function and the values are saved off as the start position for the view should it want to animate.

protected void layoutChildren() {
    // animation code omitted see below
    // when no animation is happening, we simply
    // Layout children based on the values provided by
    // the LayoutModel
    View v;
    ViewHelper vh;
    for (i = 0; i < getChildCount(); i++) {
        v = getChildAt(i);
        r = mLayoutModel.getLayoutRect(i,\_selected);
        v.layout(r.left, r.top, r.right,r.bottom);
        // also, store this position as the start position
        // for the next time we want to animate
        vh = getViewHelper(v, i);
        vh.setStartPosition(r.left, r.top, r.right,r.bottom);
    }
}

When a new child is selected, the LayoutModel is called again to get the new position. It is up to the implementation of the LayoutModel to set the positions based on the new selected child. These new values are stored off as the target Rect for the views. At this point, a flag is set that indicates animation should occur and layout is called.

protected void animateLayout() {
    int i;
    // set up ViewHelpers to animate the views to a new location
    // by asking for new layout Rect and passing in the updated
    // selected item position. The current position is already
    // stored away
    View v;
    ViewHelper vh;
    Rect r;
    for (i = 0; i < getChildCount(); i++) {
        v = getChildAt(i);
        vh = getViewHelper(v,i);
        r = mLayoutModel.getLayoutRect(i, \_selected);
        vh.setTargetPosition(r.left,r.top,r.right,r.bottom);
    }
    // once all the target positions are gathered
    // set the "please animate me" flag and call layout
    mAnimating = true;
    mStartDrawTime = -1;
    layoutChildren();
}

With the animating flag set, we revisted the layoutChildren() function to see how the view animation works. It is similar to the android.view.animation.* classes, using a percentage from start to end time to determine where along the animation path to set the view. Once the views are laid out at the intermediate location, the function uses post() to place another call to layoutChildren in the queue. This happens until the animation duration is elapsed.

protected void layoutChildren() {
    int i;
    Rect r;
    // Is the view animating to new layout positions?
    if (mAnimating) {
        if (mStartDrawTime == -1) {
            mStartDrawTime = SystemClock.uptimeMillis();
        }
        // here we are moving the views and forcing layout
        // to update the child view locations
        // get current time
        long timenow = SystemClock.uptimeMillis();
        // if animation time not expired
        if ((timenow - mStartDrawTime) < mAnimationDuration) {
            // determine time as %
            float tdiff = (float) timenow - (float) mStartDrawTime;
            float timeslice = mStartDrawTime == -1 ? .05f : tdiff
            / (float) mAnimationDuration;
            // use layout helper to layout views
            // the layoutAtTime method will determine where to
            // position the view based on % between the start position
            // and requested end position. timeslice is a % value from
            // 0 to 1 depending on how much animation time remains.
            // the view helper is also where the interpolator is
            // called.
            for (ViewHelper vh : helperList.values()) {
                vh.layoutAtTime(timeslice);
            }
            mLastDrawTime = SystemClock.uptimeMillis();
            // post event to do the layout again
            post(new Runnable() {
                public void run() {
                    layoutChildren();
                }
            });
            } else {
            // the animation duration is passed
            // set mAnimation false
            mAnimating = false;
            mLastDrawTime = -1;
            mStartDrawTime = -1;
            // and call layoutChildren to set final locations using
            // non animation branch of layout
            layoutChildren();
        }
        } else {
        // the non animation layout code is here
    }
}

The code is available at github: DynamicLayoutViewGroup.

There are two examples included of LayoutModels. The default model lays the views out as a horizontal list of views with the selected view centered. The other example sets a different LayoutModel which shows another take on laying out the views on a single screen.

This will all likely become less useful as the new Android 3.0 render script animations become the norm, however a good adventure in coding is never wasted.

Have fun digging in the code and by all means, post questions.