Thursday, December 15, 2011

Animation Two Ways Part 2

This is part 2 in a 3 part post outlining the approach I took in handling an increasing number of animations across multiple Android OS versions.

In part 1 I demonstrated the idea of defining your moving parts (well, Views) in a UI state class.  You'll see why I did that in this post.  Before I continue, it's a good point to describe what my vision for this solution was.

Ideally all I want in my Activity are Views with listeners for touch.  When a View is notified that it's been touched, rather than put all my animation code in there, I really just want to make a call to some other entity that will do all the work for me.  I'm calling that entity my animation controller.  So all that messy animation code and version checking can be removed from the Activity and dealt with in it's own class. 

In my animation controller class, what I want to do is maintain 2 UI state objects.  One will contain our current UI state and another to contain our previous UI state.  I'll create an instance of the animation controller in my Activity and pass it my parent view.  Since the animation controller has the parent view, it can find any of the child views that we want to animate.  

public class AnimationController {
    // Object to track the state of UI
    private UIState mCurrentUIState;
    private UIState mPreviousUIState;
    // animation helper
    private AnimationHelper mAnimHelper;
    private View mContentView;
    private Context mCtx;

    public AnimationController(Activity ctx, View contentView) {
        this.mCtx = ctx;
        this.mContentView = contentView;

        // Setup animations
        if (VersionCheck.isHoneycombOrGreater()) {
            mAnimHelper = new HoneycombAnimationHelper(mCtx, mContentView);
        } else if (VersionCheck.isFroyoOrGingerbread()) {
            mAnimHelper = new FroyoAnimationHelper(mCtx, mContentView);
        }

        // Create initial UI state (ActionBar, Header shown)
        this.mCurrentUIState = new ConsumptionUIState();
        this.mPreviousUIState = new ConsumptionUIState();

        // Initialise the current UI state
        initUIState();
        mPreviousUIState.copyState(mCurrentUIState);
        // set the previous state to video list closed for the first time
        mPreviousUIState.setShowHeader();
    }

One of the keys to notice here is that I'm creating an instance of AnimationHelper based on which OS we're running on.  I've created a sub class of AnimationHelper for Honeycomb and one for Froyo & Gingerbread.  I'll talk about AnimationHelper in part 3, but for now, that's where the animation code is found.

Whenever the view we want to animate is touched, a call to the animation controller is made.  The animation controller will copy it's current UI state object to it's previous UI state object, then set the new current UI state to the new UI configuration.  In this case instead of handling a touch in my Activity, it's actually a gesture swipe up or down to move between the 3 states of my video chooser (closed, open, full screen).  In my Activity when the gesture is detected, it's determined if it was an up or down swipe and this method is called with the boolean set true if the swipe was up, false if it was down.

    public void swipeVideoHeader(boolean up) {
        // If the user swiped up
        if (up) {
            switch (mCurrentUIState.getHeaderState()) {
                case CLOSED :
                    mPreviousUIState.copyState(mCurrentUIState);
                    mCurrentUIState.setShowFilmStrip();
                    break;
                case OPEN :
                    mPreviousUIState.copyState(mCurrentUIState);
                    mCurrentUIState.setShowGrid();
                    break;
                case FULL :
                    // Do nothing, already expanded to max
                    break;
            }
            // if the user swiped down
        } else {
            switch (mCurrentUIState.getHeaderState()) {
                case CLOSED :
                    // Do nothing, already closed
                    break;
                case OPEN :
                    mPreviousUIState.copyState(mCurrentUIState);
                    mCurrentUIState.setShowHeader();
                    break;
                case FULL :
                    mPreviousUIState.copyState(mCurrentUIState);
                    mCurrentUIState.setShowFilmStrip();
                    break;
            }
        }
        animate();
    }

Finally I created an animate method in the animation controller that will be called whenever we change our current UI state object and based on our previous and current UI states, it will determine which animation should be run.

    private boolean animate() {
        if (mCurrentUIState.compare(mPreviousUIState))
            return false;
        else {
            // FILM STRIP: Animate from OPEN to CLOSED
            if ((mCurrentUIState.getHeaderState() == ConsumptionUIState.HeaderState.CLOSED)
                    && (mPreviousUIState.getHeaderState() == ConsumptionUIState.HeaderState.OPEN)) {
                mAnimHelper.startFilmStripDownAnimation();
            }

            // FILM STRIP: Animate from CLOSED to OPEN
            if ((mCurrentUIState.getHeaderState() == ConsumptionUIState.HeaderState.OPEN)
                    && (mPreviousUIState.getHeaderState() == ConsumptionUIState.HeaderState.CLOSED)) {
                mAnimHelper.startFilmStripUpAnimation();
            }

            // FILM STRIP: Animate from OPEN to FULL
            if ((mCurrentUIState.getHeaderState() == ConsumptionUIState.HeaderState.FULL)
                    && (mPreviousUIState.getHeaderState() == ConsumptionUIState.HeaderState.OPEN)) {
                mAnimHelper.startGridUpAnimation();
            }

            // FILM STRIP: Animate from FULL to OPEN
            if ((mCurrentUIState.getHeaderState() == ConsumptionUIState.HeaderState.OPEN)
                    && (mPreviousUIState.getHeaderState() == ConsumptionUIState.HeaderState.FULL)) {
                mAnimHelper.startGridDownAnimation();
            }

            // ACTIONBAR BOTTOM: Animate from shown to hidden
            if ((mCurrentUIState.showActionBarBottom() == false) && (mPreviousUIState.showPlaylists() == true))
                mAnimHelper.startHeaderBottomOutAnimation();

            // ACTIONBAR BOTTOM: Animate from hidden to shown
            if ((mCurrentUIState.showActionBarBottom() == true) && (mPreviousUIState.showActionBarBottom() == false))
                mAnimHelper.startHeaderBottomInAnimation();

            return true;
        }
    }

In part 3 I'll demonstrate the AnimationHelper super class and the OS specific sub classes where the actual animation code is. And finally tie it all together by showing how I set it all up in my Activity.

No comments:

Post a Comment