Creating your own sliding menu like Facebook with Material design theme in Android

    There is a library which allows Android developers to easily create applications with slide-in menus like Facebook or LinkedIn made by Jeremy Feinstein, it has been widely used and become very popular. I also had a post about this library in order to guiding my readers to use it. The disadvantages of it is not update the new Android design technology (the last commit was 2 years ago), so if you implement it in your project, you cannot make your app available in Material design style - the new flat-design technology from Google.
    Dealing with this situation, we should find out the way to creating a sliding menu ourself. This post is a tutorial for you about this problem with using some features in Material design technology.
    DEMO VIDEO:

Prerequisites

    After starting a new Android project, make sure that you use a "no Action Bar" theme from Appcompat library with your project:
styles.xml
<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

</resources>

Making a sliding layout

    The most important work is designing a "sliding layout", it contains 2 parts: menu view (on the left) and main view (content view). We will let the menu view hold still, while moving the content view. The content view usually sits on top of the menu view and cover the entire screen:
    Name this layout class is SlidingLayout, provide constructors first:
public class SlidingLayout extends LinearLayout {

    public SlidingLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SlidingLayout(Context context) {
        super(context);
    }
}
    Now, we'll override some necessary methods. the first is onAttachedToWindow(), called when SlidingLayout is attached to window. At this point it has a Surface and will start drawing. Note that this function is guaranteed to be called before onDraw(). Here we set child views to our view and content variable:
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();

        // Get our 2 children views
        menu = this.getChildAt(0);
        content = this.getChildAt(1);

        // Attach View.OnTouchListener
        content.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return onContentTouch(v, event);
            }
        });

        menu.setVisibility(View.GONE);
    }
    As you can see, we'll handle touching event (gesture) from user in this method to. There are 3 actions we must detect: ACTION_UP, ACTION_DOWN and ACTION_MOVE. When user drag in the content view, the main layout will be scrolled and the menu is displayed a part or whole. This is onContentTouch() method:
public boolean onContentTouch(View v, MotionEvent event) {
        // Do nothing if sliding is in progress
        if (currentMenuState == MenuState.HIDING || currentMenuState == MenuState.SHOWING)
            return false;

        // getRawX returns X touch point corresponding to screen
        // getX sometimes returns screen X, sometimes returns content View X
        int curX = (int) event.getRawX();
        int diffX = 0;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                prevX = curX;
                return true;

            case MotionEvent.ACTION_MOVE:
                // Set menu to Visible when user start dragging the content View
                if (!isDragging) {
                    isDragging = true;
                    menu.setVisibility(View.VISIBLE);
                }

                // How far we have moved since the last position
                diffX = curX - prevX;

                // Prevent user from dragging beyond border
                if (contentXOffset + diffX <= 0) {
                    // Don't allow dragging beyond left border
                    // Use diffX will make content cross the border, so only translate by -contentXOffset
                    diffX = -contentXOffset;
                } else if (contentXOffset + diffX > sldingLayoutWidth - menuRightMargin) {
                    // Don't allow dragging beyond menu width
                    diffX = sldingLayoutWidth - menuRightMargin - contentXOffset;
                }

                // Translate content View accordingly
                content.offsetLeftAndRight(diffX);

                contentXOffset += diffX;

                // Invalite this whole Slidinglayout, causing onLayout() to be called
                this.invalidate();

                prevX = curX;
                lastDiffX = diffX;
                return true;

            case MotionEvent.ACTION_UP:
                // Start scrolling
                // Remember that when content has a chance to cross left border, lastDiffX is set to 0
                if (lastDiffX > 0) {
                    // User wants to show menu
                    currentMenuState = MenuState.SHOWING;

                    // Start scrolling from contentXOffset
                    menuScroller.startScroll(contentXOffset, 0, menu.getLayoutParams().width - contentXOffset,
                            0, SLIDING_DURATION);
                } else if (lastDiffX < 0) {
                    // User wants to hide menu
                    currentMenuState = MenuState.HIDING;
                    menuScroller.startScroll(contentXOffset, 0, -contentXOffset,
                            0, SLIDING_DURATION);
                }

                // Begin querying
                menuHandler.postDelayed(menuRunnable, QUERY_INTERVAL);

                // Invalite this whole Slidinglayout, causing onLayout() to be called
                this.invalidate();

                // Done dragging
                isDragging = false;
                prevX = 0;
                lastDiffX = 0;
                return true;

            default:
                break;
        }
        return false;
    }
    So the main idea of sliding menu is to change contentXOffset and call offsetLeftAndRight for the content to move it.
    We create an enum to control sliding state:
private enum MenuState {
        HIDING, //slidingmenu is collapsing
        HIDDEN, //sliding menu is hidden
        SHOWING, //sliding menu is expanding
        SHOWN, //sliding menu is completely shown
    }
    Using it in toggleMenu() method, allow us to toggle menu:
public void toggleMenu() {
        // Do nothing if sliding is in progress
        if (currentMenuState == MenuState.HIDING || currentMenuState == MenuState.SHOWING)
            return;

        switch (currentMenuState) {
            case HIDDEN:
                currentMenuState = MenuState.SHOWING;
                menu.setVisibility(View.VISIBLE);
                menuScroller.startScroll(0, 0, menu.getLayoutParams().width,
                        0, SLIDING_DURATION);
                break;
            case SHOWN:
                currentMenuState = MenuState.HIDING;
                menuScroller.startScroll(contentXOffset, 0, -contentXOffset,
                        0, SLIDING_DURATION);
                break;
            default:
                break;
        }

        // Begin querying
        menuHandler.postDelayed(menuRunnable, QUERY_INTERVAL);

        // Invalite this whole SlidingLayout, causing onLayout() to be called
        this.invalidate();
    }
    Overriding onMeasure(), we compute menuRightMargin, this variable is the amount of right space the menu should not occupy. In this case, we want the menu to take up 85% amount of the screen width:
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        sldingLayoutWidth = MeasureSpec.getSize(widthMeasureSpec);
        //Sliding menu will take 85% screen size when completely shown
        menuRightMargin = sldingLayoutWidth * 15 / 100;
    }
    The last important method is onLayout(), this is called from layout when this view should assign a size and position to each of its children. This is where we position the menu and content view:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        // True if SlidingLayout's size and position has changed
        // If true, calculate child views size
        if (changed) {
            // Note: LayoutParams are used by views to tell their parents how they want to be laid out
            // content View occupies the full height and width
            LayoutParams contentLayoutParams = (LayoutParams) content.getLayoutParams();
            contentLayoutParams.height = this.getHeight();
            contentLayoutParams.width = this.getWidth();

            // menu View occupies the full height, but certain width
            LayoutParams menuLayoutParams = (LayoutParams) menu.getLayoutParams();
            menuLayoutParams.height = this.getHeight();
            menuLayoutParams.width = this.getWidth() - menuRightMargin;
        }

        // Layout the child views    
        menu.layout(left, top, right - menuRightMargin, bottom);
        content.layout(left + contentXOffset, top, right + contentXOffset, bottom);
    }
    There are all important methods, adding some necessary features for our own purpose, we have full code for SlidingLayout class:
SlidingLayout.java
package info.devexchanges.slidingmenu;

import android.content.Context;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Interpolator;
import android.widget.LinearLayout;
import android.widget.Scroller;

public class SlidingLayout extends LinearLayout {

    // Duration of sliding animation, in miliseconds
    private static final int SLIDING_DURATION = 500;

    // Query Scroller every 16 miliseconds
    private static final int QUERY_INTERVAL = 16;

    // Sliding width
    int sldingLayoutWidth;

    // Sliding menu
    private View menu;

    // Main content
    private View content;

    // menu does not occupy some right space
    // This should be updated correctly later in onMeasure
    private static int menuRightMargin = 0;

    // The state of menu
    private enum MenuState {
        HIDING,
        HIDDEN,
        SHOWING,
        SHOWN,
    }

    // content will be layouted based on this X offset
    // Normally, contentXOffset = menu.getLayoutParams().width = this.getWidth - menuRightMargin
    private int contentXOffset;

    // menu is hidden when initializing
    private MenuState currentMenuState = MenuState.HIDDEN;

    // Scroller is used to facilitate animation
    private Scroller menuScroller = new Scroller(this.getContext(),
            new EaseInInterpolator());

    // Used to query Scroller about scrolling position
    // Note: The 3rd paramter to startScroll is the distance
    private Runnable menuRunnable = new MenuRunnable();
    private Handler menuHandler = new Handler();

    // Previous touch position
    int prevX = 0;

    // Is user dragging the content
    boolean isDragging = false;

    // Used to facilitate ACTION_UP 
    int lastDiffX = 0;

    public SlidingLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SlidingLayout(Context context) {
        super(context);
    }

    // Overriding LinearLayout core methods
    // Ask all children to measure themselves and compute the measurement of this
    // layout based on the children
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        sldingLayoutWidth = MeasureSpec.getSize(widthMeasureSpec);
        //Sliding menu will take 85% screen size when completely shown
        menuRightMargin = sldingLayoutWidth * 15 / 100;
    }

    // This is called when SlidingLayout is attached to window
    // At this point it has a Surface and will start drawing. 
    // Note that this function is guaranteed to be called before onDraw
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();

        // Get our 2 children views
        menu = this.getChildAt(0);
        content = this.getChildAt(1);

        // Attach View.OnTouchListener
        content.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return onContentTouch(v, event);
            }
        });

        menu.setVisibility(View.GONE);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        // True if SlidingLayout's size and position has changed
        // If true, calculate child views size
        if (changed) {
            // Note: LayoutParams are used by views to tell their parents how they want to be laid out
            // content View occupies the full height and width
            LayoutParams contentLayoutParams = (LayoutParams) content.getLayoutParams();
            contentLayoutParams.height = this.getHeight();
            contentLayoutParams.width = this.getWidth();

            // menu View occupies the full height, but certain width
            LayoutParams menuLayoutParams = (LayoutParams) menu.getLayoutParams();
            menuLayoutParams.height = this.getHeight();
            menuLayoutParams.width = this.getWidth() - menuRightMargin;
        }

        // Layout the child views    
        menu.layout(left, top, right - menuRightMargin, bottom);
        content.layout(left + contentXOffset, top, right + contentXOffset, bottom);
    }

    // Custom methods for SlidingLayout
    // Used to show/hide menu accordingly
    public void toggleMenu() {
        // Do nothing if sliding is in progress
        if (currentMenuState == MenuState.HIDING || currentMenuState == MenuState.SHOWING)
            return;

        switch (currentMenuState) {
            case HIDDEN:
                currentMenuState = MenuState.SHOWING;
                menu.setVisibility(View.VISIBLE);
                menuScroller.startScroll(0, 0, menu.getLayoutParams().width,
                        0, SLIDING_DURATION);
                break;
            case SHOWN:
                currentMenuState = MenuState.HIDING;
                menuScroller.startScroll(contentXOffset, 0, -contentXOffset,
                        0, SLIDING_DURATION);
                break;
            default:
                break;
        }

        // Begin querying
        menuHandler.postDelayed(menuRunnable, QUERY_INTERVAL);

        // Invalite this whole SlidingLayout, causing onLayout() to be called
        this.invalidate();
    }

    // Query Scroller
    protected class MenuRunnable implements Runnable {
        @Override
        public void run() {
            boolean isScrolling = menuScroller.computeScrollOffset();
            adjustContentPosition(isScrolling);
        }
    }

    // Adjust content View position to match sliding animation
    private void adjustContentPosition(boolean isScrolling) {
        int scrollerXOffset = menuScroller.getCurrX();

        // Translate content View accordingly
        content.offsetLeftAndRight(scrollerXOffset - contentXOffset);

        contentXOffset = scrollerXOffset;

        // Invalite this whole Slidinglayout, causing onLayout() to be called
        this.invalidate();

        // Check if animation is in progress
        if (isScrolling)
            menuHandler.postDelayed(menuRunnable, QUERY_INTERVAL);
        else
            this.onMenuSlidingComplete();
    }

    // Called when sliding is complete
    private void onMenuSlidingComplete() {
        switch (currentMenuState) {
            case SHOWING:
                currentMenuState = MenuState.SHOWN;
                break;
            case HIDING:
                currentMenuState = MenuState.HIDDEN;
                menu.setVisibility(View.GONE);
                break;
            default:
                return;
        }
    }

    // Make scrolling more natural. Move more quickly at the end
    protected class EaseInInterpolator implements Interpolator {
        @Override
        public float getInterpolation(float t) {
            return (float) Math.pow(t - 1, 5) + 1;
        }

    }

    // Is menu completely shown
    public boolean isMenuShown() {
        return currentMenuState == MenuState.SHOWN;
    }

    // Handle touch event on content View
    public boolean onContentTouch(View v, MotionEvent event) {
        // Do nothing if sliding is in progress
        if (currentMenuState == MenuState.HIDING || currentMenuState == MenuState.SHOWING)
            return false;

        // getRawX returns X touch point corresponding to screen
        // getX sometimes returns screen X, sometimes returns content View X
        int curX = (int) event.getRawX();
        int diffX = 0;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                prevX = curX;
                return true;

            case MotionEvent.ACTION_MOVE:
                // Set menu to Visible when user start dragging the content View
                if (!isDragging) {
                    isDragging = true;
                    menu.setVisibility(View.VISIBLE);
                }

                // How far we have moved since the last position
                diffX = curX - prevX;

                // Prevent user from dragging beyond border
                if (contentXOffset + diffX <= 0) {
                    // Don't allow dragging beyond left border
                    // Use diffX will make content cross the border, so only translate by -contentXOffset
                    diffX = -contentXOffset;
                } else if (contentXOffset + diffX > sldingLayoutWidth - menuRightMargin) {
                    // Don't allow dragging beyond menu width
                    diffX = sldingLayoutWidth - menuRightMargin - contentXOffset;
                }

                // Translate content View accordingly
                content.offsetLeftAndRight(diffX);

                contentXOffset += diffX;

                // Invalite this whole Slidinglayout, causing onLayout() to be called
                this.invalidate();

                prevX = curX;
                lastDiffX = diffX;
                return true;

            case MotionEvent.ACTION_UP:
                // Start scrolling
                // Remember that when content has a chance to cross left border, lastDiffX is set to 0
                if (lastDiffX > 0) {
                    // User wants to show menu
                    currentMenuState = MenuState.SHOWING;

                    // Start scrolling from contentXOffset
                    menuScroller.startScroll(contentXOffset, 0, menu.getLayoutParams().width - contentXOffset,
                            0, SLIDING_DURATION);
                } else if (lastDiffX < 0) {
                    // User wants to hide menu
                    currentMenuState = MenuState.HIDING;
                    menuScroller.startScroll(contentXOffset, 0, -contentXOffset,
                            0, SLIDING_DURATION);
                }

                // Begin querying
                menuHandler.postDelayed(menuRunnable, QUERY_INTERVAL);

                // Invalite this whole Slidinglayout, causing onLayout() to be called
                this.invalidate();

                // Done dragging
                isDragging = false;
                prevX = 0;
                lastDiffX = 0;
                return true;

            default:
                break;
        }
        return false;
    }
}

Usage in interface (Activity)

    In the Activity layout, adding SlidingLayout as the root container, the menu view certainly is a ListView and the content view is an empty ViewGroup, we will replace Fragments to it then. The menu icon is located in Toolbar:
activity_main.xml
<info.devexchanges.slidingmenu.SlidingLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/sliding_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- This holds our menu -->
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <ListView
            android:id="@+id/activity_main_menu_listview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#ffb366"
            android:scrollbars="none" />
    </LinearLayout>

    <!-- This holds our content-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@color/colorPrimary"
            android:orientation="horizontal"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

            <ImageView
                android:id="@+id/menu_icon"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:contentDescription="@string/app_name"
                android:onClick="toggleMenu"
                android:src="@drawable/menu" />

            <TextView
                android:id="@+id/title"
                style="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="@dimen/activity_horizontal_margin"
                android:layout_marginStart="@dimen/activity_horizontal_margin"
                android:gravity="center"
                android:textColor="@android:color/white" />

        </android.support.v7.widget.Toolbar>

        <!-- Fragments container layout -->
        <FrameLayout
            android:id="@+id/activity_main_content_fragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        </FrameLayout>
    </LinearLayout>
</info.devexchanges.slidingmenu.SlidingLayout>
    As you can see, I put an ImageView and a TextView inside Toolbar. They work as a toggle menu button and the title of screens.

Programmatically code

    Your Activity must extend from AppCompatActivity, there is nothing too special in code, we set adapter for the ListView (as the menu view), handling menu item click event by replace the corresponding Fragment to the container layout:
MainActivity.java
package info.devexchanges.slidingmenu;

import android.annotation.SuppressLint;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
    // The SlidingLayout which will hold both the sliding menu and our main content
    // Main content will holds our Fragment respectively
    SlidingLayout slidingLayout;

    // ListView menu
    private ListView listMenu;
    private String[] listMenuItems;

    private Toolbar toolbar;
    private TextView title; //page title
    private ImageView btMenu; // Menu button
    private Fragment currentFragment;

    @SuppressLint("SetTextI18n")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Inflate the mainLayout
        setContentView(R.layout.activity_main);
        slidingLayout = (SlidingLayout) findViewById(R.id.sliding_layout);
        toolbar = (Toolbar) findViewById(R.id.toolbar);
        title = (TextView) findViewById(R.id.title);
        setSupportActionBar(toolbar);

        // Init menu
        listMenuItems = getResources().getStringArray(R.array.menu_items);
        listMenu = (ListView) findViewById(R.id.activity_main_menu_listview);
        listMenu.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, listMenuItems));
        listMenu.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                onMenuItemClick(parent, view, position, id);
            }

        });

        // handling menu button event
        btMenu = (ImageView) findViewById(R.id.menu_icon);
        btMenu.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Show/hide the menu
                toggleMenu(v);
            }
        });

        // Replace fragment main when activity start
        FragmentManager fm = MainActivity.this.getSupportFragmentManager();
        FragmentTransaction ft = fm.beginTransaction();

        MainFragment fragment = new MainFragment();
        ft.add(R.id.activity_main_content_fragment, fragment);
        ft.commit();

        currentFragment = fragment;
        title.setText("Sliding Menu like Facebook");
    }

    public void toggleMenu(View v) {
        slidingLayout.toggleMenu();
    }

    // Perform action when a menu item is clicked
    private void onMenuItemClick(AdapterView<?> parent, View view, int position, long id) {
        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction ft = fm.beginTransaction();
        Fragment fragment;

        if (position == 0) {
            fragment = new MainFragment();
            title.setText("Main Screen");
        } else if (position == 1) {
            fragment = new ListViewFragment();
            title.setText("ListView Fragment");
        } else if (position == 2) {
            fragment = new TextViewFragment();
            Bundle args = new Bundle();
            args.putString("KEY_STRING", "This is a TextView in the Fragment");
            fragment.setArguments(args);
            title.setText("TextView Fragment");
        } else {
            fragment = new DummyFragment();
            title.setText("Blank Fragment");
        }

        if(!fragment.getClass().equals(currentFragment.getClass())) {
            // Replace current fragment by this new one
            ft.replace(R.id.activity_main_content_fragment, fragment);
            ft.commit();

            currentFragment = fragment;
        }

        // Hide menu anyway
        slidingLayout.toggleMenu();

    }

    @Override
    public void onBackPressed() {
        if (slidingLayout.isMenuShown()) {
            slidingLayout.toggleMenu();
        } else {
            super.onBackPressed();
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        getSupportActionBar().setTitle("");
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return super.onCreateOptionsMenu(menu);
    }
}
     When running app, you'll have this output:

Coclusions and references

    With ListViewFragment, TextViewFragments and some another necessary files, you can view them on @Github. Through this post, I hope that you can be learned about making a complicated custom view (the sliding menu). This menu type is better Navigation Drawer in Android SDK, the application looks smoother in use.
    References:

Share


Previous post
« Prev Post
Next post
Next Post »