Reorder items with MR.Gestures

I’m often asked how you would use drag & drop to reorder items. The simplest solution is of course with MR.Gestures. So it’s time that I finally add a page to the GestureSample which demonstrates that and explain here what I did and why.

In my case the items have various sizes. I want to react on taps and drag them around to reorder them.

The ViewModel

I wrote a simple struct for each item:

    public struct ItemViewModel
    {
        public string Text { get; }
        public Color Color { get; }

        public ItemViewModel(string text)
        {
            Text = text;
            var rnd = new Random();
            Color = Color.FromRgb(rnd.Next(0, 255), rnd.Next(0, 255), rnd.Next(0, 255));
        }
    }

Each item has a text of various lengths and a random Color just to visualize it better.

The ViewModel of the whole page then just needs a list of those items. I also added some commands so that the View can notify the ViewModel of what is going on:

    public class DragAndDropViewModel
    {
        public List<ItemViewModel> Items { get; }

        public ICommand TappedCommand { get; }
        public ICommand StartDraggingCommand { get; }
        public ICommand DroppedCommand { get; }

I initialized the Items with some Pulp Fiction quotes from slipsum.com and wrapped each word in a ItemViewModel to get some test data.

The commands will all hold Command<ItemViewModel>, so the respective item will be passed to the Execute method.

The View

The items should be displayed side by side in multiple rows. With different widths of the items the easiest to use is a FlexLayout. The FlexLayout itself does not have a ItemsSource, but this can easily be solved with Bindable Layouts. So my XAML becomes this:

    <FlexLayout x:Name="theFlexLayout"
        Wrap="Wrap" AlignItems="Start" AlignContent="Start">
        BindableLayout.ItemsSource="{Binding Items}"
        <BindableLayout.ItemTemplate>
            <DataTemplate>
                <Frame
                    BackgroundColor="{Binding Color}"
                    CornerRadius="10">
                    <Label Text="{Binding Text}" HorizontalOptions="Center" />
                </Frame>
            </DataTemplate>
        </BindableLayout.ItemTemplate>
    </FlexLayout>

Wrap, AlignItems and AlignContent simply tell the FlexLayout to display the items in multiple rows top down, left to right. BindableLayout.ItemsSource and BindableLayout.ItemTemplate are attached properties which allow to bind to my Items from the ViewModel. And finally within the DataTemplate we have a Frame with a BackgroundColor and the Label bound to the Text.

On an iPad this looks like this:

Colorful FlexLayout

So far so good. We didn’t do any drag & drop yet.

Adjust the View for Drag & Drop

On my other test pages in the GestureSample I simply changed TranslationX/Y to move items around. But within the FlexLayout I cannot do that. The item would be displayed below other items. I need to show it above the others, so I need a surrounding Grid where I can add the draggable item to. I also need to be notified about pan gestures on that Grid so I use a MR.Gestures.Grid.

    <mr:Grid
        x:Name="theGrid"
        RowDefinitions="3*,*"
        Panning="Grid_Panning"
        Panned="Grid_Panned">

        <FlexLayout ...

The ViewModel doesn’t need to know about the drag&drop stuff. This is just View related and the VM will only be notified when dragging starts and an item is dropped. All the code for drag&drop is in the code behind of the View. So I use events in this case. The Grid_Panning method handles dragging and Grid_Panned dropping of an item.

The Xamarin.Forms.DragGestureRecognizer uses a long press to start a drag gesture. With MR.Gestures you have the option. I want to start it as soon as the user drags something. To get the touched item it is easiest to use the Down gesture.

I also want to be notified, when an item is tapped. And to be exact: the ViewModel needs to be notified because this tapping is not related to the drag&drop gesture, it triggers something in my business logic.

I replace the Xamarin.Forms.Frame with a MR.Gestures.Frame and add some gesture handlers:

    <DataTemplate>
        <mr:Frame
            TappedCommand="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.TappedCommand}"
            TappedCommandParameter="{Binding}"
            Down="Frame_Down"
            ...

Remember: for each item within the DataTemplate the BindingContext is a ItemViewModel. But the TappedCommand is a property in DragAndDropViewModel. Therefore I have to search for the surrounding ContentPage up the visual tree and use its BindingContext.TappedCommand. This is a bit tricky, because you don’t get a warning or binding error when something is wrong. I forgot the TappedCommandParameter and then the command was never executed because the type of the parameter was wrong. I searched for hours for the correct syntax of RelativeSource when in fact the problem was the missing parameter binding.

The Down parameter is easier. It’s just the name of a method in the Views code behind.

The code behind

Lets begin with that method:

    Frame draggingObject;

    private void Frame_Down(object sender, MR.Gestures.DownUpEventArgs e)
    {
        // remember draggingObject
        draggingObject = (Frame)sender;
    }

It just remembers which Frame (=item) has been touched and that’s it. I could also get that from the touch coordinates in the Grid_Panning handler, but this way you could easily replace it with e.g. LongPressing if you want to start dragging with a long press.

In Grid_Panning we first have to check, if the gesture just started.

    bool dragging = false;
    AbsoluteLayout cloneContainer;
    Frame clone;
    double x, y, w, h;
    int originalIndex, currentIndex;

    private void Grid_Panning(object sender, MR.Gestures.PanEventArgs e)
    {
        if(!dragging && draggingObject != null)
        {

Initialize dragging

Tell the ViewModel that the dragging gesture starts.

            VM.StartDraggingCommand.Execute(draggingObject.BindingContext);

Now we start the dragging operation and initialize everything.

dragging is a flag so that the View knows, that something is dragged. We could probably also do this with draggingObject, but it’s simpler like this.

            dragging = true;

originalIndex is the index of the draggingObject when the gesture started, currentIndex is the index over which the touch is at any given time.

            originalIndex = currentIndex = theFlexLayout.Children.IndexOf(draggingObject);

Find the related ItemViewModel.

            var draggingItem = (DragAndDropViewModel.ItemViewModel)draggingObject.BindingContext;

Now we create a clone of the draggingObject. This is the object, which we will move around the screen.

            clone = new Frame()
            {
                BackgroundColor = draggingItem.Color,
                CornerRadius = draggingObject.CornerRadius,
                BorderColor = Color.Red,

                Content = new Label()
                {
                    Text = draggingItem.Text,
                    HorizontalOptions= LayoutOptions.Center,
                }
            };

Get position and size of the object. GetAbsolutePosition is a helper method which just adds up X and Y of the draggingObject and its parents.

            (x, y) = GetAbsolutePosition(draggingObject);
            w = draggingObject.Width;
            h = draggingObject.Height;

Add clone to the page at the same position as the draggingObject but on top of all other objects. For that we add an AbsoluteLayout to the same Grid.Row/Column (0/0) as the FlexLayout. The position of the clone within the AbsoluteLayout is the same position as the original draggingObject.

            cloneContainer = new AbsoluteLayout();
            cloneContainer.Children.Add(clone, new Rectangle(x, y, w, h));
            theGrid.Children.Add(cloneContainer);

Make the draggingObject invisible. If I set IsVisible to false, then the FlexLayout removes the child. But I want a gap where the item would be inserted so that the user can easily see where it will go. So I leave it visible, but make it transparent.

            draggingObject.Opacity = 0;
        }

Drag

The dragging gesture is started and the clone is in place. Now we are in the loop which will be executed every time, the finger moves on screen.

        if (dragging && draggingObject != null)
        {

Change the coordinates of the clone. Here I use PanEventArgs.DeltaDistance to see, how much the touch moved since the last Panning event.

            x += e.DeltaDistance.X;
            y += e.DeltaDistance.Y;
            AbsoluteLayout.SetLayoutBounds(clone, new Rectangle(x, y, w, h));

Check if the child under the touch coordinates changed. Here again I need MR.Gestures to get the coordinate of the first finger on the screen. GetChildAt is a helper method which I will not list here. You’ll find it in the full source code.

            var newIndex = GetChildAt(e.Touches[0]);
            if (newIndex == -1)
                newIndex = originalIndex;

            if(newIndex != currentIndex)
            {

If the touch moved to a different item, then we remove the draggingObject from the old position and insert it at the new one. Remember that the draggingObject is transparent. So we actually only move the gap around at which the item would be dropped.

                theFlexLayout.Children.RemoveAt(currentIndex);
                theFlexLayout.Children.Insert(newIndex, draggingObject);

We then change the currentIndex to the newIndex. A small detail which must not be forgotten.

                currentIndex = newIndex;
            }
        }
    }

Drop

When the finger is lifted from the display, the Panned gesture is raised.

    private void Grid_Panned(object sender, MR.Gestures.PanEventArgs e)
    {
        if (dragging && draggingObject != null)
        {

Remove the clone and cloneContainer from the page.

            theGrid.Children.Remove(cloneContainer);

Make draggingObject visible again. It is already at the correct position.

            draggingObject.Opacity = 1;

Change the order of the Items in ViewModel.

            var draggingItem = VM.Items[originalIndex];
            VM.Items.RemoveAt(originalIndex);
            VM.Items.Insert(currentIndex, draggingItem);

Tell VM that the order changed

            VM.DroppedCommand.Execute(draggingObject.BindingContext);
        }

Reset my temporary variables.

        dragging = false;
        draggingObject = null;
        clone = null;
        cloneContainer = null;
    }

This solution does not need any native code. It works out of the box on iOS, Android and UWP. I didn’t test the other platforms, but they should work too.

I added the code to the GestureSample. So the easiest way to browse it is to check that out. When you run the sample, the test page can be found in Tests / Drag&Drop Items in FlexLayout.

The code used for this sample is in
ViewModels/Tests/DragAndDropViewModel.cs
Views/Tests/DragAndDropPage.xaml
Views/Tests/DragAndDropPage.xaml.cs

Updated:

Leave a comment

Your email address will not be published. Required fields are marked *

Loading...