September 12, 2015

Sliding/Flyout-in menu in android xamarin using Animation and Gesture detection

In Brief: This article will help you on building sliding menu and expandable description window in android xamarin with simple steps using Animation and Touch detection. 

In my previous post shared my thought about How to draw route between two geo-location in a google map xamrin iOS, Best Practice and issues with ListView in Android Xamarin
How to use Google Place API with Autocomplete in Xamarin AndroidIntegrating Login by Google Account in Xamarin.Android,How to avoid ImageBitmap OutOfMemoryException and Rounded corner Image in android Xamarin,Drawing path between two location in xamarin android.



In Detail: 
Sliding menu is now became the one of the most common feature in a mobile application. This menu is hidden and  can be shown by swiping the screen from left to right or tapping the icon on action bar,and in the same way hides on swipe from right to left or tap any where on the screen. 

Here i have implemented this based on swiping gestures and animation. As we know touch gestures are most user friendly interaction in mobile application and animation are key to designing a pleasant user experience. Sliding menu can also be achieved by using "Navigation Drawer",but in this post i didn't made use of that concept.This below mentioned procedure will be helpful to those who are looking for a sliding menu implementation with quicker and lesser coding :) ;)

In steps:

1.Edit the layout file as follow :
Listview is used to bind the menu items and Textview to show expandable description window in the bottom of the screen.
Here Listview is placed below in the layout file for the higher the z-index value.


Main.axml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:minWidth="25px"
    android:minHeight="25px">
    <RelativeLayout
        android:id="@+id/titleBarLinearLayout"
        android:orientation="horizontal"
        android:layout_width="fill_parent"
        android:layout_height="55dp"
        android:background="#ff46a1e1">
        <ImageView
            android:id="@+id/menuIconImgView"
            android:src="@drawable/menu_icon"
            android:scaleType="fitXY"
            android:layout_height="fill_parent"
            android:layout_width="50dp"
            android:padding="0dp"
            android:requiresFadingEdge="none"
            android:fadingEdge="none"
            android:layout_marginTop="2dp"
            android:layout_marginBottom="2dp" />
        <TextView
            android:id="@+id/txtActionBarText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#FFFFFF"
            android:text="Home"
            android:layout_gravity="center"
            android:clickable="true"
            android:layout_centerVertical="true"
            android:textSize="18dp"
            android:layout_centerHorizontal="true" />
    </RelativeLayout>
    <TextView
        android:id="@+id/txtPage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#FFFFFF"
        android:text="Home"
        android:layout_gravity="center"
        android:clickable="true"
        android:layout_centerVertical="true"
        android:textSize="25dp"
        android:layout_centerHorizontal="true"
        android:textStyle="bold" />
    <TextView
        android:id="@+id/txtDescription"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:textColor="#000000"
        android:text="Desription goes here"
        android:layout_gravity="center"
        android:clickable="true"
        android:textSize="18dp"
        android:layout_above="@+id/btnImgExpander"
        android:background="#FFFFFF"
        android:gravity="center"
        android:visibility="gone" />
    <ImageView
        android:id="@+id/btnImgExpander"
        android:layout_alignParentBottom="true"
        android:layout_height="30dp"
        android:layout_width="match_parent"
        android:src="@drawable/up_arrow"
        android:background="#fff2f2f2" />
    <ListView
        android:id="@+id/menuListView"
        android:layout_below="@+id/titleBarLinearLayout"
        android:background="#ff64bbf8"
        android:divider="#CFEBFF"
        android:dividerHeight="1dp"
        android:layout_marginLeft="0dp"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        android:visibility="gone" />
</RelativeLayout>
2: create custom layout for Menu item Listview 

MenuCustomLayout.axml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#ff64bbf8">
    <ImageView
        android:id="@+id/ivMenuImg"
        android:src="@drawable/Icon"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:padding="1dp"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="15dp"
        android:layout_marginBottom="9.2dp"
        android:layout_gravity="center_vertical" />
    <TextView
        android:id="@+id/txtMnuText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="18dp"
        android:layout_marginLeft="65dp"
        android:text="suchith"
        android:textSize="16dp"
        android:typeface="sans"
        android:textColor="#FFFFFF" />
</RelativeLayout>

3. Write a customListview adapter class
String array of menu text and menu icon passed as argument
public class MenuListAdapterClass : BaseAdapter<string> {
        Activity _context;
        string[] _mnuText;
        int[] _mnuUrl;
        //action event to pass selected menu item to main activity
        internal event Action<string> actionMenuSelected;
        public MenuListAdapterClass(Activity context,string[] strMnu,int[] intImage)
        {
            _context = context;
            _mnuText = strMnu;
            _mnuUrl = intImage;
        }
        public override string this[int position]
        {
            get { return this._mnuText[position]; }
        }

        public override int Count
        {
            get { return this._mnuText.Count(); }
        }

        public override long GetItemId(int position)
        {
            return position;
        }
        public override View GetView(int position, View convertView, ViewGroup parent)
        { 
            MenuListViewHolderClass objMenuListViewHolderClass;
            View view;
            view = convertView;
            if ( view == null )
            {
                view= _context.LayoutInflater.Inflate ( Resource.Layout.MenuCustomLayout , parent , false );
                objMenuListViewHolderClass = new MenuListViewHolderClass ();

                objMenuListViewHolderClass.txtMnuText = view.FindViewById<TextView> ( Resource.Id.txtMnuText );
                objMenuListViewHolderClass.ivMenuImg=view.FindViewById<ImageView> ( Resource.Id.ivMenuImg );

                objMenuListViewHolderClass.initialize ( view );
                view.Tag = objMenuListViewHolderClass;
            }
            else
            {
                objMenuListViewHolderClass = ( MenuListViewHolderClass ) view.Tag;
            }
            objMenuListViewHolderClass.viewClicked = () =>
            {
                if ( actionMenuSelected != null )
                {
                    actionMenuSelected ( _mnuText[position] );
                }
            };
            objMenuListViewHolderClass.txtMnuText.Text = _mnuText [position];
            objMenuListViewHolderClass.ivMenuImg.SetImageResource ( _mnuUrl [position] );
            return view;
        }
    }
    //Viewholder class
    internal class MenuListViewHolderClass:Java.Lang.Object
    {
        internal Action viewClicked{ get; set;}
        internal TextView txtMnuText;
        internal ImageView ivMenuImg;
        public void initialize(View view)
        {
            view.Click += delegate
            {
                viewClicked();
            };
        }

    }
4. Write the class for gesture listening.
Here Left to right swipe area is restricted to 100 unit from the left[if(e1.GetX()<100) ].
//GestureListener.cs
 using System;
 using Android.Views;
 namespace AndroidGesture
 {
  class GestureListener: Java.Lang.Object, GestureDetector.IOnGestureListener
  {
   public event Action LeftEvent;
   public event Action RightEvent;
   public event Action SingleTapEvent;
   static int SWIPE_MAX_OFF_PATH = 250;
   static int SWIPE_MIN_DISTANCE = 100;
   static int SWIPE_THRESHOLD_VELOCITY = 200;

   public GestureListener()
   {
   }

   public bool OnFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
   {
    try
    {
     if ( Math.Abs ( e1.GetY () - e2.GetY () ) > SWIPE_MAX_OFF_PATH )
      return false;
     // right to left swipe
     if ( e1.GetX () - e2.GetX () > SWIPE_MIN_DISTANCE && Math.Abs ( velocityX ) > SWIPE_THRESHOLD_VELOCITY && LeftEvent != null )
     {
      RightEvent ();
     }
     else if ( e2.GetX () - e1.GetX () > SWIPE_MIN_DISTANCE && Math.Abs ( velocityX ) > SWIPE_THRESHOLD_VELOCITY && RightEvent != null )
     {
      //left to right swipe
      if(e1.GetX()<100)
       LeftEvent ();
     }
    }
    catch ( Exception e )
    {
     Console.WriteLine ( "Failed to work" +e.Message);
    }
    return false;
   }

   public bool OnDown(MotionEvent e)
   {
    return true;
   }
   public void OnLongPress(MotionEvent e) {}
   public bool OnScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
   {
    return true;
   }
   public void OnShowPress(MotionEvent e)
   {

   }
   public bool OnSingleTapUp(MotionEvent e)
   {
    SingleTapEvent ();
    Console.WriteLine ( "Single tap up" );
    return true;
   }
  }
 }

5. Declare and Initialize the GestureListener and UI controls.
Here also Sliding menu width is restricted to 3/4 of the full screen width.

using System;
using Android.App; 
using Android.Views;
using Android.Widget;
using Android.OS;
using Android.Views.Animations;
using System.Linq;
using Android.Graphics;

namespace AndroidGesture
{
 [Activity ( Label = "SlidingMenu" , MainLauncher = true , Icon = "@drawable/icon" )]
 public class MainActivity : Activity
 { 

  GestureDetector gestureDetector;
  GestureListener gestureListener;

  ListView menuListView; 
  MenuListAdapterClass objAdapterMenu;
  ImageView menuIconImageView;
  int intDisplayWidth;
  bool isSingleTapFired=false;
  TextView txtActionBarText;
  TextView txtPageName;
  TextView txtDescription;
  ImageView btnDescExpander;
  protected override void OnCreate ( Bundle bundle )
  {
   base.OnCreate ( bundle );
   Window.RequestFeature ( WindowFeatures.NoTitle );
   SetContentView ( Resource.Layout.Main ); 
   FnInitialization (); 
   TapEvent ();  
   FnBindMenu (); //find definition in below steps
  }
  void TapEvent()
  {
   //title bar menu icon
   menuIconImageView.Click += delegate(object sender , EventArgs e )
   {
    if ( !isSingleTapFired )
    {
     FnToggleMenu ();  //find definition in below steps
     isSingleTapFired = false;
    }
   };
   //bottom expandable description window
   btnDescExpander.Click += delegate(object sender , EventArgs e )
   {
    FnDescriptionWindowToggle(); 
   };
  }
  void FnInitialization()
  {
   //gesture initialization
   gestureListener = new GestureListener ();  
   gestureListener.LeftEvent += GestureLeft; //find definition in below steps
   gestureListener.RightEvent += GestureRight; 
   gestureListener.SingleTapEvent += SingleTap;  
   gestureDetector = new GestureDetector (this,gestureListener);

   menuListView = FindViewById<ListView> ( Resource.Id.menuListView );
   menuIconImageView = FindViewById<ImageView> ( Resource.Id.menuIconImgView );
   txtActionBarText = FindViewById<TextView> ( Resource.Id.txtActionBarText );
   txtPageName=FindViewById<TextView> ( Resource.Id.txtPage );
   txtDescription=FindViewById<TextView> ( Resource.Id.txtDescription );
   btnDescExpander =FindViewById<ImageView> ( Resource.Id.btnImgExpander );

   //changed sliding menu width to 3/4 of screen width 
   Display display = this.WindowManager.DefaultDisplay; 
   var point = new Point ();
   display.GetSize (point);
   intDisplayWidth = point.X;
   intDisplayWidth=intDisplayWidth - (intDisplayWidth/3);
   using ( var layoutParams = ( RelativeLayout.LayoutParams ) menuListView.LayoutParameters  )
   {
    layoutParams.Width = intDisplayWidth;
    layoutParams.Height = ViewGroup.LayoutParams.MatchParent;
    menuListView.LayoutParameters = layoutParams;
   }  
  }

6. Bind the menu item to listview and display the selected item in title bar
  
 void FnBindMenu()
      {   
   string[] strMnuText={ GetString(Resource.String.Home),GetString(Resource.String.AboutUs),GetString(Resource.String.Products),GetString(Resource.String.Events),GetString(Resource.String.Serivce),GetString(Resource.String.Clients),GetString(Resource.String.Help),GetString(Resource.String.Solution),GetString(Resource.String.ContactUs)};
            int [] strMnuUrl={Resource.Drawable.icon_home,Resource.Drawable.icon_aboutus,Resource.Drawable.icon_product,Resource.Drawable.icon_event,Resource.Drawable.icon_service,Resource.Drawable.icon_client,Resource.Drawable.icon_help,Resource.Drawable.icon_solution,Resource.Drawable.icon_contactus}; 
            if ( objAdapterMenu != null )
            {
                objAdapterMenu.actionMenuSelected -= FnMenuSelected;
                objAdapterMenu = null;
            }
            objAdapterMenu = new MenuListAdapterClass (this,strMnuText,strMnuUrl); 
            objAdapterMenu.actionMenuSelected += FnMenuSelected;
            menuListView.Adapter = objAdapterMenu;   
        }
        void FnMenuSelected(string strMenuText)
        {
            txtActionBarText.Text = strMenuText;
            txtPageName.Text = strMenuText;
            //selected action goes here
        }
7. Write down the action for above defined gesture events.
Here boolean flag "isSingleTapFired" is declared to avoid the conflict between gesture event "SingleTap()" and menu click event "menuIconImageView.Click()" i.e. Conflict is when you click on title bar menuIcon to expand the menu, in the other side gesture event SingleTap() also get fires.

 void GestureLeft()
        {
            if(!menuListView.IsShown)
                FnToggleMenu (); 
            isSingleTapFired = false; 
        }
        void GestureRight()
        {
            if(menuListView.IsShown)
                FnToggleMenu (); 
            isSingleTapFired = false; 
        }
        void SingleTap()
        {
            if ( menuListView.IsShown )
            {
                FnToggleMenu ();
                isSingleTapFired = true;
            }
            else
            {
                isSingleTapFired = false;
            }
        }
        public override bool DispatchTouchEvent (MotionEvent ev)
        {
            gestureDetector.OnTouchEvent ( ev );
            return base.DispatchTouchEvent (ev); 
        }

8. At last but important one is sliding the menu.
Animate the menu from left to right and right to left using TranslateAnimation function.
For the movement along x-axis specify start,end point and keep y-axis start,end point as zero.

//toggling the left menu
void FnToggleMenu()
{
 Console.WriteLine ( menuListView.IsShown );
 if(menuListView.IsShown)
 { 
  menuListView.Animation = new  TranslateAnimation ( 0f , -menuListView.MeasuredWidth , 0f , 0f );
  menuListView.Animation.Duration = 300;
  menuListView.Visibility = ViewStates.Gone;  
 }
 else
 {  
  menuListView.Visibility =   ViewStates.Visible; 
  menuListView.RequestFocus (); 
  menuListView.Animation = new  TranslateAnimation ( -menuListView.MeasuredWidth, 0f , 0f , 0f );//starting edge of layout 
  menuListView.Animation.Duration = 300;  
 }
} 

//bottom desription window sliding 
void FnDescriptionWindowToggle()
{
 if(txtDescription.IsShown)
 {  
  txtDescription.Visibility = ViewStates.Gone; 
  txtDescription.Animation = new  TranslateAnimation ( 0f ,0f,0f,txtDescription.MeasuredHeight  );
  txtDescription.Animation.Duration = 300;
  btnDescExpander.SetImageResource ( Resource.Drawable.up_arrow ); 
 }
 else
 {    
  txtDescription.Visibility =   ViewStates.Visible;
  txtDescription.RequestFocus (); 
  txtDescription.Animation = new  TranslateAnimation ( 0f , 0f ,txtDescription.MeasuredHeight,0f);
  txtDescription.Animation.Duration = 300;   
  btnDescExpander.SetImageResource ( Resource.Drawable.down_arrow );
 }

Explore the sample code at github: https://github.com/suchithm/GestureSwipeXamarin.Android
Screen Recording:




Final touch: 
Even though it takes too much to scroll down[;(] it is simple concept. need to more focus on the animation function and the gesture listener and rest is regular coding.
Thanks for your time here,Visit again. I will come back with new and exciting topic on mobile application development. 
 
Happiee coding :)


Reference:
http://developer.xamarin.com/api/type/Android.Views.Animations.TranslateAnimation/
https://developer.xamarin.com/guides/cross-platform/application_fundamentals/touch/part_3_touch_in_android/
http://forums.xamarin.com/discussion/comment/40052/

15 comments:

  1. How can we shift menu to right and swipe from right to left?

    ReplyDelete
  2. Hi Suchith Madavu, Great it works well. But one issue I am facing. I have implemented this for my filter layer. On starting of application my filter layer will be hidden. On click of filter button in tablet this layer has to show with animation on close button click filter layer will hide with animation. Very first time when I click the filter button filter layer shows up without animation. Next time onward animation works fine. Do you have any idea why this happen?

    ReplyDelete
    Replies
    1. Hello Ranjith, you can check this http://www.appliedcodelog.com/2016/01/navigation-drawer-using-material-design.html navigation drawer using android material design.

      Delete
    2. Please replace visibility from viewstate.gone to viewstate.invisible
      This will solve the non animation for the very first time movement.

      Delete
  3. Excellent Suchith..It's working Properly. thanx a lot

    ReplyDelete
  4. Suchith can you tell me how to add theme in this example? like in your output coming in my output black screen coming(with proper output) there is no theme but in your screen one theme is there.

    ReplyDelete
    Replies
    1. Here didn't used any theme. Changes in the output screen is because of the device internal theme.

      Delete
    2. Here didn't used any theme. Changes in the output screen is because of the device internal theme.

      Delete
  5. Hello,
    And great work,
    But I want to ask how to open or call a new page when any item pressed..
    which event do that please?

    ReplyDelete
  6. Hello,
    And great work,
    But I want to ask how to open or call a new page when any item pressed..
    which event do that please?

    ReplyDelete
    Replies
    1. Hello Yasmeen,
      FnMenuSelected() inside an activity class gets fired according to the action event "actionMenuSelected" from adapter class. where you can think of adding new screen.

      Delete
    2. This example gives more emphasis on custom gesture swipe recognition. To call new page you can think of adding FrameLayout in Main.axml and you can load Fragments according to the menu selection. check this: http://www.appliedcodelog.com/2016/01/navigation-drawer-using-material-design.html

      Delete
  7. hello,
    how to add scroll to screen have contain this menu?
    i add scroll to screen but side one tab appear from side menu and other disappear

    ReplyDelete
    Replies
    1. Sorry I didn't get you. Menu has been added to list view,so how you get scrolling issue there.

      Delete