Working with the SlidingPaneLayout

In the latest I/O 2013, Google showed a new app, Google Hangouts.
This app is designed around the SlidingPaneLayout, which is a new addition to the support library.

It provides and easy way to create two pane screens. You define the two panes and how much room they need to display. If the device has insufficient space to show both at once, the content pane will overlap the master pane and you can slide horizontally to reveal it; then swipe back to content.

Implementing the SlidingPaneLayout is simple. The components required are now included in the latest support library (release 13).

We can start with the layout file,and we'll add its tag to our layout XML file for an Activity.

<android.support.v4.widget.SlidingPaneLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/sliding_pane_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <!--
         The first child view becomes the left pane. When the combined
         desired width (expressed using android:layout_width) would
         not fit on-screen at once, the right pane is permitted to
         overlap the left.-->

    <fragment
        android:id="@+id/list_pane"
        android:name="it.gmariotti.android.examples.slidingpane.MyListFragment"
        android:layout_width="280dp"
        android:layout_height="match_parent"
        android:layout_gravity="left" />

    <!--
         The second child becomes the right (content) pane. In this
         example, android:layout_weight is used to express that this
         pane should grow to consume leftover available space when the
         window is wide enough. This allows the content pane to
         responsively grow in width on larger screens while still
         requiring at least the minimum width expressed by
         android:layout_width.  -->

    <fragment
        android:id="@+id/content_pane"
        android:name="it.gmariotti.android.examples.slidingpane.DetailFragment"
        android:layout_width="450dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:paddingLeft="16dp"
        android:paddingRight="16dp"  />

</android.support.v4.widget.SlidingPaneLayout>
Like the DrawLayout the SlidingPaneLayout contains two children, the first child is the Master or left pane and the second child is the content or detail pane.

For each pane you define the width that the pane requires. If the sum of these two widths exceed the width available then they will overlap.
Setting the layout_weight on the content pane to 1 tells it to grow to fill the available space when overlapping so layout_width really defines the minimum width which it will lay out at.

It is enough to get a simple SlidingPaneLayout working.

You can see below two cases.


Now we want to listen for opening and closing event to manage ActionBar and its icons and actions.
We can use a SimplePanelSlideListener.
public class MainActivity extends Activity {

  private SlidingPaneLayout mSlidingLayout;
  private ActionBar mActionBar;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mActionBar = getActionBar();
    mSlidingLayout = (SlidingPaneLayout) findViewById(R.id.sliding_pane_layout);

    mSlidingLayout.setPanelSlideListener(new SliderListener());
    mSlidingLayout.openPane();

 }
/**
 * This panel slide listener updates the action bar accordingly for each
 * panel state.
 */
private class SliderListener extends SlidingPaneLayout.SimplePanelSlideListener {

   @Override
   public void onPanelOpened(View panel) {
      Toast.makeText(panel.getContext(), "Opened", Toast.LENGTH_SHORT).show();
      panelOpened();
   }

   @Override
   public void onPanelClosed(View panel) {
      Toast.makeText(panel.getContext(), "Closed", Toast.LENGTH_SHORT).show();
      panelClosed();
   }

   @Override
   public void onPanelSlide(View view, float v) {}
}


We can use this panel slide listener to update the action bar accordingly for each panel state.
/**
 * 
 * @param panel
 */
private void panelClosed() {
   
   getFragmentManager().findFragmentById(R.id.content_pane).setHasOptionsMenu(true);
   getFragmentManager().findFragmentById(R.id.list_pane).setHasOptionsMenu(false);
}
This method is called when a sliding pane becomes slid completely closed.
In our example we would like to hidden optionmenus for left panel, and show only options for detail pane.

/**
 * 
 * @param panel
 */
private void panelOpened() {
   
   if (mSlidingLayout.isSlideable()) {
      getFragmentManager().findFragmentById(R.id.content_pane).setHasOptionsMenu(false);
      getFragmentManager().findFragmentById(R.id.list_pane).setHasOptionsMenu(true);
   } else {
      getFragmentManager().findFragmentById(R.id.content_pane).setHasOptionsMenu(true);
      getFragmentManager().findFragmentById(R.id.list_pane).setHasOptionsMenu(false);
   }
}
This method is called when a sliding pane becomes slid completely open. In the same way we would like to show only options for left pane when the pane is open.
When the slinding pane isn't slideable we would like to show the options for detail pane. In this case we can use the method mSlidingLayout.isSlideable()


We can use the same methods to show/hide the Up action.
/**
 * 
 * @param panel
 */
private void panelClosed() {
   mActionBar.setDisplayHomeAsUpEnabled(true);
   mActionBar.setHomeButtonEnabled(true);

   getFragmentManager().findFragmentById(R.id.content_pane).setHasOptionsMenu(true);
   getFragmentManager().findFragmentById(R.id.list_pane).setHasOptionsMenu(false);
}
/**
 * 
 * @param panel
 */
private void panelOpened() {
   mActionBar.setHomeButtonEnabled(false);
   mActionBar.setDisplayHomeAsUpEnabled(false);

   if (mSlidingLayout.isSlideable()) {
      getFragmentManager().findFragmentById(R.id.content_pane).setHasOptionsMenu(false);
      getFragmentManager().findFragmentById(R.id.list_pane).setHasOptionsMenu(true);
   } else {
      getFragmentManager().findFragmentById(R.id.content_pane).setHasOptionsMenu(true);
      getFragmentManager().findFragmentById(R.id.list_pane).setHasOptionsMenu(false);
   }
}


We can handle the up action with this code:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
  /*
   * The action bar up action should open the slider if it is currently
   * closed, as the left pane contains content one level up in the
   * navigation hierarchy.
   */
  if (item.getItemId() == android.R.id.home && !mSlidingLayout.isOpen()) {
     mSlidingLayout.openPane();
    return true;
  }
  return super.onOptionsItemSelected(item);
}
We can use global layout listener to fire an event after first layout occurs and then it is removed.
This gives us a chance to configure actionbar and optionsmenu.
@Override
protected void onCreate(Bundle savedInstanceState) {

         ......
       mSlidingLayout.getViewTreeObserver().addOnGlobalLayoutListener( 
                   new FirstLayoutListener());
 
}
 /**
  * This global layout listener is used to fire an event after first layout
  * occurs and then it is removed. This gives us a chance to configure parts
  * of the UI that adapt based on available space after they have had the
  * opportunity to measure and layout.
  */
 private class FirstLayoutListener implements ViewTreeObserver.OnGlobalLayoutListener {
    @Override
    public void onGlobalLayout() {

     if (mSlidingLayout.isSlideable() && !mSlidingLayout.isOpen()) {
        panelClosed();
     } else {
        panelOpened();
     }
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
        mSlidingLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
     } else {
        mSlidingLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
     }
    }
 }

Finally we want to handle click on list items. We can use a callback function.
public class MyListFragment extends ListFragment {

   ListFragmentItemClickListener iItemClickListener;

   /** An interface for defining the callback method */
   public interface ListFragmentItemClickListener {
     /**
      * This method will be invoked when an item in the ListFragment is
      * clicked
      */
     void onListFragmentItemClick(View view, int position);
   }
 
   /** A callback function, executed when this fragment is attached to an activity */
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try{
             // This statement ensures that the hosting activity implements 
             // ListFragmentItemClickListener 
             iItemClickListener = (ListFragmentItemClickListener) activity;
        }catch(Exception e){
             Toast.makeText(activity.getBaseContext(), "Exception",Toast.LENGTH_SHORT).show();
        }
    }
  
   @Override
   public void onListItemClick(ListView list, View view, int position, long id) {

    /**
     * Invokes the implementation of the method onListFragmentItemClick in
     * the hosting activity
     */
     iItemClickListener.onListFragmentItemClick(view, position);

   }
}
We can modify our Activity:
public class MainActivity extends Activity implements
  ListFragmentItemClickListener {

        @Override
        public void onListFragmentItemClick(View view, int position) {
                mActionBar.setTitle(MyListFragment.items[position]);
                mSlidingLayout.closePane();
        }

}
In our example, when we click on list item, we modify title and close left pane.


One last note:
SlidingPaneLayout is also marked as experimental in the documentation, so its future is uncertain.

EDIT: UPDATE BY Adam Powell
SPL is here to stay, at least for the foreseeable future. The "experimental" comment was a leftover from its, well, experimental days. :)

You can get code from GitHub:

Comments

Popular posts from this blog

AntiPattern: freezing a UI with Broadcast Receiver

NotificationListenerService and kitkat

How to centralize the support libraries dependencies in gradle