[MAUI] Implement draggable sortable list in .NET MAUI

.NET MAUI provides a drag-drop (drag-drop) gesture recognizer that allows users to move controls by dragging gestures. In this post, we'll learn how to implement draggable sortable lists using drag and drop gesture recognizers. In this example, tiles of different sizes are displayed in the list and can be dragged and sorted.

insert image description here

Using .NET MAU to achieve cross-platform support, this project can run on Android and iOS platforms.

Create a draggable control

Create a new .NET MAUI project and name it Tile

When the finger touches the draggable area for more than a certain period of time (the duration may not be the same on different platforms, such as 1s in Android), the drag gesture will be triggered.
When the finger is lifted off the screen, the drop gesture is triggered.

enable dragging

Create a drag gesture recognizer (DragGestureRecognizer) for the page view control, which defines the following properties:

Attributes type describe
CanDrag bool Indicates whether the control the gesture recognizer is attached to can be a drag source. The default value of this property is true.
CanDrag bool Indicates whether the control the gesture recognizer is attached to can be a drag source. The default value of this property is true.
DragStartingCommand ICommand Executed the first time a drag gesture is recognized.
DragStartingCommandParameter object is the parameter passed to DragStartingCommand.
DropCompletedCommand ICommand Executed when the drag source is dropped.
DropCompletedCommandParameter object is the parameter passed to DropCompletedCommand.

enable placement

Create a drop gesture recognizer (DropGestureRecognizer) for the page view control, which defines the following properties:

Attributes type describe
AllowDrop bool Indicates whether the element the gesture recognizer is attached to can be a drop target. The default value of this property is true.
DragOverCommand ICommand Executed when a drag source is dragged onto a drop target.
DragOverCommandParameter object is the parameter passed to DragOverCommand.
DragLeaveCommand ICommand Executed when a drag source is dragged onto a drop target.
DragLeaveCommandParameter object is the parameter passed to DragLeaveCommand.
DropCommand ICommand Executed when the drag source is dropped onto the drop target.
DropCommandParameter object is the parameter passed to DropCommand.

Create a binding class for draggable controls, implement the IDraggableItem interface, and define drag-related properties and commands.

public interface IDraggableItem
{
    bool IsBeingDraggedOver { get; set; }
    bool IsBeingDragged { get; set; }
    Command Dragged { get; set; }
    Command DraggedOver { get; set; }
    Command DragLeave { get; set; }
    Command Dropped { get; set; }
    object DraggedItem { get; set; }
    object DropPlaceHolderItem { get; set; }
}

Dragged: A command triggered when dragging starts.
DraggedOver: The command triggered when the dragged control hovers over the current control.
DragLeave: The command triggered when the dragging control leaves the current control.
Dropped: The command that is triggered when the dragged control is placed above the current control.

When IsBeingDragged is true, it notifies that the current control is being dragged.
When IsBeingDraggedOver is true, it notifies the current control that a drag control is hovering over it.

DraggedItem: The control being dragged.
DropPlaceHolderItem: The control when hovering over it, that is, the placeholder control of the current control.

At this point, the draggable control is a tile segment (TileSegment), and a class is created to describe the properties that the tile can display, such as title, description, icon, color, etc.

public class TileSegment 
{
    public string Title { get; set; }
    public string Type { get; set; }
    public string Desc { get; set; }
    public string Icon { get; set; }
    public Color Color { get; set; }
}

Create a binding service class

Create a binding service class TileSegmentService for draggable controls, inherit ObservableObject, and implement the IDraggableItem interface.

public class TileSegmentService : ObservableObject, ITileSegmentService
{
    ...
}

Drag

When dragging starts, set IsBeingDragged to true to notify that the current control is being dragged, and set DraggedItem as the current control.

private void OnDragged(object item)
{
    IsBeingDragged=true;
    DraggedItem=item;
}

Drag over, over (DragOver)

When the drag control hovers over the current control, set IsBeingDraggedOver to true to notify the current control that a drag control is hovering over it, and at the same time look for the service being dragged in the service list, and set DropPlaceHolderItem to the current controls.

private void OnDraggedOver(object item)
{
    if (!IsBeingDragged && item!=null)
    {
        IsBeingDraggedOver=true;

        var itemToMove = Container.TileSegments.First(i => i.IsBeingDragged);
        if (itemToMove.DraggedItem!=null)
        {
            DropPlaceHolderItem=itemToMove.DraggedItem;

        }
    }

}

IsBeingDraggedOver is set to false when leaving above the control

private void OnDragLeave(object item)
{
    IsBeingDraggedOver = false;
}

Release (Drop)

When the dragging is completed, get the control currently being dragged, remove it from the service list, and then insert it into the position of the current control, and notify the current control that the dragging is complete.

private void OnDropped(object item)
{
    var itemToMove = Container.TileSegments.First(i => i.IsBeingDragged);

    if (itemToMove == null ||  itemToMove == this)
        return;


    Container.TileSegments.Remove(itemToMove);

    var insertAtIndex = Container.TileSegments.IndexOf(this);

    Container.TileSegments.Insert(insertAtIndex, itemToMove);
    itemToMove.IsBeingDragged = false;
    IsBeingDraggedOver = false;
    DraggedItem=null;

}

The complete TileSegmentService code is as follows:

public class TileSegmentService : ObservableObject, ITileSegmentService
{

    public TileSegmentService(
        TileSegment tileSegment)
    {
        Remove = new Command(RemoveAction);
        TileSegment = tileSegment;

        Dragged = new Command(OnDragged);
        DraggedOver = new Command(OnDraggedOver);
        DragLeave = new Command(OnDragLeave);
        Dropped = new Command(i => OnDropped(i));

    }

    private void OnDragged(object item)
    {
        IsBeingDragged=true;
    }

    private void OnDraggedOver(object item)
    {
        if (!IsBeingDragged && item!=null)
        {
            IsBeingDraggedOver=true;

            var itemToMove = Container.TileSegments.First(i => i.IsBeingDragged);
            if (itemToMove.DraggedItem!=null)
            {
                DropPlaceHolderItem=itemToMove.DraggedItem;

            }
        }

    }


    private object _draggedItem;

    public object DraggedItem
    {
        get { return _draggedItem; }
        set
        {
            _draggedItem = value;
            OnPropertyChanged();
        }
    }

    private object _dropPlaceHolderItem;

    public object DropPlaceHolderItem
    {
        get { return _dropPlaceHolderItem; }
        set
        {
            _dropPlaceHolderItem = value;
            OnPropertyChanged();
        }
    }

    private void OnDragLeave(object item)
    {

        IsBeingDraggedOver = false;
        DraggedItem = null;

    }

    private void OnDropped(object item)
    {
        var itemToMove = Container.TileSegments.First(i => i.IsBeingDragged);

        if (itemToMove == null ||  itemToMove == this)
            return;


        Container.TileSegments.Remove(itemToMove);

        var insertAtIndex = Container.TileSegments.IndexOf(this);

        Container.TileSegments.Insert(insertAtIndex, itemToMove);
        itemToMove.IsBeingDragged = false;
        IsBeingDraggedOver = false;
        DraggedItem=null;

    }

    private async void RemoveAction(object obj)
    {
        if (Container is ITileSegmentServiceContainer)
        {
            (Container as ITileSegmentServiceContainer).RemoveSegment.Execute(this);
        }
    }


    public IReadOnlyTileSegmentServiceContainer Container { get; set; }


    private TileSegment tileSegment;

    public TileSegment TileSegment
    {
        get { return tileSegment; }
        set
        {
            tileSegment = value;
            OnPropertyChanged();

        }
    }


    private bool _isBeingDragged;
    public bool IsBeingDragged
    {
        get { return _isBeingDragged; }
        set
        {
            _isBeingDragged = value;
            OnPropertyChanged();

        }
    }

    private bool _isBeingDraggedOver;
    public bool IsBeingDraggedOver
    {
        get { return _isBeingDraggedOver; }
        set
        {
            _isBeingDraggedOver = value;
            OnPropertyChanged();

        }
    }

    public Command Remove { get; set; }


    public Command Dragged { get; set; }

    public Command DraggedOver { get; set; }

    public Command DragLeave { get; set; }

    public Command Dropped { get; set; }
}

Create page elements

Create tile controls of different sizes in the Controls directory, as shown in the figure below.

insert image description here

Create CollectionView in MainPage to display tile elements in list form.

<CollectionView Grid.Row="1"
                x:Name="MainCollectionView"
                ItemsSource="{Binding TileSegments}"
                ItemTemplate="{StaticResource TileSegmentDataTemplateSelector}">
    <CollectionView.ItemsLayout>
        <LinearItemsLayout Orientation="Vertical" />
    </CollectionView.ItemsLayout>
</CollectionView>

Create MainPageViewModel, create a collection of bound service classes TileSegments, add some tiles of different colors and sizes in the initialization, and set TileSegmentService.Container to itself (this).

Tiles of different sizes are displayed using different data templates by binding corresponding data. Please read the blog post [MAUI Programming] Interface polymorphism and implementation to learn how to realize the polymorphism of list Item.

insert image description here

Create a tile fragment data template selector (TileSegmentDataTemplateSelector) in MainPage, which is used to select different data templates according to the size of the tile fragment.

<DataTemplate x:Key="SmallSegment">
    <controls1:SmallSegmentView  Margin="0,5"
                                    ControlTemplate="{StaticResource TileSegmentTemplate}">
    </controls1:SmallSegmentView>
</DataTemplate>
<DataTemplate x:Key="MediumSegment">
    <controls1:MediumSegmentView Margin="0,5"
                                    ControlTemplate="{StaticResource TileSegmentTemplate}">

    </controls1:MediumSegmentView>
</DataTemplate>
<DataTemplate x:Key="LargeSegment">
    <controls1:LargeSegmentView Margin="0,5"
                                ControlTemplate="{StaticResource TileSegmentTemplate}">

    </controls1:LargeSegmentView>
</DataTemplate>
<controls1:TileSegmentDataTemplateSelector x:Key="TileSegmentDataTemplateSelector"
                                            ResourcesContainer="{x:Reference Main}" />

Create a tile control template TileSegmentTemplate and specify DropGestureRecognizer here

<ControlTemplate x:Key="TileSegmentTemplate">
    <ContentView>
        <StackLayout>
            <StackLayout.GestureRecognizers>
                <DropGestureRecognizer AllowDrop="True"
                                        DragLeaveCommand="{TemplateBinding BindingContext.DragLeave}"
                                        DragLeaveCommandParameter="{TemplateBinding}"
                                        DragOverCommand="{TemplateBinding BindingContext.DraggedOver}"
                                        DragOverCommandParameter="{TemplateBinding}"
                                        DropCommand="{TemplateBinding BindingContext.Dropped}"
                                        DropCommandParameter="{TemplateBinding}" />
            </StackLayout.GestureRecognizers>
            
        </StackLayout>
    </ContentView>
</ControlTemplate>

Create a tile control appearance Layout, <ContentPresenter />where the content of the tile fragment will be rendered. Specify DragGestureRecognizer in Layout.

<Border x:Name="ContentLayout"
        Margin="0">
    <Grid>
        <Grid.GestureRecognizers>
            <DragGestureRecognizer CanDrag="True"
                                    DragStartingCommand="{TemplateBinding BindingContext.Dragged}"
                                    DragStartingCommandParameter="{TemplateBinding}" />
        </Grid.GestureRecognizers>

        <ContentPresenter />
        <Button CornerRadius="100"
                HeightRequest="20"
                WidthRequest="20"
                Padding="0"
                BackgroundColor="Red"
                TextColor="White"
                Command="{TemplateBinding BindingContext.Remove}"
                Text="×"
                HorizontalOptions="End"
                VerticalOptions="Start"></Button>
    </Grid>
</Border>

insert image description here

Create a placeholder control to indicate the location area where the control will be placed when the finger is released, and bind the height and width of the DropPlaceHolderItem here.

<Border StrokeThickness="4"
        StrokeDashArray="2 2"
        StrokeDashOffset="6"
        Stroke="black"
        HorizontalOptions="Center"
        IsVisible="{TemplateBinding BindingContext.IsBeingDraggedOver}">
    <Grid HeightRequest="{TemplateBinding BindingContext.DropPlaceHolderItem.Height}"
            WidthRequest="{TemplateBinding BindingContext.DropPlaceHolderItem.Width}">
        <Label HorizontalTextAlignment="Center"
                VerticalOptions="Center"
                Text="松开手指将放置条目至此处"></Label>


    </Grid>
</Border>

final effect

insert image description here

project address

Github:maui-samples

Follow me and learn more about .NET MAUI development!

Guess you like

Origin blog.csdn.net/jevonsflash/article/details/132298036