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:


video


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/