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

[MAUI]在.NET MAUI中实现可拖拽排序列表的更多相关文章

  1. jQuery可拖拽排序列表jquery-sortable-lists

    jquery-sortable-lists可以通过鼠标进行拖动排列树型菜单,可以定义某个列表元素是否拖动,拖动后回调,点击可以折叠树型结点,可以用来在后台模仿wordpress后台拖动菜单,实现多级菜 ...

  2. 移动端的拖拽排序在react中实现 了解一下

    最近做一个拖拽排序的功能找了好几个有一个步骤简单,结合redux最好不过了,话不多说上代码 第一步: npm install react-draggable-tags --save 第二步 sort. ...

  3. vue中基于sortablejs与el-upload实现文件上传后拖拽排序

    今天做冒烟测试的时候发现商品发布有一个拖拽图片排序功能没做,赶紧加上 之前别的同事基于 vuedraggable 实现过这个功能,我这里自己深度封装了 el-upload ,用这种方式改动很大,而且感 ...

  4. dragsort html拖拽排序

    一.Jquery List DragSort 对于有些页面,如首页的定制,需要进行动态的拖拽排序.由于自己实现比较困难,我们一般会使用一些js插件来实现.dragsort 就是帮助我们完成这一需求.通 ...

  5. 使用knockout-sortable实现对自定义菜单的拖拽排序

    在开始之前,照例,我们先看效果和功能实现. 关于自定义菜单的实现,这里就不多说了,需要了解的请访问:http://www.cnblogs.com/codelove/p/4838766.html 这里需 ...

  6. RecyclerView拖拽排序和滑动删除实现

    效果图 如何实现 那么是如何实现的呢?主要就要使用到ItemTouchHelper ,ItemTouchHelper 一个帮助开发人员处理拖拽和滑动删除的实现类,它能够让你非常容易实现侧滑删除.拖拽的 ...

  7. jquery sortTable拖拽排序

    所有的事件回调函数都有两个参数:event和ui,浏览器自有event对象,和经过封装的ui对象   ui.helper - 表示sortable元素的JQuery对象,通常是当前元素的克隆对象   ...

  8. ListView列表拖拽排序

    ListView列表拖拽排序能够參考Android源代码下的Music播放列表,他是能够拖拽的,源代码在[packages/apps/Music下的TouchInterceptor.java下]. 首 ...

  9. zTree的拖拽排序

    ztree本身是可以支持拖拽的,但是却没有找到明确的支持拖拽的排序,也就是说,在拖拽过程中,需要自定义维护拖拽后的顺序并保存至后台. 在这样一个比较常规的需求情况下,网上也有朋友给出了一些解决方案,比 ...

  10. RecyclerViewItemTouchHelperDemo【使用ItemTouchHelper进行拖拽排序功能】

    版权声明:本文为HaiyuKing原创文章,转载请注明出处! 前言 记录使用ItemTouchHelper对Recyclerview进行拖拽排序功能的实现. 效果图 代码分析 ItemTouchHel ...

随机推荐

  1. 2020-09-15:java里的wait()和sleep()的区别有哪些?

    福哥答案2020-09-15:#福大大架构师每日一题# 1.线程状态.wait()的线程状态是TimedWaiting和Waiting.sleep()的线程状态是Waiting.2.指定时间.wait ...

  2. 2021-04-03:给定两个字符串str1和str2,想把str2整体插入到str1中的某个位置,形成最大的字典序,返回字典序最大的结果。

    2021-04-03:给定两个字符串str1和str2,想把str2整体插入到str1中的某个位置,形成最大的字典序,返回字典序最大的结果. 福大大 答案2021-04-03: 1.暴力法. 2.DC ...

  3. PLSQL一些常用的知识点

    1.背景 此处简单的记录一下在 oracle中如何使用plsql语法,记录一些简单的例子,防止以后忘记. 2.变量的声明 declare -- 声明变量 v_name varchar2(20); -- ...

  4. Solon v2.2.17 发布,Java 新的生态型应用开发框架

    相对于 Spring Boot 和 Spring Cloud 的项目: 启动快 5 - 10 倍. (更快) qps 高 2- 3 倍. (更高) 运行时内存节省 1/3 ~ 1/2. (更少) 打包 ...

  5. UCOS II 源码分析一

    再进行ucos操作系统源码分析前,先对ucos源码文件结构说个简单说明,只有掌握了源码文件结构才能在接下来的源码分析中逐渐感受到会当凌绝顶, 一览众山小,最后的感受就是RTOS也不是很神秘!下面以正点 ...

  6. Vue 路由router

    简单案例: App.vue是核心组件,其中的<router-link>相当于a标签,to相当于href,export是暴露函数,这样某组件才能被其他组件识别到 代码: <templa ...

  7. CentOS系统重命名

    感觉默认的系统名称很难看,得改改,如下 一.查看linux 系统版本信息 [root@iZbp15rmf8lyj2yhq136ebZ ~]# cat /etc/redhat-release CentO ...

  8. kafka消费者那些事儿

    前言 消息的消费一般有两种模式,推模式和拉模式.推模式是服务端主动将消息推送给消费者,而拉模式是消费者主动向服务端发起请求来拉取消息.kakfa采用的是拉模式,这样可以很好的控制消费速率.那么kafk ...

  9. mysql_三大范式

    介绍 数据库的三大范式就是数据库的表应该如何设计,应该注意什么. 第一范式 要求每一张表都有一个主键,每一个字段都不可再分. 举例: id username address 1 张三 中国,北京 2 ...

  10. gateway异常:DefaultDataBuffer cannot be cast to org.springframework.core.io.buffer.NettyDataBuffer

    启动gateway后 出现java.lang.ClassCastException: org.springframework.core.io.buffer.DefaultDataBufferFacto ...