Saturday, December 17, 2011

Animation Two Ways part 3


This is part 3 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.
In part 2 I demonstrated the idea of managing all your animation situations in an animation controller class.

In the final part I will demonstrate how I deal with supporting both the old Animation API in Gingerbread and lower, as well as the new Animation API in Honeycomb and greater.

You might have noticed in part 2 that I created an instance of AnimationHelper.  This is the super class where I define the method signatures for all the animations I want to execute.  I've created it as an abstract class, so I'm not actually doing anything here other than creating the method signatures and holding references to the Views I plan to animate.

public abstract class AnimationHelper {

    protected View mPlaylists;
    protected View mContent;
    protected View mFilmStrip;
    protected Context mCtx;

    public AnimationHelper() {
        super();
    }

    /**
     * Define the animation for raising the film strip
     */
    public void startFilmStripUpAnimation() {
    }

    /**
     * Define the animation for lowering the film strip
     */
    public void startFilmStripDownAnimation() {
    }

Then I create a sub class of AnimationHelper for each OS version I want to support. In this case there are only 2 Animation API's. Gingerbread and lower and Honeycomb and higher. So I create FroyoAnimationHelper and HoneycombAnimationHelper where I implement the methods from the super class.  Here is a snippet from my HoneycombAnimationHelper class showing some of the implementation.  Of course the implementation will look different in the FroyoAnimationHelper since you will use the old Animation API for older versions.

public class HoneycombAnimationHelper extends AnimationHelper {
 
 /**
  * Create a HoneycombAnimationHelper object
  * @param mCtx  Activity context
  * @param parent Parent view containing a playlist and a filmstrip
  */
    public HoneycombAnimationHelper(Context mCtx, View parent) {
  this.mCtx = mCtx;
  this.mContent = parent.findViewById(R.id.content);
  this.mPlaylists = parent.findViewById(R.id.playlists);
  this.mFilmStrip = parent.findViewById(R.id.consumption_videos);
 }

 /* (non-Javadoc)
  * @see com.macadamian.android.ottasee.animation.AnimationHelper#startFilmStripUpAnimation()
  * 
  * Using new Honeycomb ObjectAnimator see animation definition in res/animator/animator_film_strip_up.xml
  */
 @Override
 public void startFilmStripUpAnimation() {
  ObjectAnimator set = (ObjectAnimator) AnimatorInflater.loadAnimator(mCtx, R.animator.animator_film_strip_up);
  set.setTarget(mFilmStrip);
  set.start();
 }

 /* (non-Javadoc)
  * @see com.macadamian.android.ottasee.animation.AnimationHelper#startFilmStripDownAnimation()
  * 
  * Using new Honeycomb ObjectAnimator see animation definition in res/animator/animator_film_strip_down.xml
  */
 @Override
 public void startFilmStripDownAnimation() {
  ObjectAnimator set = (ObjectAnimator) AnimatorInflater.loadAnimator(mCtx, R.animator.animator_film_strip_down);
  set.setTarget(mFilmStrip);
  set.start();
 }

 /* (non-Javadoc)
  * @see com.macadamian.android.ottasee.animation.AnimationHelper#startGridUpAnimation()
  */
 @Override
 public void startGridUpAnimation() {
  ObjectAnimator set = (ObjectAnimator) AnimatorInflater.loadAnimator(mCtx, R.animator.animator_grid_up);
  set.setTarget(mFilmStrip);
  set.addListener(new AnimatorListener() {
   
   @Override
   public void onAnimationStart(Animator animation) {    
    mFilmStrip.findViewById(R.id.the_grid).setVisibility(View.VISIBLE);
    mFilmStrip.findViewById(R.id.film_strip_list).setVisibility(View.GONE);
   }
   
   @Override
   public void onAnimationRepeat(Animator animation) {    
   }
   
   @Override
   public void onAnimationEnd(Animator animation) {
   }
   
   @Override
   public void onAnimationCancel(Animator animation) {    
   }
  });
  set.start();
 }

The beauty of implementing it this way is that in your AnimationControler you can create an AnimationHelper but then check your OS version and instantiate the correct sub class at run time.  Here's a clip from my AnimationControler constructor showing how I did this.

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

        // 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();
    }

As you can see, by setting up your animations like this we can keep our Activity code nice and clean, support multiple OS versions and make it easier for others to work with your animations code.  I hope this helps you keep your code clean!

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.

Animation Two Ways Part 1

If you've been writing Android code for a while, you've probably done some work with the Animation API.  You might have noticed that Google released a new Animation API for Honeycomb.  I'm not going to talk about the details of the Animation API's.  Instead I want to show you a technique I've used recently to help develop an app containing not only a lot of view animations, but also had to support both Honeycomb and previous versions of the Android OS

What I discovered early on was that it gets ugly quickly trying to support multiple versions of Android in your app.  You end up with a lot of version checks throughout your apps Activity's.  Add to this the relatively verbose Animation code and things start to get out of hand.

In my case, I have various screen elements that can be in various states of display.  Specifically I have a video chooser that can be in 3 states (closed, open, full screen) and a playlist selection panel that can be shown and hidden.  On top of that I need to be able to hide the UI when a video is played and show it again when it's paused or finished.


I'm going to show you a nice clean way to deal with all those Animations across multiple OS versions in 3 steps.

First lets create a class that that will contain state information about our UI.  So I define a boolean for each View that can be shown or hidden in my UI.  Then I create a static enum defining my 3 positions for my video chooser view.

public class UIState {

    public static enum HeaderState {
     CLOSED,
     OPEN,
     FULL
    }
    
    private boolean mShowActionBar = true;
    private boolean mShowPlaylists = false;

    private boolean mShowHeader = true;
    private boolean mShowFilmStrip = false;
    private boolean mShowGrid = false;

Then create getters and setters for those data members.  I created a few additional methods to copy and compare UIState objects, as well as a few special setters for "configuring" my header, film strip and grid booleans in specific configurations.

    /**
     * @param mShowHeader
     *            the mShowHeader to set
     */
    public void setShowHeader() {
        mShowHeader = true;
        mShowFilmStrip = false;
        mShowGrid = false;
    }

    /**
     * @param mShowFilmStrip
     *            the mShowFilmStrip to set
     */
    public void setShowFilmStrip() {
        mShowHeader = true;
        mShowFilmStrip = true;
        mShowGrid = false;
    }

    /**
     * 
     * @param mShowGrid
     */
    public void setShowGrid() {
        mShowHeader = true;
        mShowFilmStrip = false;
        mShowGrid = true;
    }

Finally I create a method that will return the current state of the video chooser based on the configuration of header, film strip and grid.

    /**
     * Method that returns the current state of the header (CLOSED,OPEN,FULL)
     */
    public HeaderState getHeaderState() {
     if (showFilmStrip()) {
      return HeaderState.OPEN;
     }
     if (showGrid()) {
      return HeaderState.FULL;
     }
     if (!showFilmStrip() && !showGrid()) {
      return HeaderState.CLOSED;
     }
     // should only hit this if there is a bug in how we are setting our UI state objects
     return HeaderState.CLOSED;
    }

In part 2 I will show you how to create an Animation helper super class that we'll subclass for Honeycomb and Gingerbread and lower.  This is where we will define our individual animations for our views.