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

5 comments:

  1. HI,
    Thank for your help!Actually I used your source code but the thing is ,its not working while lots of data , lets add some 25 data and try to swap ,it will not work

    ReplyDelete
    Replies
    1. it is works more than 25 rows. but, index is wrang.

      so, I fixed source code by:

      MyViewCellRenderer.cs Line 57

      // FirstIndex = ListView.IndexOfChild(ViewGroup.Parent as View);
      FirstIndex = ListView.GetPositionForView(ViewGroup.Parent as View);

      and Line 72

      // SecondIndex = ListView.IndexOfChild(ViewGroup.Parent as View);
      SecondIndex = ListView.GetPositionForView(ViewGroup.Parent as View);

      Delete
  2. This comment has been removed by the author.

    ReplyDelete
  3. This is working great in my Droid project, but the ItemLongClick event is overriding the ContextActions defined in my Xamarin.Forms page. If I comment out the Control.ItemLongClick += OnItemLongClick line, the Android context menu shows just fine.


    On iOS, dragging a cell to the side displays the context action menu items. And in Android, if I do not have the Control.ItemLongClick, it, too, displays the context menu as expected.

    In the DraggableListViewRenderer I implemented based on your code, I added:


    public override bool OnInterceptTouchEvent(MotionEvent ev)
    {
    _x = ev.RawX;
    _y = ev.RawY;
    return base.OnInterceptTouchEvent(ev);
    }

    And in OnItemLongClick, I've added a test in ItemLongClick to only start the drag if the touch event is x < 100, which is where I've placed a drag image. Somehow the event handler overrides the ability to display the context menu.

    ReplyDelete
  4. Did you ever do an iOS implementation of this?

    ReplyDelete