经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » 编程经验 » 查看文章
[MAUI]实现动态拖拽排序网格
来源:cnblogs  作者:林晓lx  时间:2023/9/19 8:40:35  对本文有异议

@


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

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

在这里插入图片描述

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

创建页面元素

新建.NET MAUI项目,命名Tile

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

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

创建GridTilesPage.xaml

在页面中创建CollectionView,

  1. <CollectionView Grid.Row="1"
  2. x:Name="MainCollectionView"
  3. ItemsSource="{Binding TileSegments}">
  4. <CollectionView.ItemTemplate>
  5. <DataTemplate>
  6. <ContentView HeightRequest="110" WidthRequest="110" HorizontalOptions="Center" VerticalOptions="Center">
  7. <StackLayout>
  8. <StackLayout.GestureRecognizers>
  9. <DropGestureRecognizer AllowDrop="True"
  10. DragLeaveCommand="{Binding DragLeave}"
  11. DragLeaveCommandParameter="{Binding}"
  12. DragOverCommand="{Binding DraggedOver}"
  13. DragOverCommandParameter="{Binding}"
  14. DropCommand="{Binding Dropped}"
  15. DropCommandParameter="{Binding}" />
  16. </StackLayout.GestureRecognizers>
  17. <Border x:Name="ContentLayout"
  18. StrokeThickness="0"
  19. Margin="0">
  20. <Grid>
  21. <Grid.GestureRecognizers>
  22. <DragGestureRecognizer CanDrag="True"
  23. DragStartingCommand="{Binding Dragged}"
  24. DragStartingCommandParameter="{Binding}" />
  25. </Grid.GestureRecognizers>
  26. <controls1:TileSegmentView HeightRequest="100"
  27. WidthRequest="100"
  28. Margin="5,5">
  29. </controls1:TileSegmentView>
  30. <Button CornerRadius="100"
  31. HeightRequest="20"
  32. WidthRequest="20"
  33. Padding="0"
  34. Margin="2,2"
  35. BackgroundColor="Red"
  36. TextColor="White"
  37. Command="{Binding Remove}"
  38. Text="×"
  39. HorizontalOptions="End"
  40. VerticalOptions="Start"></Button>
  41. </Grid>
  42. </Border>
  43. </StackLayout>
  44. </ContentView>
  45. </DataTemplate>
  46. </CollectionView.ItemTemplate>
  47. <CollectionView.ItemsLayout>
  48. <GridItemsLayout Orientation="Vertical"
  49. Span="3" />
  50. </CollectionView.ItemsLayout>
  51. </CollectionView>

呈现效果如下:

在这里插入图片描述

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

创建IDraggableItem接口, 此处定义拖动相关的属性和命令。

  1. public interface IDraggableItem
  2. {
  3. bool IsBeingDraggedOver { get; set; }
  4. bool IsBeingDragged { get; set; }
  5. Command Dragged { get; set; }
  6. Command DraggedOver { get; set; }
  7. Command DragLeave { get; set; }
  8. Command Dropped { get; set; }
  9. object DraggedItem { get; set; }
  10. object DropPlaceHolderItem { get; set; }
  11. }

Dragged: 拖拽开始时触发的命令。
DraggedOver: 拖拽控件悬停在当前控件上方时触发的命令。
DragLeave: 拖拽控件离开当前控件时触发的命令。
Dropped: 拖拽控件放置在当前控件上方时触发的命令。

IsBeingDragged 为true时,通知当前控件正在被拖拽。
IsBeingDraggedOver 为true时,通知当前控件正在有拖拽控件悬停在其上方。

DraggedItem: 正在拖拽的控件。
DropPlaceHolderItem: 悬停在其上方时的控件,即当前控件的占位控件。

创建一个TileSegement类,用于描述磁贴可显示的属性,如标题、描述、图标、颜色等。

  1. public class TileSegment
  2. {
  3. public string Title { get; set; }
  4. public string Type { get; set; }
  5. public string Desc { get; set; }
  6. public string Icon { get; set; }
  7. public Color Color { get; set; }
  8. }

创建可绑定对象

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

  1. private ObservableCollection<ITileSegmentService> _tileSegments;
  2. public ObservableCollection<ITileSegmentService> TileSegments
  3. {
  4. get { return _tileSegments; }
  5. set
  6. {
  7. _tileSegments = value;
  8. OnPropertyChanged();
  9. }
  10. }

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

  1. public GridTilesPageViewModel()
  2. {
  3. TileSegments = new ObservableCollection<ITileSegmentService>();
  4. CreateSegmentAction("TileSegment", "App1", "Some description here", Colors.LightPink);
  5. CreateSegmentAction("TileSegment", "App2", "Some description here", Colors.LightGreen);
  6. ...
  7. }
  1. private ITileSegmentService CreateTileSegmentService(object obj, string title, string desc, Color color)
  2. {
  3. var type = obj as string;
  4. var tileSegment = new TileSegment()
  5. {
  6. Title = title,
  7. Type = type,
  8. Desc = desc,
  9. Icon = "dotnet_bot.svg",
  10. Color = color,
  11. };
  12. var newModel = new GridTileSegmentService(tileSegment);
  13. if (newModel != null)
  14. {
  15. newModel.Container = this;
  16. }
  17. return newModel;
  18. }

创建绑定服务类

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

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

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

  1. public class GridTileSegmentService : ObservableObject, ITileSegmentService
  2. {
  3. public GridTileSegmentService(TileSegment tileSegment)
  4. {
  5. TileSegment = tileSegment;
  6. Dragged = new Command(OnDragged);
  7. DraggedOver = new Command(OnDraggedOver);
  8. DragLeave = new Command(OnDragLeave);
  9. Dropped = new Command(i => OnDropped(i));
  10. this.PropertyChanged+=GridTileSegmentService_PropertyChanged;
  11. }
  12. ...
  13. }

拖拽(Drag)

拖拽开始时,将IsBeingDragged设置为true,通知当前控件正在被拖拽,同时将DraggedItem设置为当前控件。

  1. private void OnDragged(object item)
  2. {
  3. IsBeingDragged=true;
  4. DraggedItem=item;
  5. }

拖拽悬停,经过(DragOver)

拖拽控件悬停在当前控件上方时,将IsBeingDraggedOver设置为true,通知当前控件正在有拖拽控件悬停在其上方,同时在服务列表中寻找当前正在被拖拽的服务,将DropPlaceHolderItem设置为当前控件。

  1. private void OnDraggedOver(object item)
  2. {
  3. if (!IsBeingDragged && item!=null)
  4. {
  5. var itemToMove = Container.TileSegments.First(i => i.IsBeingDragged);
  6. if (itemToMove.DraggedItem!=null)
  7. {
  8. DropPlaceHolderItem=itemToMove.DraggedItem;
  9. }
  10. IsBeingDraggedOver=true;
  11. }
  12. }

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

  1. private void OnDragLeave(object item)
  2. {
  3. IsBeingDraggedOver = false;
  4. DropPlaceHolderItem = null;
  5. }

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

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

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

  1. private void GridTileSegmentService_PropertyChanged(object sender, PropertyChangedEventArgs e)
  2. {
  3. if (e.PropertyName==nameof(this.IsBeingDraggedOver))
  4. {
  5. if (this.IsBeingDraggedOver && DropPlaceHolderItem!=null)
  6. {
  7. var newIndex = Container.TileSegments.IndexOf(this);
  8. var oldIndex = Container.TileSegments.IndexOf(DropPlaceHolderItem as ITileSegmentService);
  9. Container.TileSegments.Move(oldIndex, newIndex);
  10. }
  11. }
  12. }

效果如下:

在这里插入图片描述

释放(Drop)

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

  1. private void OnDropped(object item)
  2. {
  3. var itemToMove = Container.TileSegments.First(i => i.IsBeingDragged);
  4. if (itemToMove == null)
  5. return;
  6. itemToMove.IsBeingDragged = false;
  7. IsBeingDraggedOver = false;
  8. DraggedItem=null;
  9. DropPlaceHolderItem = null;
  10. }

完整的TileSegmentService代码如下:

  1. public class GridTileSegmentService : ObservableObject, ITileSegmentService
  2. {
  3. public GridTileSegmentService(
  4. TileSegment tileSegment)
  5. {
  6. Remove = new Command(RemoveAction);
  7. TileSegment = tileSegment;
  8. Dragged = new Command(OnDragged);
  9. DraggedOver = new Command(OnDraggedOver);
  10. DragLeave = new Command(OnDragLeave);
  11. Dropped = new Command(i => OnDropped(i));
  12. this.PropertyChanged+=GridTileSegmentService_PropertyChanged;
  13. }
  14. private void GridTileSegmentService_PropertyChanged(object sender, PropertyChangedEventArgs e)
  15. {
  16. if (e.PropertyName==nameof(this.IsBeingDraggedOver))
  17. {
  18. if (this.IsBeingDraggedOver && DropPlaceHolderItem!=null)
  19. {
  20. var newIndex = Container.TileSegments.IndexOf(this);
  21. var oldIndex = Container.TileSegments.IndexOf(DropPlaceHolderItem as ITileSegmentService);
  22. Container.TileSegments.Move(oldIndex, newIndex);
  23. }
  24. }
  25. }
  26. private void OnDragged(object item)
  27. {
  28. IsBeingDragged=true;
  29. DraggedItem=item;
  30. }
  31. private void OnDraggedOver(object item)
  32. {
  33. if (!IsBeingDragged && item!=null)
  34. {
  35. var itemToMove = Container.TileSegments.First(i => i.IsBeingDragged);
  36. if (itemToMove.DraggedItem!=null)
  37. {
  38. DropPlaceHolderItem=itemToMove.DraggedItem;
  39. }
  40. IsBeingDraggedOver=true;
  41. }
  42. }
  43. private object _draggedItem;
  44. public object DraggedItem
  45. {
  46. get { return _draggedItem; }
  47. set
  48. {
  49. _draggedItem = value;
  50. OnPropertyChanged();
  51. }
  52. }
  53. private object _dropPlaceHolderItem;
  54. public object DropPlaceHolderItem
  55. {
  56. get { return _dropPlaceHolderItem; }
  57. set
  58. {
  59. _dropPlaceHolderItem = value;
  60. OnPropertyChanged();
  61. }
  62. }
  63. private void OnDragLeave(object item)
  64. {
  65. IsBeingDraggedOver = false;
  66. DropPlaceHolderItem = null;
  67. }
  68. private void OnDropped(object item)
  69. {
  70. var itemToMove = Container.TileSegments.First(i => i.IsBeingDragged);
  71. if (itemToMove == null)
  72. return;
  73. itemToMove.IsBeingDragged = false;
  74. IsBeingDraggedOver = false;
  75. DraggedItem=null;
  76. DropPlaceHolderItem = null;
  77. }
  78. private async void RemoveAction(object obj)
  79. {
  80. if (Container is ITileSegmentServiceContainer)
  81. {
  82. (Container as ITileSegmentServiceContainer).RemoveSegment.Execute(this);
  83. }
  84. }
  85. public IReadOnlyTileSegmentServiceContainer Container { get; set; }
  86. private TileSegment tileSegment;
  87. public TileSegment TileSegment
  88. {
  89. get { return tileSegment; }
  90. set
  91. {
  92. tileSegment = value;
  93. OnPropertyChanged();
  94. }
  95. }
  96. private bool _isBeingDragged;
  97. public bool IsBeingDragged
  98. {
  99. get { return _isBeingDragged; }
  100. set
  101. {
  102. _isBeingDragged = value;
  103. OnPropertyChanged();
  104. }
  105. }
  106. private bool _isBeingDraggedOver;
  107. public bool IsBeingDraggedOver
  108. {
  109. get { return _isBeingDraggedOver; }
  110. set
  111. {
  112. if (value!=_isBeingDraggedOver)
  113. {
  114. _isBeingDraggedOver = value;
  115. OnPropertyChanged();
  116. }
  117. }
  118. }
  119. public Command Remove { get; set; }
  120. public Command Dragged { get; set; }
  121. public Command DraggedOver { get; set; }
  122. public Command DragLeave { get; set; }
  123. public Command Dropped { get; set; }
  124. }

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

限流(Throttle)和防抖(Debounce)

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

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

在这里插入图片描述

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

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

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

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

在这里插入图片描述

代码引用自 ThrottleDebounce

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

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

改写GridTileSegmentService_PropertyChanged如下:

  1. private void GridTileSegmentService_PropertyChanged(object sender, PropertyChangedEventArgs e)
  2. {
  3. if (e.PropertyName==nameof(this.IsBeingDraggedOver))
  4. {
  5. if (this.IsBeingDraggedOver && DropPlaceHolderItem!=null)
  6. {
  7. lock (throttledLocker)
  8. {
  9. var newIndex = Container.TileSegments.IndexOf(this);
  10. var oldIndex = Container.TileSegments.IndexOf(DropPlaceHolderItem as ITileSegmentService);
  11. var originalAction = () =>
  12. {
  13. Container.TileSegments.Move(oldIndex, newIndex);
  14. };
  15. throttledAction.Update(originalAction);
  16. throttledAction.Invoke();
  17. }
  18. }
  19. }
  20. }

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

在这里插入图片描述

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

在这里插入图片描述

项目地址

Github:maui-samples

原文链接:https://www.cnblogs.com/jevonsflash/p/17712687.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号