Friday 20 February 2015

Xamarin.Forms ListView Drag and Drop to Reorder

We are currently looking at ways to add some UX improvements to our application so I thought I would investigate drag and drop on ListView.

There doesn't seem to be anything out of the box yet in Xamarin.Forms so I did a search and found the following :-

http://xamurais.com/drag-and-drop-entre-listview-en-xamarin-android/

This sample was written in classic Xamarin.Android, however I was looking for a Xamarin.Forms implementation. This sample however provided me with the ground work for the sample I propose below.

My sample is only targeting Android at the moment.

My implementation consists of a ViewCellRenderer, this allows you to define a ListView with an ItemTemplate, so that you can bind your ListView to more than a List.

MyViewCellRenderer.cs :-

using System.Collections;
using Android.Content;
using Android.Views;
using ListViewDragDropSample.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using View = Android.Views.View;

[assembly: ExportRenderer(typeof(ViewCell), typeof(MyViewCellRenderer))]
namespace ListViewDragDropSample.Droid
{
 public class MyViewCellRenderer : ViewCellRenderer
 {
  public ListView ParentListView { get; set; }

  public IList Items { get; set; }

  protected override View GetCellCore(Cell item, View convertView, ViewGroup parent, Context context)
  {
   ParentListView = item.ParentView as ListView;

   if (ParentListView != null)
   {
    Items = ParentListView.ItemsSource as IList;
   }

   var cellcore = base.GetCellCore(item, convertView, parent, context);

   cellcore.Drag -= CellcoreOnDrag;
   cellcore.Drag += CellcoreOnDrag;

   return cellcore;
  }

  private void CellcoreOnDrag(object sender, View.DragEventArgs args)
  {
   ViewGroup = sender as ViewGroup;

   if (ViewGroup != null)
   {
    ListView = ViewGroup.Parent.Parent as Android.Widget.ListView;
   }

   switch (args.Event.Action)
   {
    case DragAction.Started:
     args.Handled = true;
     break;

    case DragAction.Entered:
     args.Handled = true;

     if (ListView != null)
     {
      if (FirstIndex == -1)
      {
       FirstIndex = ListView.IndexOfChild(ViewGroup.Parent as View);
      }
     }

     break;

    case DragAction.Exited:
     args.Handled = true;
     break;

    case DragAction.Drop:
     args.Handled = true;

     if (SecondIndex == -1)
     {
      SecondIndex = ListView.IndexOfChild(ViewGroup.Parent as View);
     }

     if (FirstIndex != -1)
     {
      var firstItem = Items[FirstIndex];

      if (firstItem != null)
      {
       Items.RemoveAt(FirstIndex);
       Items.Insert(SecondIndex, firstItem);

       ParentListView.ItemsSource = null;
       ParentListView.ItemsSource = Items;
      }
     }

     FirstIndex = -1;
     SecondIndex = -1;

     break;
    case DragAction.Ended:
     args.Handled = true;
     break;
   }
  }

  public Android.Widget.ListView ListView { get; set; }

  public ViewGroup ViewGroup { get; set; }

  private static int _firstIndex = -1;
  private static int _secondIndex = -1;

  public static int FirstIndex
  {
   get { return _firstIndex; }
   set { _firstIndex = value; }
  }
  public static int SecondIndex
  {
   get { return _secondIndex; }
   set { _secondIndex = value; }
  }
 }
}

MyListViewRenderer.cs :-

using Android.Content;
using ListViewDragDropSample.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(ListView), typeof(MyListViewRenderer))]
namespace ListViewDragDropSample.Droid
{
 public class MyListViewRenderer : ListViewRenderer
 {
  protected override void OnElementChanged(ElementChangedEventArgs e)
  {
   base.OnElementChanged(e);

   Control.ItemLongClick += (s, args) =>
   {
    ClipData data = ClipData.NewPlainText("List", args.Position.ToString());
    MyDragShadowBuilder myShadownScreen = new MyDragShadowBuilder(args.View);
    args.View.StartDrag(data, myShadownScreen, null, 0);
   };
  }
 }
}

MainPage.xaml :-

 
  
   
    
     
      
    
   
  
 


MainPage.xaml.cs :-

using System.Collections.Generic;
using Xamarin.Forms;

namespace ListViewDragDropSample
{
 public partial class MainPage : ContentPage
 {
  public MainPage()
  {
   InitializeComponent();

   Items = new List();

   for (int i = 1; i < 11; i++)
   {
    Items.Add(new Item()
    {
     Title = "Title : " + i,
     Description = "Description : " + i,
    });
   }

   BindingContext = this;
  }

  public List Items { get; set; }
 }
}



MyDragShadowBuilder.cs :-
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Views;

namespace ListViewDragDropSample.Droid
{
 public class MyDragShadowBuilder : View.DragShadowBuilder
 {
  private Drawable shadow;

  public MyDragShadowBuilder(View v)
   : base(v)
  {
   v.DrawingCacheEnabled = true;
   Bitmap bm = v.DrawingCache;
   shadow = new BitmapDrawable(bm);
   shadow.SetColorFilter(Color.ParseColor("#4EB1FB"), PorterDuff.Mode.Multiply);
  }

  public override void OnProvideShadowMetrics(Point size, Point touch)
  {
   int width = View.Width;
   int height = View.Height;
   shadow.SetBounds(0, 0, width, height);
   size.Set(width, height);
   touch.Set(width / 2, height / 2);
  }

  public override void OnDrawShadow(Canvas canvas)
  {
   base.OnDrawShadow(canvas);
   shadow.Draw(canvas);
  }
 }
}


And finally the Item.cs :-

namespace ListViewDragDropSample
{
 public class Item
 {
  public string Title { get; set; }
  public string Description { get; set; }
 }
}

The complete sample is here :-

http://www.smartmobiledevice.co.uk/Samples/Xamarin/ListViewDragDropSample.zip

Xamarin.Forms TabbedPage and Swipe

As of version 1.3.5-pre1 of Xamarin.Forms, there doesn't seem to be a way to swipe a TabbedPage on Android out of the box.

I first thought I could add the gesture via a GestureListener in a Custom Renderer, however this causes issues when trying to swipe while showing a full screen button or ListView, as these two controls consume the Gestures, so this wasn't an option.

I then had an idea about combining a CarouselPage and TabbedPage, hence this post.

This is very convoluted and will not perform very well when used in complex UI layouts, but it might be useful.

First the XAML :-

 
  
   
    
     
      
     
    
    
    
   
  
  
   
    
    
     
      
     
    
    
   
  
  
   
    
    
    
     
      
     
    
   
  
 


As you can see this layout won't work for Windows Phone and I haven't tested this on iOS however I am only really targeting Android at the moment.

Next is the C# :-

public partial class MainPage : TabbedPage
{
 public MainPage()
 {
  InitializeComponent();

  AttachCurrentPageChanged();
 }

 private void AttachCurrentPageChanged()
 {
  Page1.CurrentPageChanged += MultiPage_OnCurrentPageChanged;
  Page2.CurrentPageChanged += MultiPage_OnCurrentPageChanged;
  Page3.CurrentPageChanged += MultiPage_OnCurrentPageChanged;
 }

 private void DetachCurrentPageChanged()
 {
  Page1.CurrentPageChanged -= MultiPage_OnCurrentPageChanged;
  Page2.CurrentPageChanged -= MultiPage_OnCurrentPageChanged;
  Page3.CurrentPageChanged -= MultiPage_OnCurrentPageChanged;
 }

 private void MultiPage_OnCurrentPageChanged(object sender, EventArgs e)
 {
  DetachCurrentPageChanged();

  CarouselPage carouselPage = sender as CarouselPage;
  if (carouselPage != null)
  {
   int indexOf = carouselPage.Children.IndexOf(carouselPage.CurrentPage);

   var tabbedPage = carouselPage.ParentView as TabbedPage;

   if (tabbedPage != null)
   {
    tabbedPage.CurrentPage = tabbedPage.Children[indexOf];

    var newCarouselPage = tabbedPage.CurrentPage as CarouselPage;

    if (newCarouselPage != null)
    {
     newCarouselPage.CurrentPage = newCarouselPage.Children[indexOf];
    }
   }
  }

  AttachCurrentPageChanged();
 }
}
The OnCurrentPageChanged is used to track the Page Change of the CarouselPage, the index of this page is then used to set the CurrentPage of the next CarouselPage.

As this tracking needs to occur the number of child Pages in each of the CarouselPages needs to equal the numbwe of Tabs you would like to show.

If you want to add more Tabs, you add more CarouselPage's as children of the TabbedPage and then ensure that each CarouselPage is updated with the correct number of children.

This is by far an ideal solution but was interesting nevertheless.

The complete sample is here :-

http://www.smartmobiledevice.co.uk/Samples/Xamarin/SwipeTabbedPageSample.zip