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.

Wednesday, November 30, 2011

Irregular ActionBar

So you've been working with the Honeycomb API's for a while and are comfortable with the new ActionBar.  Then your designer sends you this visual design...


Can you spot what is going on in that screenshot?  ActionBar is the rectangular area at the top of your Honeycomb app where you can put things like tabs, menus, drop down lists, etc.  But for all the styling you can apply to your ActionBar, it's still a rectangle.  If you want to add a bit of flare to your ActionBar and break out of that rectangular box.  Here's a neat trick.

Your visual designer just sent you the background resource for your apps ActionBar, and it's got some irregularities to it's shape, with some transparency and they expect your frame content to seamlessly scroll behind your ActionBar.  Here's what you do.

Step 1: Send it back and tell them the ActionBar is a rectangle! This rarely works.

Step 2: Bring the image into your favorite image editing software (I use Gimp).  Now you want to cut the image horizontally and save the top (rectangular) portion in one image file, and the bottom irregular portion in a second image file.

ActionBar header top (header_top.png)

ActionBar header bottom (header_bottom.png)
Step 3: Create a custom style that uses Widget.Holo.ActionBar as it's parent and set the header_top.png as the background drawable for your ActionBar.  Then make sure you add your style as a theme to your manifest file under application (android:theme="@style/MyTheme")  So far nothing too special, you've set a background image for your ActionBar.

(res/values/styles.xml)


    

    
    

Step 4: Modify your content layout file to include the bottom part of the ActionBar as an ImageView and use android:layout_alignParentTop="true" to make sure it is "attached" to the top of your parent view and looks like it's part of your ActionBar.  Note that your parent layout will need to be a RelativeLayout in order for this to work.

Now, if you run your app and you can't see the bottom part of the ActionBar that you just added, you probably put your ImageView before your content in your layout.  Even though you've told your ImageView to alignParentTop, if you create it before your content view, it will be covered up and you actually want it to be drawn last in your view.  So move it to be the last view in your layout.  That should do it!  Check out your awesome irregular ActionBar!

(res/layout/main.xml)


 
    
    
     
     
    

 

Credit to my designer Stefan for the awesome idea!

Saturday, August 20, 2011

Renderscript: Components of a 3d scene

For anyone looking into renderscript who has never done any 3d graphics before, this is for you!  There is a lot of new terminology, and a whole lot of math involved.  Scared yet?  Don't be, the set of components involved in rendering a 3d scene is finite and well defined.

Rendering Pipeline
Rendering a scene simple means that you are transforming a 3D field of objects into a 2D image.  I found this easy to understand drawing that should help you to visualize the rendering pipeline.  Credit to these Waterloo university course notes here.



Models
This is what it's all about.  Objects in a scene, comprised of primitives.  There is some key terminology used when we discuss how to build an object in 3d space.

  • Plane This is a a one dimensional slice in space that runs along one of three axis, x, y, or z.
  • Coordinate This is an element that is used to describe a precise point in a single plane.
  • Vertex This is a set of coordinates that are used to describe a precise point in a 3d space.  A vertex is described in the x, y, and z planes.  Typically x represents the horizontal plane, y represents the vertical plane and z represents the depth plane.
  • Primitive A primitive is an atomic geometric object.  ie: points, planes, lines, circles, triangles, spline curves.  For more on primitives see this wiki page.
An object is composed of a set of vertices, and a type of geometric primitive that describes how the vertices should be connected to make a shape.

A models coordinate system (MCS) needs to be transformed into your worlds coordinate system (WCS) using a model transformation matrix.

A 3D world scene will contain your entire scene.  However, the user may not need to see your entire scene on their device.  In order to see smaller sections of your world scene, you need to create a view scene by transforming your world scene to your view scene using a view transformation matrix.  This is a transformation from your WCS to your view coordinate system (VCS).

In order to display your 3D view of your scene on your 2D screen, first we need to calculate what parts of your 3D view will actually be seen.  This is done through a planer projection.  There are two types of 3D planer projections, the one we would normally be using in a renderscript context will be perspective projection.  Basically perspective projection is the idea that objects that are further away look smaller than objects that are closer to the viewer.

The components we need to define in renderscript in order to project a 3D scene onto a 2D screen are as follows:
  • Camera The camera is positioned somewhere in the 3D scene and oriented towards a particular part of the scene.  We also need to define the cameras field of view, which determines how much of the scene is seen by the camera.  In order to actually see something on your 2D screen, you will finally need to define a plane with which we project our scene through.  You can see some drawings that depict this description here.
  • Model This is your 3D model that you want projected.
  • Plane through which your camera projects your scene.
Once your scene is projected into a normalized device coordinate system (NDCS) it is ready for rasterization into the final 2D image in in your screens coordinate system (SCS).  The process of rasterization involves transforming your 2D scene into it's pixel representation on your screen.

This is just a high level view of the basics of 3D rendering, but you need to understand this before digging deeper into the details of how all of this works.  It involves a lot of complicated vector and matrix math.  For more details, have a look at these wikipedia articles.

Thursday, August 18, 2011

Renderscript: A First Look

What is Renderscript?
Renderscript is a C99 based language used for 3D rendering and compute API's at the native level on an Android device.  It's a new API provided in the Honeycomb SDK level 11.


When should I use Renderscript?
When you need a high performance graphics rendering solution in your application and/or a high performance compute language for doing resource intensive operations.  Renderscript doesn't require you to write any JNI code to interface with your renderscript code.  The interface between your renderscript and your SDK code is generated for you.  The trade offs are,  you have a reduced feature set compared to using OpenGL with the NDK and you can't allocate memory in your renderscript, it must be allocated from your SDK code.


How is it different than OpenGL?
OpenGL can be used in two different ways in Android.
  • OpenGL API's can be used to produce 3d graphics using the Android SDK.  This is a good choice when performance isn't critical and you don't need the added complexity of using the NDK.
  • OpenGL with the NDK offers you the most flexability and features of OpenGL and is a good choice for graphics intense applications.  However, you have the added complexity of the NDK and you are required to write your own JNI to use your NDK code.
Renderscript components
An app that uses renderscript is composed of three required components.


Renderscript component
This is your actual Renderscript code contained in a .rs file. Renderscript code is C99 based but only has a small set of functionality so as to ensure portability across different CPUs.  You can also create renderscript header files (.rsh) where you can define your structures and function stubs.  You can have multiple .rs and .rsh files.  You can find the provided renderscript header files in the <sdk_root>/platforms/android-11/renderscript directory of the Android SDK.


Reflected component
This component is set of classes generated for you by the Android build tools and are used to interface with your renderscript component.  This includes hooks to the functions and variables in your renderscript code.  Also provided are methods for allocating memory to the pointers defined in your renderscript.
For each .rs file you've created, a class will be generated with the name ScriptC_renderscript_filename. This is the java representation of your .rs that you can call from your SDK code. Code that will be reflected in this generated class is as follows:
  • Non-static functions
  • Non-static global variables are accessed with generated set_ and get_ methods, if a variable is marked as const in your renderscript, then only a get_ method is generated.  It should be noted that the last value set to a variable through the reflected set_ method, is the value that will be returned through the get_ method, even if that variable is changed inside your renderscript during execution.
  • Global pointers are accessed in the reflected component by special methods generated called bind_pointer_name. This will allocate memory for the pointer both in the SDK code and in your renderscript.
Android Framework component
This is the usual Android framework where you have access to the android.renderscript classes as well as the usual things like Activity lifecycle, touch events, etc.  You will access your renderscript from here through the reflected components.

Sunday, April 17, 2011

Honeycomb Tip #1: PreferenceFragment

One of the biggest new changes in the Android SDK is the addition of the Fragment API. The idea is that you can define multiple components of your application as Fragments and then link them together within your Activity. Building your Activity with Fragments allows you to layout your Activity's components in different ways depending on your screen size and orientation..

Ugly PreferenceScreen with default grey on black?
Today I want to talk specifically about PreferenceFragment. While porting one of my apps to Honeycomb, I discovered some interesting things about the way preferences can be displayed. If you have a lot of preferences to display, you can define preference headers which allows you to categorize your preferences logically under, well, headers. This is really nice for larger apps with a lot of preferences, but what if you only have a few preferences but still want to break some of them out into their own PreferenceScreens? If you're like me and you're porting your apps to Honeycomb, you might have just created a PreferenceFragment and tried to use your existing preference.xml layout. This will work for the top level of your preferences and they will look as expected inside your Fragment view container. However, if you select one of your nested PreferenceScreens, you see that it displays it outside of your Fragment view container and frankly looks really really bad. Not to mention if you hit the back key it won't take you back to the top level of preferences, instead it will back your Activity out to it's last state (or back to the home screen). Eeek!

Lets start with containing those nested PreferenceScreens within your defined Fragment view container. First you will need to create separate xml files for each PreferenceScreen. Like this..

prefs.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen  xmlns:android="http://schemas.android.com/apk/res/android">
<ListPreference
android:key="nzb_server_choice" 
android:title="@string/prefs_server_choice"
android:summary="@string/prefs_server_choice_sum"
android:entries="@array/nzb_server_choice"
android:entryValues="@array/nzb_server_choice"
android:defaultValue="@string/sv_name_hellanzb" />
<PreferenceScreen
android:title="@string/prefs_hella_settings"
android:key="@string/hella_prefs_key"
android:fragment="com.brockoli.android.nzbmobilepro.HomeActivity$HellaPrefsFragment">
</PreferenceScreen>
</PreferenceScreen>

hella_prefs.xml
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
  android:title="@string/prefs_hella_settings">
<EditTextPreference
  android:key="hella_server_url"
  android:title="@string/prefs_sv_url_title"
  android:summary="@string/prefs_hella_server_sum"
  android:defaultValue="@string/prefs_hella_server_default"/>
<EditTextPreference
  android:key="hella_server_port"
  android:title="@string/prefs_sv_port_title"
  android:summary="@string/prefs_hella_server_port_sum"
  android:defaultValue="@string/prefs_hella_server_port_default"/>
<EditTextPreference
  android:key="hella_server_password"
  android:title="@string/prefs_hella_server_pw_title"
  android:summary="@string/prefs_hella_server_pw"
  android:password="true"/>
</PreferenceScreen>

Take note of how I defined my PreferenceScreen for hella settings in prefs.xml. You need to specify android:key as well as android:fragment. This way you can key in on the specific PreferenceScreen the user selects from your code and handle it as a Fragment. Now for the code...
public static class PrefsFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.prefs);
}
@Override
public boolean onPreferenceTreeClick(PreferenceScreen prefScreen, Preference pref) {
super.onPreferenceTreeClick(prefScreen, pref);
if (pref.getKey().equals(getActivity().getResources().getString(R.string.hella_prefs_key))) {
// Display the fragment as the main content.
getFragmentManager().beginTransaction().replace(R.id.details, new HellaPrefsFragment())
.addToBackStack(null).commit();
return true;
         }
         return false;
     }
}
public static class HellaPrefsFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.hella_prefs);
}
}
Top level PreferenceScreen
In your code, you will need to define separate PreferenceFragment's for each PreferenceScreen you have nested. The trick to having the nested PreferenceScreen display within the Fragment view container that you specified is to catch when the user selects each PreferenceScreen. You can do this by overriding onPreferenceTreeClick callback to the parent PreferenceFragment. In that method, simply check the passed in preference's key (which you set in it's xml file) and replace the Fragment with the selected PreferenceFragment. Now you can contain all your PreferenceScreens within the same Fragment view container.


Finally, you should handle the back button correctly so the user can navigate back up through your preferences. To do this, simple add the parent PreferenceFragment to the fragment backstack. To do that you simple call the addToBackStack method of the transaction after replacing the parent fragment.

Hope this helps to clean up preferences when porting your apps to Honeycomb!

Second level PreferenceScreen within the Fragment view container

Tuesday, January 25, 2011

Pass me the Data

You've written the Hello Android app, then created your own simple app. Now you've created a much more complicated app with several Activities. You may need to be able to pass data between your Activities. In this post, I will take you from the basics of passing simple data types between Activities right up to making your own custom objects Parcelable so they can also be passed along with the same ease as a simple data type.

Before we start passing data around, first you will need to understand how an Activity is launched. If you're reading this, then you are probably familiar with Intents. If not, then you should not pass go, and definitely not collect $200. Instead go here and learn about Android Application Fundamentals.

To launch a new activity, you would create an Intent and pass it to the Activity method startActivity().

Intent newIntent = new Intent(Context, Class);
startActivity(newIntent);

Now lets say you have a ListActivity and whenever a user taps on an entry in the list, you want to launch a new Activity that does something with the data from that list entry. The most basic scenario for passing data is to attach an extra to the Intent in the form of

newIntent.putExtra(name,value);

Where name is a String that is used to "tag" your data and value is the actual data object you are passing to the intended Activity. Value can be many different types, including String, int, Boolean, etc. You can see the complete list of putExtra methods here.

Here's an example from the calling Activity.

Intent newIntent = new Intent(this, SomeActivity.class);
newIntent.putExtra("MAGIC_NUMBER", 42);
startActivity(newIntent);

Now on the called Activity side (receiving the Intent), in your Activities onCreate() method, you will need to retrieve the extra data from the Intent. You can do this by calling getIntent() to get the Intent that started the Activity, then getIntExtra(name, default). This is if you passed an int value, if you passed a different type, you would use the corresponding method for that type. For example..

int magic_number = getIntent().getIntExtra("MAGIC_NUMBER", 420);

Note that the default value is used if the tag "MAGIC_NUMBER" had no value assigned to it.

This is great if you are just passing one piece of data using a basic data type, but if you are passing more data than just a single type, you might want to consider using Bundle.  Basically a Bundle is just a mapping of tag-data pairs grouped together into one passable object.

Lets say you had a contact list and when the user taps a contact, you want to pass the name, id and phone number to the called Activity.  You could do so like this.

Intent newIntent = Intent(this, ContactDetails.class);
Bundle extras = new Bundle();
extras.putString("CONTACT_NAME", name);
extras.putInt("CONTACT_ID", id);
extras.putString("CONTACT_NUMBER", number);
newIntent.putExtras(extras);
startActivity(newIntent);

And on the receiving Activity...

Bundle extras = getIntent().getExtras();
String name = extras.getString("CONTACT_NAME");
int id = extras.getInt("CONTACT_ID");
String number = extras.getString("CONTACT_NUMBER");

Now lets say you've created your own data class with several different types of data representing the class.

public class Dog extends Object {
  private String mType;
  private String mName;
  private int mId;
 
  public Dog(String type, String name, int id) {
    mType = type;
    mName = name;
    mId = id;
  } 

You could create an instance of Dog and pass each piece of data separately by adding 4 tag-data pairs to a Bundle and passing the Bundle, but a smarter way of doing this is to implement the Parcelable interface in your Dog class.  The Parcelable interface has 2 methods you need to implement, describeContents() and writeToParcel(Parcel, int).


    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mType);
        dest.writeString(mName);
        dest.writeInt(id);
    }

In this method you must add each of your data types to the Parcel with the correct write method for it's data type.

If your class will have child classes, you can use the describeContents() method to differentiate between child classes so that when you unparcel your parceled object, you can create the correct child object.

This is only half of the solution.  Now that you have the facilities to write your object to a Parcel, you also need to be able to rebuild your object in the called Activity.  First you will need a new constructor for your class.  One that takes a Parcel object and can populate it's data members from it.


    public Dog(Parcel dest) {
        mType = dest.readString();
        mName = dest.readString();
        mId = dest.readInt();
    }

Note the order.  You must read your data types from your Parcel in the same order you wrote them.  Essentially you're flattening your data into one data stream, then reading it back on the other side.

Finally, you will need to create a Parcelable.Creator in your class that will trigger your new Parcelable constructor when needed.


    public static final Parcelable.Creator<LocalContact> CREATOR = new Parcelable.Creator<LocalContact>() {
        @Override
        public LocalContact createFromParcel(Parcel source) {
            return new LocalContact(source);
        }

        @Override
        public LocalContact[] newArray(int size) {
            return new LocalContact[size];
        }
    };

One last thing I'll leave you with is the concept of chaining your Parcelable objects.  Lets say your Dog class has a custom object as one of it's data members.  You will want to make sure that class also implements the Parcelable interface.  In order to support parcelizing your custom object inside your Dog class, you will need to change 2 methods.

First Dog's writeToParcel(Parcel, int) method needs to tell your custom object to write itself to the parcel.  To do this we can call writeParcelable(Object, int).  This method will invoke your custom classes writeToParcel method.

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mType);
        dest.writeString(mName);
        dest.writeInt(id);
        dest.writeParcelable(dogTag, flags);
    }

Here dogTag would be your custom class that could implement say, a phone number and address for where the dog lives.

Second Dog's Parcel constructor.  Here we will call readParcelable(Classloader) to get the custom objects parceled data.

    public Dog(Parcel dest) {
        mType = dest.readString();
        mName = dest.readString();
        mId = dest.readInt();
        dogTag = dest.readParcelable(DogTag.class.getClassLoader());
    }

This should get you passing your data around to your activities!