.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. 2021-09-01:三数之和。给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

    2021-09-01:三数之和.给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组. ...

  2. 使用openlayers扩展插件ol-ext设置地图指定区域高亮

    最近要实现一个从底图向上发光的功能,着实纠结了好久,起初像是使用polygon 颜色透明度来实现,但毕竟底图不亮,增加图层效果不理想呀 一.ui设计是这样 二.绘制面 为底图增加一个MultiPoly ...

  3. 使用 StarCoder 创建一个编程助手

    如果你是一个软件开发者,你可能已经使用过 ChatGPT 或 GitHub 的 Copilot 去解决一些写代码过程中遇到的问题,比如将代码从一种语言翻译到另一种语言,或者通过自然语言,诸如" ...

  4. drf——全局处理异常、接口文档、jwt介绍、based64编码与解码

    全局异常处理原理 # 对于前端来讲,后端即便报错,也要返回统一的格式,前端便于处理 {code:999,msg:'系统异常,请联系系统管理员'} # 只要三大认证,视图类的方法出了异常,都会执行一个函 ...

  5. web_枚举

    网页枚举 使用工具 gobuster,Nikto,WPScan Gobuster 安装:sudo apt install gobuster 有用的全局标志 -t 线程 并发线程数(默认10) -v 冗 ...

  6. python里class(类)中object是什么意思

    python的class(类)中的object是什么意思? 写object和不写object有什么区别? 示例代码 class Test: name = "libai" class ...

  7. 手记系列之五 ----- SQL使用经验分享

    前言 本篇文章主要介绍的关于本人从刚工作到现在使用Sql一些使用方法和经验,从最基本的SQL函数使用,到一些场景的业务场景SQL编写. SQL基础函数使用 1.字段转换 CASE WHEN  意义: ...

  8. CANoe学习笔记(二):创建第一个事件触发帧(基于LIN)

    内容: 创建一个事件触发帧: 包含几个不同无条件帧: 事件触发帧的触发: 事先准备: 创建三个文件夹,用来放不同类型文件: 工程创建 新建一个Lin工程,双击即可,然后命名为LINconf保存. 创建 ...

  9. 喜报 | ShowMeBug获国家高新技术企业认证!

    近日,深圳至简天成科技有限公司(以下简称至简天成)顺利通过国家高新技术企业认证! 国家高新技术企业是由国务院主导.科技部牵头的国家级荣誉资质,是我国科技类企业中的"国"字号招牌,完 ...

  10. 搭建自动化 Web 页面性能检测系统 —— 实现篇

    我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品.我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值.. 本文作者:琉易 liuxianyu.cn 前段时间分享了<搭 ...