.NET MAUI 中提供了拖放(drag-drop)手势识别器,允许用户通过拖动手势来移动控件。在这篇文章中,我们将学习如何使用拖放手势识别器来实现可拖拽排序列表。在本例中,列表中显示不同大小的磁贴(Tile)并且可以拖拽排序。

使用.NET MAU实现跨平台支持,本项目可运行于Android、iOS平台。
创建可拖放控件
新建.NET MAUI项目,命名Tile
当手指触碰可拖拽区域超过一定时长(不同平台下时长不一定相同,如在Android中是1s)时,将触发拖动手势。
 手指离开屏幕时,将触发放置手势。
启用拖动
为页面视图控件创建拖动手势识别器(DragGestureRecognizer), 它定义了以下属性:
| 属性 | 类型 | 描述 | 
|---|---|---|
| CanDrag | bool | 指明手势识别器附加到的控件能否为拖动源。 此属性的默认值为 true。 | 
| CanDrag | bool | 指明手势识别器附加到的控件能否为拖动源。 此属性的默认值为 true。 | 
| DragStartingCommand | ICommand | 在第一次识别拖动手势时执行。 | 
| DragStartingCommandParameter | object | 是传递给 DragStartingCommand 的参数。 | 
| DropCompletedCommand | ICommand | 在放置拖动源时执行。 | 
| DropCompletedCommandParameter | object | 是传递给 DropCompletedCommand 的参数。 | 
启用放置
为页面视图控件创建放置手势识别器(DropGestureRecognizer), 它定义了以下属性:
| 属性 | 类型 | 描述 | 
|---|---|---|
| AllowDrop | bool | 指明手势识别器附加到的元素能否为放置目标。 此属性的默认值为 true。 | 
| DragOverCommand | ICommand | 在拖动源被拖动到放置目标上时执行。 | 
| DragOverCommandParameter | object | 是传递给 DragOverCommand 的参数。 | 
| DragLeaveCommand | ICommand | 在拖动源被拖至放置目标上时执行。 | 
| DragLeaveCommandParameter | object | 是传递给 DragLeaveCommand 的参数。 | 
| DropCommand | ICommand | 在拖动源被放置到放置目标上时执行。 | 
| DropCommandParameter | object | 是传递给 DropCommand 的参数。 | 
创建可拖拽控件的绑定类,实现IDraggableItem接口,定义拖动相关的属性和命令。
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: 拖拽开始时触发的命令。
 DraggedOver: 拖拽控件悬停在当前控件上方时触发的命令。
 DragLeave: 拖拽控件离开当前控件时触发的命令。
 Dropped: 拖拽控件放置在当前控件上方时触发的命令。
IsBeingDragged 为true时,通知当前控件正在被拖拽。
 IsBeingDraggedOver 为true时,通知当前控件正在有拖拽控件悬停在其上方。
DraggedItem: 正在拖拽的控件。
 DropPlaceHolderItem: 悬停在其上方时的控件,即当前控件的占位控件。
此时可拖拽控件为磁贴片段(TileSegement), 创建一个类用于描述磁贴可显示的属性,如标题、描述、图标、颜色等。
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; }
}
创建绑定服务类
创建可拖拽控件的绑定服务类TileSegmentService,继承ObservableObject,并实现IDraggableItem接口。
public class TileSegmentService : ObservableObject, ITileSegmentService
{
    ...
}
拖拽(Drag)
拖拽开始时,将IsBeingDragged设置为true,通知当前控件正在被拖拽,同时将DraggedItem设置为当前控件。
private void OnDragged(object item)
{
    IsBeingDragged=true;
    DraggedItem=item;
}
拖拽悬停,经过(DragOver)
拖拽控件悬停在当前控件上方时,将IsBeingDraggedOver设置为true,通知当前控件正在有拖拽控件悬停在其上方,同时在服务列表中寻找当前正在被拖拽的服务,将DropPlaceHolderItem设置为当前控件。
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设置为false
private void OnDragLeave(object item)
{
    IsBeingDraggedOver = false;
}
释放(Drop)
拖拽完成时,获取当前正在被拖拽的控件,将其从服务列表中移除,然后将其插入到当前控件的位置,通知当前控件拖拽完成。
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;
}
完整的TileSegmentService代码如下:
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; }
}
创建页面元素
在Controls目录下创建不同大小的磁贴控件,如下图所示。

在MainPage中创建CollectionView,用于将磁贴元素以列表形式展示。
<CollectionView Grid.Row="1"
                x:Name="MainCollectionView"
                ItemsSource="{Binding TileSegments}"
                ItemTemplate="{StaticResource TileSegmentDataTemplateSelector}">
    <CollectionView.ItemsLayout>
        <LinearItemsLayout Orientation="Vertical" />
    </CollectionView.ItemsLayout>
</CollectionView>
创建MainPageViewModel,创建绑定服务类集合TileSegments,初始化中添加一些不同颜色,大小的磁贴,并将TileSegementService.Container设置为自己(this)。
不同大小的磁贴通过绑定相应的数据,使用不同的数据模板进行展示。请阅读博文 [MAUI程序设计]界面多态与实现,了解如何实现列表Item的多态。

在MainPage中创建磁贴片段数据模板选择器(TileSegmentDataTemplateSelector),用于根据磁贴片段的大小选择不同的数据模板。
<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}" />
创建磁贴控件模板TileSegmentTemplate,并在此指定DropGestureRecognizer
<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>
创建磁贴控件外观Layout,<ContentPresenter />处将呈现磁贴片段的内容。在Layout指定DragGestureRecognizer。
<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>

创建占位控件,用于指示松开手指时,控件将放置的位置区域,在这里绑定DropPlaceHolderItem的高度和宽度。
<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>
最终效果

项目地址
Github:maui-samples
关注我,学习更多.NET MAUI开发知识!



![【Linux】Shell脚本之流程控制语句 if判断、for循环、while循环、case循环判断 + 实战详解[⭐建议收藏!!⭐]](https://img-blog.csdnimg.cn/9573137493ad4504a3f463e81691b3fb.png)















