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!
Inline coding questions will not be answsered. Instead, ask on StackOverflow and put the link in the comment.
Brilliant. Thank you for the great trick.
ReplyDeleteHi madam,thank you ,This is good and easy to me,which is help my project.
ReplyDeleteThank you so much for making a post on this!
ReplyDeleteI will try to implement a menu like gmail has on tablets!! thx for the trick!
ReplyDeletereally helpful