📜 ⬆️ ⬇️

Implement side navigation in Android

Recently, among mobile application design patterns, there has been a steady tendency to simplify user interaction with the final application. In particular, special emphasis began to be placed on the recognition of gestures. Gestures are intuitive and natural, they are convenient and allow you to get rid of unnecessary interface elements, simplifying the application.

A good example of the proper use of gestures is the lateral navigation that is gaining popularity. On Habré previously published an article about lateral navigation as a pattern, but it did not say anything about the implementation.

Unfortunately, there are very few projects implementing side navigation, and most of them are slow and inconvenient. I was lucky: some time after the start of the search, I stumbled upon the ActionsContentView project, which, in my opinion, worked well and quickly. The project solved all the problems that I once encountered myself. After a careful study of the project, he was slightly rewritten by me for my own needs.
')
Initially, I wanted to describe in this article both the way of opening the side menu by clicking and the way of opening the menu with a gesture. However, towards the end of the article, it became obvious that the processing of gestures and the opening of navigation on them is a rather voluminous issue, in which many features should also be taken into account. The article in such a case is so huge that it is simply inconvenient to read it.
Therefore, I decided to describe so far only a side menu implementation by click.



Application architecture


As a layer with content we will use Fragment, the menu will be located in the Activity in the background.
The advantage of the fragments is obvious: in fact, we can use all the benefits of the Activity inside them, plus from the Activity the layer with the fragment is seen as View, which allows us to use the standard and usual methods of working with it as a layer.

We will make the activity static; during transitions inside a fragment, only the fragment itself should change. It is also necessary to provide in the fragment a method for starting a new fragment in the same window, as well as methods for opening / closing the menu.

To implement this, create an interface that describes the methods of interaction between the fragment and the Activity:

import android.support.v4.app.Fragment; public interface SideMenuListener { public void startFragment(Fragment fragment); public boolean toggleMenu(); } 

We implement it in Activity:
 import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; public class MainActivity extends FragmentActivity implements SideMenuListener { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void startFragment(Fragment fragment) { // TODO Auto-generated method stub } public boolean toggleMenu() { // TODO Auto-generated method stub return false; } } 

Since we need to have access to the above methods from any fragment with content, we extend the Fragment class and add them:
 import android.support.v4.app.Fragment; public class ContentFragment extends Fragment { protected void startFragment(Fragment fragment) { ((SideMenuListener) getActivity()).startFragment(fragment); } protected boolean toggleMenu() { return ((SideMenuListener) getActivity()).toggleMenu(); } } 

In the future, all of our fragments, we will inherit from it.

Create markup, implement the change of fragments


Now we need to create a list that imitates the menu itself, and fill it. We also need the fragment itself with content.

The Activity markup file is incredibly simple:
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#888" > <ListView android:id="@+id/menu" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" /> </RelativeLayout> 

Just like filling it out:
  private String[] names = { "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "" }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView menu = (ListView) findViewById(R.id.menu); ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, names); menu.setAdapter(adapter); } 

Let's create and add our fragment now:
 import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.Button; public class TestFragment extends ContentFragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.test_fragment, container, true); Button toogle = (Button) v.findViewById(R.id.toggle); toogle.setOnClickListener( new OnClickListener() { public void onClick(View arg0) { toggleMenu(); } }); return v; } } 

  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#CCC" > <Button android:id="@+id/toggle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:text="Toggle" /> </RelativeLayout> 


By this button, as you already guessed, we will open or close the menu.

We realize while the mechanism for changing fragments:
 public class MainActivity extends FragmentActivity implements SideMenuListener { private String[] names = { "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "" }; private FragmentTransaction fragmentTransaction; private View content; private int contentID = R.id.content; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); content = findViewById(contentID); // ... } public void startFragment(Fragment fragment) { fragmentTransaction = getSupportFragmentManager().beginTransaction(); fragmentTransaction.replace(contentID, fragment); fragmentTransaction.addToBackStack(null); fragmentTransaction.commit(); } // ... } 


Add the resulting fragment on top of the Activity:
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#888" > <ListView android:id="@+id/menu" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" /> <fragment android:id="@+id/content" android:name="com.habr.sidemenu.TestFragment" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" /> </RelativeLayout> 

The frame is ready, now you can implement the very side navigation.

Side click navigation


The toggleMenu () method automatically opens or closes it, depending on the state of the menu. Accordingly, we need to keep state.
We also need to have a coordinate value to which the menu will “reach” in the event of opening. Since mobile phone displays have different widths, the coefficient must be stored, and the value itself must be calculated based on the resolution of the phone.
It is also advisable to specify the duration of the opening and closing animation in milliseconds.

So:
 public class MainActivity extends FragmentActivity implements SideMenuListener { private final double RIGTH_BOUND_COFF = 0.75; private static int DURATION = 250; private boolean isContentShow = true; private int rightBound; // .. @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); DisplayMetrics displaymetrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(displaymetrics); rightBound = (int) (displaymetrics.widthPixels * RIGTH_BOUND_COFF); // .. } } 


Now a little about the implementation of the class that will scroll through the menu. For our purposes, we will use the Scroller class that encapsulates scrolling. In fact, this class takes a starting point and an offset value, and then for some time generates a certain number.

Most often, Scroller is used within a thread that calls itself recursively. In all the examples I’ve met, Scroller is used that way.
Perhaps it can also be used in conjunction with an infinite loop in a separate thread, but I decided to use just such an implementation.

For opening / closing menus, we have openMenu () and closeMenu () methods. These methods reinitialize the variables of the scrolling start and start the fling () method, which deals with the shift itself.

In the fling () method, after a series of checks, the Scroller count starts, and then the stream starts.
The run () method of the thread performs two actions:


Actually, the class itself is made internal:
  private class ContentScrollController implements Runnable { private final Scroller scroller; private int lastX = 0; public ContentScrollController(Scroller scroller) { this.scroller = scroller; } public void run() { if (scroller.isFinished()) return; final boolean more = scroller.computeScrollOffset(); final int x = scroller.getCurrX(); final int diff = lastX - x; if (diff != 0) { content.scrollBy(diff, 0); lastX = x; } if (more) content.post(this); } public void openMenu(int duration) { isContentShow = false; final int startX = content.getScrollX(); final int dx = rightBound + startX; fling(startX, dx, duration); } public void closeMenu(int duration) { isContentShow = true; final int startX = content.getScrollX(); final int dx = startX; fling(startX, dx, duration); } private void fling(int startX, int dx, int duration) { if (!scroller.isFinished()) scroller.forceFinished(true); if (dx == 0) return; if (duration <= 0) { content.scrollBy(-dx, 0); return; } scroller.startScroll(startX, 0, dx, 0, duration); lastX = startX; content.post(this); } } 


Now we just need to initialize such a field in the class and fill in toggleMenu ():
 public class MainActivity extends FragmentActivity implements SideMenuListener { private ContentScrollController menuController; // ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); menuController = new ContentScrollController(new Scroller(getApplicationContext(), new DecelerateInterpolator(3))); // ... } public boolean toggleMenu() { if(isContentShow) menuController.openMenu(DURATION); else menuController.closeMenu(DURATION); return isContentShow; } } 


Is done. We have a quick side menu that opens by a button. The only bug - the menu scrolls while scrolling on the fragment. To eliminate this bug, it is necessary to check whether the coordinates of pressing a finger are in the fragment area and, depending on this, to determine whether the event is being used or not.

 public class MainActivity extends FragmentActivity implements SideMenuListener { private Rect contentHitRect = new Rect(); // ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); content.setOnTouchListener(new OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { v.getHitRect(contentHitRect); contentHitRect.offset(-v.getScrollX(), v.getScrollY()); if (contentHitRect.contains((int)event.getX(), (int)event.getY())) return true; return v.onTouchEvent(event); } }); // ... } } 


Now everything works.
The resulting side menu works very quickly on a variety of phones, while we have a ready-made architectural solution for organizing screen changes.

I will welcome any comments.

Ready source code


SideMenuListener.java
 package com.habr.sidemenu; import android.support.v4.app.Fragment; public interface SideMenuListener { public void startFragment(Fragment fragment); public boolean toggleMenu(); } 


ContentFragment.java
 package com.habr.sidemenu; import android.support.v4.app.Fragment; public class ContentFragment extends Fragment { protected void startFragment(Fragment fragment) { ((SideMenuListener) getActivity()).startFragment(fragment); } protected boolean toggleMenu() { return ((SideMenuListener) getActivity()).toggleMenu(); } } 


TestFragment.java
 package com.habr.sidemenu; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.Button; public class TestFragment extends ContentFragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.test_fragment, container, true); Button toogle = (Button) v.findViewById(R.id.toggle); toogle.setOnClickListener( new OnClickListener() { public void onClick(View arg0) { toggleMenu(); } }); return v; } } 


MainActivity.java
 package com.habr.sidemenu; import android.graphics.Rect; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentTransaction; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.view.animation.DecelerateInterpolator; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.Scroller; public class MainActivity extends FragmentActivity implements SideMenuListener { private String[] names = { "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "" }; private FragmentTransaction fragmentTransaction; private View content; private int contentID = R.id.content; private final double RIGTH_BOUND_COFF = 0.75; private static int DURATION = 250; private boolean isContentShow = true; private int rightBound; private ContentScrollController menuController; private Rect contentHitRect = new Rect(); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); content = findViewById(contentID); menuController = new ContentScrollController(new Scroller(getApplicationContext(), new DecelerateInterpolator(3))); DisplayMetrics displaymetrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(displaymetrics); rightBound = (int) (displaymetrics.widthPixels * RIGTH_BOUND_COFF); content.setOnTouchListener(new OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { v.getHitRect(contentHitRect); contentHitRect.offset(-v.getScrollX(), v.getScrollY()); if (contentHitRect.contains((int)event.getX(), (int)event.getY())) return true; return v.onTouchEvent(event); } }); ListView menu = (ListView) findViewById(R.id.menu); ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, names); menu.setAdapter(adapter); } public void startFragment(Fragment fragment) { fragmentTransaction = getSupportFragmentManager().beginTransaction(); fragmentTransaction.replace(contentID, fragment); fragmentTransaction.addToBackStack(null); fragmentTransaction.commit(); } public boolean toggleMenu() { if(isContentShow) menuController.openMenu(DURATION); else menuController.closeMenu(DURATION); return isContentShow; } private class ContentScrollController implements Runnable { private final Scroller scroller; private int lastX = 0; public ContentScrollController(Scroller scroller) { this.scroller = scroller; } public void run() { if (scroller.isFinished()) return; final boolean more = scroller.computeScrollOffset(); final int x = scroller.getCurrX(); final int diff = lastX - x; if (diff != 0) { content.scrollBy(diff, 0); lastX = x; } if (more) content.post(this); } public void openMenu(int duration) { isContentShow = false; final int startX = content.getScrollX(); final int dx = rightBound + startX; fling(startX, dx, duration); } public void closeMenu(int duration) { isContentShow = true; final int startX = content.getScrollX(); final int dx = startX; fling(startX, dx, duration); } private void fling(int startX, int dx, int duration) { if (!scroller.isFinished()) scroller.forceFinished(true); if (dx == 0) return; if (duration <= 0) { content.scrollBy(-dx, 0); return; } scroller.startScroll(startX, 0, dx, 0, duration); lastX = startX; content.post(this); } } } 


activity_main.xml
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#888" > <ListView android:id="@+id/menu" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" /> <fragment android:id="@+id/content" android:name="com.habr.sidemenu.TestFragment" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" /> </RelativeLayout> 


test_fragment.xml
  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#CCC" > <Button android:id="@+id/toggle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:text="Toggle" /> </RelativeLayout> 



Sources used


Source: https://habr.com/ru/post/158453/


All Articles