Android

The good, the bad, and the ugly: Working with Android's child fragment manager and custom transitions

William J. Francis presents a workaround for an issue that he suspects will plague Android developers for some time: applying custom transitions to fragments that host child fragments.

 

android-logo-generic-hai.jpg
 

With the advent of Android's fragment support library, the introduction of nested fragments, and fragment transitions, there isn't a good reason to write a new application and not use fragments. Android's fragment model provides superior flexibility when it comes to providing an optimal user experience across devices, with displays ranging from less than 4 inches to over 10. However, with so much work put into allowing fragments to run across such a variety of devices, the APIs still have some rough edges.

One particularly frustrating issue for me was applying custom transitions to fragments that hosted child fragments. In other words, my activity hosted a fragment, and that fragment hosted a child fragment. Whenever I did a replace of the fragment at the activity, the UI framework dropped the child view before the animation happened. The result was a jarring white flash every time I tried to slide in a new fragment (Figure A). Not pretty.

Figure A

 

 

After several hours of trial and error, I came up with a workable solution (Figure B). 

Figure B

 

 

The code below demonstrates my approach in the most stripped down way I could think of to present the material; that said, it is more advanced than most of my TechRepublic tutorials. If you find yourself struggling, or aren't familiar with fragments, try starting with my introduction to fragments

1. Create a new Android project in Eclipse. Target Android 2.2 or better.

2. Right-click your newly created project in the Eclipse package explorer, find Android Tools, and choose Add Support Library.

3. In the /res folder, create a new folder called /anim. This is where we'll add our slide in translation animation.

 

frag_slide_in_from_bottom.xml
<?xml version="1.0" encoding="utf-8"?>
 <set xmlns:android="http://schemas.android.com/apk/res/android">
   <translate android:fromYDelta="100%p" android:toYDelta="0%p" 
    android:interpolator="@android:anim/overshoot_interpolator"
    android:fillAfter="true"
 android:duration="500"/>
</set>
 

4. In our /layout folder we have three layouts: one for the activity, one for the parent fragment, and one for the child fragment. The layouts are pretty contrived for the purpose of this demonstration; they largely consist of a frame to hold a subsequent layout.

 

activity_main.xml
<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:id="@+id/container_activity" >

    <FrameLayout
        android:id="@+id/content_activity"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    
    <Button 
        android:id="@+id/button_activity"
        android:layout_centerHorizontal="true"
        android:layout_alignParentBottom="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Swap Fragments" />
    
</RelativeLayout>

parent_fragment.xml
<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" >

    <FrameLayout
        android:id="@+id/content_parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</RelativeLayout>

child_fragment.xml
<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">
    
    <TextView 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="30sp"
        android:textStyle="bold"
        android:textColor="#ffffffff"
        android:layout_centerInParent="true"
        android:id="@+id/text_child"/>
    
</RelativeLayout>
 

5. We're ready to start the actual programming. To keep things as simple as possible, I included all of the code in a single java file, which means I used a couple of inner classes where I might not otherwise have.  Keep in mind I'm trying to demonstrate the workaround with as little code as possible. The first thing we need to do is wire up the on click handler so that when the button gets pressed, the fragment (and by proxy, its child fragment) gets swapped out.

 

MainActivity.java
package com.authorwjf.fragmanager;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;

public class MainActivity extends FragmentActivity {
	
	private static final String NAME = "name";
	private static final String COLOR = "color";
	private static final String FRAG_1 = "FRAGMENT NUMBER ONE";
	private static final String FRAG_2 = "FRAGMENT NUMBER TWO";
	private int currentFrag;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		
		requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 
                                WindowManager.LayoutParams.FLAG_FULLSCREEN);
        
        setContentView(R.layout.activity_main);
        
		currentFrag = 0;
		
		findViewById(R.id.button_activity).setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				if (currentFrag<2) {
					currentFrag = 2;
					ParentFragment frag2 = new ParentFragment();
					Bundle fragTwoArgs = new Bundle();
					fragTwoArgs.putString(NAME, FRAG_2);
					fragTwoArgs.putInt(COLOR, Color.BLUE);
					frag2.setArguments(fragTwoArgs);
					FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
				    transaction.setCustomAnimations(R.anim.frag_slide_in_from_bottom, 0);
				    transaction.replace(R.id.content_activity, frag2);
				    transaction.commit();
				} else {
					currentFrag = 1;
					ParentFragment frag1 = new ParentFragment();
					Bundle fragOneArgs = new Bundle();
					fragOneArgs.putString(NAME, FRAG_1);
					fragOneArgs.putInt(COLOR, Color.RED);
					frag1.setArguments(fragOneArgs);
					FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
					transaction.setCustomAnimations(R.anim.frag_slide_in_from_bottom, 0);
				    transaction.replace(R.id.content_activity, frag1);
				    transaction.commit();
				}
			}
			
		});
		
		findViewById(R.id.button_activity).performClick();
	}
	
	
}
 

6. Add in our two inner classes, and then you can go ahead and run the code. It should execute just fine; however, the transitions between screens are the pits.

 

MainActivity.java
static public class ParentFragment extends Fragment {
		
		public ParentFragment() {
			
		}
		 
		@Override
		 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
			View root = inflater.inflate(R.layout.parent_fragment, null);
			ChildFragment childFrag = new ChildFragment();
			childFrag.setArguments(getArguments());
	        this.getChildFragmentManager().beginTransaction().add(R.id.content_parent, childFrag).commit();
	        return root;
			 
		 }
		
	}

	static public class ChildFragment extends Fragment {
		
		public ChildFragment() {
			
		}
		
		@Override
		 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
			View root = inflater.inflate(R.layout.child_fragment, null);
	        TextView tv = (TextView) root.findViewById(R.id.text_child);
	        tv.setText(getArguments().getString(NAME));
	        root.setBackgroundColor(getArguments().getInt(COLOR));
	        return root;
			 
		 }
		
	}
 

7. The workaround involves grabbing a screen shot of the outgoing child fragment just prior to the transition and assigning it as the background to the parent activity. First, add the following function to your main activity.

 

MainActivity.java
private Drawable getScreenShot() {
		View view = getWindow().getDecorView();
        view.setDrawingCacheEnabled(true);
        view.buildDrawingCache();
        Bitmap drawingCache = view.getDrawingCache();
        Rect frame = new Rect();
        getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
        int statusBarHeight = frame.top;
        Bitmap b = Bitmap.createBitmap(drawingCache, 0, statusBarHeight, drawingCache.getWidth(), drawingCache.getHeight() - statusBarHeight);
        view.destroyDrawingCache();
        return new BitmapDrawable(getResources(),b);
    }
 

8. Go back to the anonymous click handler defined in step 5 and insert the following line at the top of the function just before the line that reads: "if (currentFrag<2) {").

 

MainActivity.java
if (currentFrag>0) {
	findViewById(R.id.container_activity).setBackgroundDrawable(getScreenShot());
}
 

I know this tutorial is a bit complicated, but it solves a problem that I suspect will continue to plague Android developers for some time to come. I recommend downloading the entire project and importing it into Eclipse so you have a complete copy in your toolbox if you need it. 

 

About

William J Francis began programming computers at age eleven. Specializing in embedded and mobile platforms, he has more than 20 years of professional software engineering under his belt, including a four year stint in the US Army's Military Intellige...

0 comments

Editor's Picks