[MAUI]实现动态拖拽排序网格

文章目录

    • 创建页面元素
    • 创建可绑定对象
    • 创建绑定服务类
      • 拖拽(Drag)
      • 拖拽悬停,经过(DragOver)
      • 释放(Drop)
    • 限流(Throttle)和防抖(Debounce)
    • 项目地址

上一章我们使用拖放(drag-drop)手势识别实现了可拖拽排序列表,对于列表中的条目,完整的拖拽排序过程是:
手指触碰条目 -> 拖拽条目 -> 拖拽悬停在另一个条目上方 -> 松开手指 -> 移动条目至此处。

其是在松开手指之后才向列表提交条目位置变更的命令。今天我们换一个写法,将拖拽条目放置在另一个条目上方时,即可将条目位置变更。即实时拖拽排序。

在这里插入图片描述

使用.NET MAU实现跨平台支持,本项目可运行于Android、iOS平台。

创建页面元素

新建.NET MAUI项目,命名Tile

本章的实例中使用网格布局的CollectionView控件作为Tile的容器。

CollectionView 的其他布局方式请参考官方文档 指定 CollectionView 布局

创建GridTilesPage.xaml

在页面中创建CollectionView,

<CollectionView Grid.Row="1"x:Name="MainCollectionView"ItemsSource="{Binding TileSegments}"><CollectionView.ItemTemplate><DataTemplate><ContentView HeightRequest="110" WidthRequest="110" HorizontalOptions="Center" VerticalOptions="Center"><StackLayout><StackLayout.GestureRecognizers><DropGestureRecognizer AllowDrop="True"DragLeaveCommand="{Binding DragLeave}"DragLeaveCommandParameter="{Binding}"DragOverCommand="{Binding DraggedOver}"DragOverCommandParameter="{Binding}"DropCommand="{Binding Dropped}"DropCommandParameter="{Binding}" /></StackLayout.GestureRecognizers><Border x:Name="ContentLayout"StrokeThickness="0"Margin="0"><Grid><Grid.GestureRecognizers><DragGestureRecognizer CanDrag="True"DragStartingCommand="{Binding Dragged}"DragStartingCommandParameter="{Binding}" /></Grid.GestureRecognizers><controls1:TileSegmentView HeightRequest="100"WidthRequest="100"Margin="5,5"></controls1:TileSegmentView><Button CornerRadius="100"HeightRequest="20"WidthRequest="20"Padding="0"Margin="2,2"BackgroundColor="Red"TextColor="White"Command="{Binding Remove}"Text="×"HorizontalOptions="End"VerticalOptions="Start"></Button></Grid></Border></StackLayout></ContentView></DataTemplate></CollectionView.ItemTemplate><CollectionView.ItemsLayout><GridItemsLayout Orientation="Vertical"Span="3" /></CollectionView.ItemsLayout>
</CollectionView>

呈现效果如下:

在这里插入图片描述

DropGestureRecognizer中设置了拖拽悬停、离开、放置时的命令,

创建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; }
}

创建可绑定对象

创建GridTilesPageViewModel,创建绑定服务类集合TileSegments。

private ObservableCollection<ITileSegmentService> _tileSegments;public ObservableCollection<ITileSegmentService> TileSegments
{get { return _tileSegments; }set{_tileSegments = value;OnPropertyChanged();}
}

构造函数中初始化一些不同颜色的磁贴,并将TileSegementService.Container设置为自己(this)。

public GridTilesPageViewModel()
{TileSegments = new ObservableCollection<ITileSegmentService>();CreateSegmentAction("TileSegment", "App1", "Some description here", Colors.LightPink);CreateSegmentAction("TileSegment", "App2", "Some description here", Colors.LightGreen);...
}
private ITileSegmentService CreateTileSegmentService(object obj, string title, string desc, Color color)
{var type = obj as string;var tileSegment = new TileSegment(){Title = title,Type = type,Desc = desc,Icon = "dotnet_bot.svg",Color = color,};var newModel = new GridTileSegmentService(tileSegment); if (newModel != null){newModel.Container = this;}return newModel;
}

创建绑定服务类

创建可拖拽控件的绑定服务类GridTileSegmentService,继承ObservableObject,并实现IDraggableItem接口。

创建ICommand属性:Dragged, DraggedOver, DragLeave, Dropped。

订阅PropertyChanged事件以便在属性更改时触发相关操作

public class GridTileSegmentService : ObservableObject, ITileSegmentService
{public GridTileSegmentService(TileSegment tileSegment){TileSegment = tileSegment;Dragged = new Command(OnDragged);DraggedOver = new Command(OnDraggedOver);DragLeave = new Command(OnDragLeave);Dropped = new Command(i => OnDropped(i));this.PropertyChanged+=GridTileSegmentService_PropertyChanged;}...
}

拖拽(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){var itemToMove = Container.TileSegments.First(i => i.IsBeingDragged);if (itemToMove.DraggedItem!=null){DropPlaceHolderItem=itemToMove.DraggedItem;}IsBeingDraggedOver=true;}
}

离开控件上方时,IsBeingDraggedOver设置为false

private void OnDragLeave(object item)
{IsBeingDraggedOver = false;DropPlaceHolderItem = null;
}

通过订阅PropertyChanged, 在GridTileSegmentService_PropertyChanged方法中响应IsBeingDraggedOver属性的值变更。

当IsBeingDraggedOver为True时代表有拖拽中控件悬停在其上方,DropPlaceHolderItem即为悬停在其上方的控件对象。

此时我们应该将悬停在其上方的控件对象插入到自身的前方,通过获取两者在集合的角标并调用Move()方法。


private void GridTileSegmentService_PropertyChanged(object sender, PropertyChangedEventArgs e)
{if (e.PropertyName==nameof(this.IsBeingDraggedOver)){if (this.IsBeingDraggedOver && DropPlaceHolderItem!=null){var newIndex = Container.TileSegments.IndexOf(this);var oldIndex = Container.TileSegments.IndexOf(DropPlaceHolderItem as ITileSegmentService);Container.TileSegments.Move(oldIndex, newIndex);}}}

效果如下:

在这里插入图片描述

释放(Drop)

拖拽完成时,获取当前正在被拖拽的控件,将其从服务列表中移除,然后将其插入到当前控件的位置,通知当前控件拖拽完成。

private void OnDropped(object item)
{var itemToMove = Container.TileSegments.First(i => i.IsBeingDragged);if (itemToMove == null)return;itemToMove.IsBeingDragged = false;IsBeingDraggedOver = false;DraggedItem=null;DropPlaceHolderItem = null;
}

完整的TileSegmentService代码如下:

public class GridTileSegmentService : ObservableObject, ITileSegmentService
{public GridTileSegmentService(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));this.PropertyChanged+=GridTileSegmentService_PropertyChanged;}private void GridTileSegmentService_PropertyChanged(object sender, PropertyChangedEventArgs e){if (e.PropertyName==nameof(this.IsBeingDraggedOver)){if (this.IsBeingDraggedOver && DropPlaceHolderItem!=null){var newIndex = Container.TileSegments.IndexOf(this);var oldIndex = Container.TileSegments.IndexOf(DropPlaceHolderItem as ITileSegmentService);Container.TileSegments.Move(oldIndex, newIndex);}}}private void OnDragged(object item){IsBeingDragged=true;DraggedItem=item;}private void OnDraggedOver(object item){if (!IsBeingDragged && item!=null){var itemToMove = Container.TileSegments.First(i => i.IsBeingDragged);if (itemToMove.DraggedItem!=null){DropPlaceHolderItem=itemToMove.DraggedItem;}IsBeingDraggedOver=true;}}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;DropPlaceHolderItem = null;}private void OnDropped(object item){var itemToMove = Container.TileSegments.First(i => i.IsBeingDragged);if (itemToMove == null)return;itemToMove.IsBeingDragged = false;IsBeingDraggedOver = false;DraggedItem=null;DropPlaceHolderItem = 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{if (value!=_isBeingDraggedOver){_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; }
}

运行程序,此时我们可以看到拖拽控件悬停在其它控件上方时,其它控件会自动调整位置。

限流(Throttle)和防抖(Debounce)

在特定平台的列表控件中更新项目集合时,引发的动画效果会导致列表中的控件位置错乱。

当以比较快的速度,拖拽Tile经过较多的位置时,后面的Tile会短暂地替代原先的位置,导致拖拽中的Tile不在期望的Tile上方,而拖拽中的Tile与错误的Tile产生了交叠从而触发DraggedOver事件,导致错乱。

在这里插入图片描述

在某些机型上甚至会引发错乱的持续循环

一个办法是禁用动画,如在iOS中配置

listView.On<iOS>().SetRowAnimationsEnabled(false);

动效问题最终要解决。由于快速拖拽Tile经过较多的位置频繁触发Move操作,通过限制事件的触发频率,引入限流(Throttle)和防抖(Debounce)机制可以有效地解决这个问题。限流和防抖的作用如下图:

在这里插入图片描述

代码引用自 ThrottleDebounce

在GridTileSegmentService中创建静态限流器对象变量throttledAction。以及全局锁对象throttledLocker。

public static RateLimitedAction throttledAction = Debouncer.Debounce(null, TimeSpan.FromMilliseconds(500), leading: false, trailing: true);public static object throttledLocker = new object();

改写GridTileSegmentService_PropertyChanged如下:

private void GridTileSegmentService_PropertyChanged(object sender, PropertyChangedEventArgs e)
{if (e.PropertyName==nameof(this.IsBeingDraggedOver)){if (this.IsBeingDraggedOver && DropPlaceHolderItem!=null){lock (throttledLocker){var newIndex = Container.TileSegments.IndexOf(this);var oldIndex = Container.TileSegments.IndexOf(DropPlaceHolderItem as ITileSegmentService);var originalAction = () =>{Container.TileSegments.Move(oldIndex, newIndex);};throttledAction.Update(originalAction);throttledAction.Invoke();}}}}

此时,在500毫秒内,只会执行一次Move操作。问题解决!

在这里插入图片描述

因为有500毫秒的延迟,Tile响应上感觉没有那么“灵动”,这算是一种牺牲。在不同的平台上可以调整这个时间以达到一种平衡,不知道屏幕前的你有没有更好的方式解决呢?

在这里插入图片描述

项目地址

Github:maui-samples

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/112056.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CocosCreator3.8研究笔记(十八)CocosCreator UI组件(二)

前面的文章已经介绍了Canvas 组件、UITransform 组件、Widget 组件 。 想了解的朋友&#xff0c;请查看 CocosCreator3.8研究笔记&#xff08;十七&#xff09;CocosCreator UI组件&#xff08;一&#xff09;。 今天我们主要介绍CocosCreator 常用容器组件&#xff1a;Layout …

C++---异常处理

异常处理 异常处理try语句块和throw表达式异常的抛出和捕获异常的抛出和匹配原则 异常安全异常规范标准异常 异常处理 异常是指存在于运行时的反常行为&#xff0c;这些行为超出了函数正常功能的范围。当程序的某部分检测到一个他无法处理的问题时&#xff0c;需要用到异常处理…

springboot基础--实现默认登录页面

1、搭建项目 依赖中 多加入thymeleaf依赖 <dependencies><!--thymeleaf的依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><!--we…

Cpp/Qt-day010915Qt

目录 将工程文件进行注释 实现如下界面 头文件&#xff1a;widget.h: 源文件&#xff1a;widget.cpp: 运行效果 思维导图 将工程文件进行注释 实现如下界面 头文件&#xff1a;widget.h: #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QLabe…

【Linux】基础IO,软硬链接,动静态库

1. 认识IO 什么是IO I/O简单来说对应的就是两个单词Input和Output&#xff0c;指的是计算机系统与外部环境&#xff08;通常是硬件设备或其他计算机系统&#xff09;之间的数据交换过程 I/O 可以分为两种主要类型&#xff1a; 输入&#xff08;Input&#xff09;&#xff1a; …

人工智能训练师

人工智能训练师是一个较新的职业&#xff0c;2020年2月才被正式纳入国家职业分类目录。他们主要负责在人工智能产品使用过程中进行数据库管理、算法参数设置、人机交互设计、性能测试跟踪及其他辅助作业。 这个职业的背景源于AI公司从客户&#xff08;用户&#xff09;那里获取…

【SpringMVC】文件上传与下载、JREBEL使用

目录 一、引言 二、文件的上传 1、单文件上传 1.1、数据表准备 1.2、添加依赖 1.3、配置文件 1.4、编写表单 1.5、编写controller层 2、多文件上传 2.1、编写form表单 2.2、编写controller层 2.3、测试 三、文件下载 四、JREBEL使用 1、下载注册 2、离线设置 一…

uniapp视频播放功能

UniApp提供了多种视频播放组件&#xff0c;包括视频播放器&#xff08;video&#xff09;、多媒体组件&#xff08;media&#xff09;、WebView&#xff08;内置Video标签&#xff09;等。其中&#xff0c;video和media组件是最常用的。 video组件 video组件是基于HTML5 vide…

Windows Server 2012 R2系统远程桌面的数字证书算法SHA1升级到SHA256

问题描述&#xff1a; 最近项目进行密评的时候&#xff0c;Windows Server 2012 R2发现了以下证书问题&#xff1a; Windows Server 2012 R2系统远程桌面的TLS 1.2协议使用SHA1算法数字证书&#xff0c;且证书有效日期截止23年10月&#xff0c;建议注意证书到期时间&#xff…

JDK14特性——其他变化

文章目录 友好的空指针异常提示JAVA打包工具JFR事件流简介JFR使用JMC工具JFR事件JFR事件流 外部存储器API (孵化阶段)非易失性映射字节缓冲区 友好的空指针异常提示 NullpointerException是java开发中经常遇见的问题&#xff0c;在JDK14之前的版本中&#xff0c;空指针异常的提…

WebGL 计算点光源下的漫反射光颜色

目录 点光源光 逐顶点光照&#xff08;插值&#xff09; 示例程序&#xff08;PointLightedCube.js&#xff09; 代码详解 示例效果 逐顶点处理点光源光照效果时出现的不自然现象 更逼真&#xff1a;逐片元光照 示例程序&#xff08;PointLightedCube_perFragment.js…

农民朋友有福利啦!建行江门市分行“裕农通+农资结算”平台正式上线

随着广东广圣农业发展有限公司办公室内的裕农通“智慧眼”结算机“叮”的一声到账提醒&#xff0c;标志着全国首个“裕农通农资结算“平台的成功上线&#xff0c;也标志着建行广东省江门市分行的裕农通业务又迈上了一个新的台阶。 广东广圣农业发展有限公司&#xff08;以下简…