.NET MAUI 中提供了拖放(drag-drop)手势识别器,允许用户通过拖动手势来移动控件。在这篇文章中,我们将学习如何使用拖放手势识别器来实现可拖拽排序列表。在本例中,列表中显示不同大小的磁贴(Tile)并且可以拖拽排序。
![[MAUI]在.NET MAUI中实现可拖拽排序列表](http://www.itfaba.com/wp-content/themes/kemi/images/loading.gif)
使用.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目录下创建不同大小的磁贴控件,如下图所示。
![[MAUI]在.NET MAUI中实现可拖拽排序列表](http://www.itfaba.com/wp-content/themes/kemi/images/loading.gif)
在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的多态。
![[MAUI]在.NET MAUI中实现可拖拽排序列表](http://www.itfaba.com/wp-content/themes/kemi/images/loading.gif)
在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>
![[MAUI]在.NET MAUI中实现可拖拽排序列表](http://www.itfaba.com/wp-content/themes/kemi/images/loading.gif)
创建占位控件,用于指示松开手指时,控件将放置的位置区域,在这里绑定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>
最终效果
![[MAUI]在.NET MAUI中实现可拖拽排序列表](http://www.itfaba.com/wp-content/themes/kemi/images/loading.gif)
项目地址
关注我,学习更多.NET MAUI开发知识!