[MAUI]在.NET MAUI中实现可拖拽排序列表

文章目录

.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开发知识!

相关推荐
唐青枫1 天前
我用 C# 开发了一款电子发票自动整理工具
c#·.net
我是唐青枫1 天前
我用 C# 开发了一款电子发票自动整理工具
c#·.net
唐青枫1 天前
别把 WPF 写成一个 MainWindow:C#.NET Prism 模块化开发详解
c#·.net
小羊没烦恼!7 天前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
_Jonas7 天前
.net framework开发环境正式环境配置文件的切换
.net·wpf
Y3815326629 天前
C# 调用谷歌搜索 API:HttpClient 与 System.Text.Json 实战
搜索引擎·.net
qq_410194299 天前
.NET 8.0 下基于 PaddleOCRSharp 的离线文字识别落地指南
算法·.net
唐青枫11 天前
别急着拆服务:C#.NET 微服务架构从边界设计到实战
c#·.net
唐青枫12 天前
别把 Razor 当成几段 HTML:C#.NET Razor Pages、MVC 视图与实战详解
c#·.net
宝桥南山12 天前
Microsoft Fabric - 简单尝试一下Microsoft Fabric .NET SDK
microsoft·微软·.net·.netcore·powerbi·fabric