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
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
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)
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
As you can see, I put an <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>
ImageView
and a TextView
inside Toolbar
. They work as a toggle menu button and the title of screens.
Programmatically code
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
When running app, you'll have this output: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);
}
}
Coclusions and references
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:
- Sliding menu in Fantageek.
- Android Material design introduction.