Monday, January 19, 2015

Partial SlidingPaneLayout

Gmail has an interesting UI on tablet:

The side pane is always visible, showing icons when collapsed, cross fading to more details when expanded. How is it implemented?

My first observation is that the main pane slides when the side pane expands, so I know it is not a NavigationDrawer. Let's try a SlidingPaneLayout.

SlidingPaneLayout

<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">
  <TextView
      android:layout_width="240dp"
      android:layout_height="match_parent"
      android:background="@color/blue"
      android:text="@string/pane_1"/>
  <TextView
      android:layout_width="400dp"
      android:layout_height="match_parent"
      android:layout_weight="1"
      android:background="@color/light_blue"
      android:text="@string/pane_2"/>
</android.support.v4.widget.SlidingPaneLayout>

Looks good, except the main pane turns gray. Fortunately we can change the fade color to transparent.

SlidingPaneLayout layout = (SlidingPaneLayout) 
    findViewById(R.id.sliding_pane_layout);
layout.setSliderFadeColor(Color.TRANSPARENT);

Partial side pane

Now I want to make the side pane partially visible when collapsed. Took me a while (plus a shower) to figure that out, but once I did it was really simple: add margin to the main pane.

<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">
  <TextView
      android:layout_width="240dp"
      android:layout_height="match_parent"
      android:background="@color/blue"
      android:text="@string/pane_1"/>
  <TextView
      android:layout_width="400dp"
      android:layout_height="match_parent"
      android:layout_weight="1"
      android:layout_marginLeft="64dp"
      android:background="@color/light_blue"
      android:text="@string/pane_2"/>
</android.support.v4.widget.SlidingPaneLayout>

With the margin, the side pane peeks from below when collapsed.

Cross fade

Finally, the cross fade. I replaced the side pane with FrameLayout, the bottom view being the full pane and the top view being the partial pane.

<com.sqisland.android.CrossFadeSlidingPaneLayout
    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">
  <FrameLayout
      android:layout_width="240dp"
      android:layout_height="match_parent"
      android:background="@color/purple">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@string/full"/>
    <TextView
        android:layout_width="64dp"
        android:layout_height="match_parent"
        android:background="@color/blue"
        android:text="@string/partial"/>
  </FrameLayout>
  <TextView
      android:layout_width="400dp"
      android:layout_height="match_parent"
      android:layout_weight="1"
      android:layout_marginLeft="64dp"
      android:background="@color/light_blue"
      android:text="@string/pane_2"/>
</com.sqisland.android.CrossFadeSlidingPaneLayout>

Then I subclass SlidingPaneLayout to cross fade between the partial pane and the full pane on slide. To do that, I need to get the two panes.

@Override
protected void onFinishInflate() {
  super.onFinishInflate();

  if (getChildCount() < 1) {
    return;
  }

  View panel = getChildAt(0);
  if (!(panel instanceof ViewGroup)) {
    return;
  }

  ViewGroup viewGroup = (ViewGroup) panel;
  if (viewGroup.getChildCount() != 2) {
    return;
  }
  fullView = viewGroup.getChildAt(0);
  partialView = viewGroup.getChildAt(1);

  super.setPanelSlideListener(crossFadeListener);
}

Since SlidingPaneLayout already has the convention of specifying the side pane vs main pane by position, I also look for the partial pane and full pane by position. The first child of the SlidingPaneLayout is the side pane, its first child is the full pane, second child is the partial pane. I stash them in the fields fullView and partialView, which are used in the cross-fade listener.

private SimplePanelSlideListener crossFadeListener 
    = new SimplePanelSlideListener() {
  @Override
  public void onPanelSlide(View panel, float slideOffset) {
    super.onPanelSlide(panel, slideOffset);
    if (partialView == null || fullView == null) {
      return;
    }

    partialView.setVisibility(isOpen() ? View.GONE : VISIBLE);
    partialView.setAlpha(1 - slideOffset);
    fullView.setAlpha(slideOffset);
  }
};

Here, I change the alpha of the partial pane and the full pane depending on the slide offset. Since I don't want the partial pane to react to touch events when the layout is open, I set the partial pane to View.GONE. The same logic needs to be applied onLayout because devices with sufficient width (e.g. tablets) may start with the layout opened.

@Override
protected void onLayout(
    boolean changed, int l, int t, int r, int b) {
  super.onLayout(changed, l, t, r, b);

  if (partialView != null) {
    partialView.setVisibility(isOpen() ? View.GONE : VISIBLE);
  }
}

Here we go, a partially shown side pane that cross fades into a different view when expanded. Enjoy!

Source: https://github.com/chiuki/sliding-pane-layout

4 comments:

Inline coding questions will not be answsered. Instead, ask on StackOverflow and put the link in the comment.

  1. Brilliant. Thank you for the great trick.

    ReplyDelete
  2. Hi madam,thank you ,This is good and easy to me,which is help my project.

    ReplyDelete
  3. Thank you so much for making a post on this!

    ReplyDelete
  4. I will try to implement a menu like gmail has on tablets!! thx for the trick!
    really helpful

    ReplyDelete