首页 > 其他分享 >[MAUI]实现动态拖拽排序列表

[MAUI]实现动态拖拽排序列表

时间:2023-09-18 18:15:36浏览次数:52  
标签:控件 set get object public MAUI 排序 拖拽

@

目录
上一章我们使用拖放(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

标签:控件,set,get,object,public,MAUI,排序,拖拽
From: https://www.cnblogs.com/jevonsflash/p/17712687.html

相关文章

  • KingbaseESV8R6汉字首字母排序
    目的本文目的实现汉字首字母排序。排序规则和字符集的关系如下。selectsys_encoding_to_char(collencoding)asencoding,collname,collcollate,collctypefromsys_collation;按照UTF8字符集匹配中文排序规则如下。selectcollcollatefromsys_collationwheresys_encod......
  • natsort.natsorted()-用于自然排序(natural sorting)字符串列表。
    参考:https://natsort.readthedocs.io/en/stable/api.html#natsort.natsorted语法格式natsort.natsorted(seq:Iterable[T],key:Optional[Callable[[T],Union[natsort.utils.SupportsDunderLT,natsort.utils.SupportsDunderGT,None]]]=None,reverse:bool=False,alg:......
  • 八大排序算法介绍及其Java实现
    八大排序算法包括:冒泡排序(BubbleSort)选择排序(SelectionSort)插入排序(InsertionSort)希尔排序(ShellSort)归并排序(MergeSort)快速排序(QuickSort)堆排序(HeapSort)计数排序(CountingSort)下面是它们的复杂度、应用场景以及Java语言描述算法:冒泡排序复杂度:最坏情况下需要O(n^2)的时间复......
  • C语言实现选择法排序
    1.选择法排序的思路:假设有一组无序数,扫描一遍数组,找到最小值与数组下标为0的数交换,接着扫描数组,将数组下标为1的数与最小值交换,直至整个数组为有序数组。2.图示:3.代码实现:#include<stdio.h>#definemax10//假设是一个10个数的无序数组intmain(){ inta[max]; for(inti=......
  • 【java基础】冒泡排序 正序倒叙排序 【一眼就会系列】
    //本节课:1、数组冒泡排序:倒序、正序//1、冒泡正序:intnum[]={345,2,3,777,34,233,97,30};for(inti=0;i<num.length;i++){for(intt=0;t<num.length-1-i;t++){if(num[t]>num[t+1......
  • 王道数据结构:设线性表中每个元素有两个数据项k1和k2,现对线性表按一下规则进行排序:先
    题目:设线性表中每个元素有两个数据项k1和k2,现对线性表按一下规则进行排序:先看数据项k1,k1值小的元素在前,大的在后;在k1值相同的情况下,再看k2,k2值小的在前,大的在后。满足这种要求的排序方法是()A.先按k1进行直接插入排序,再按k2进行简单选择排序B.先按k2进行直接插入排序,再按k1进行简......
  • jquery设置图片可手动拖拽
    JQuery是一款流行的JavaScript框架,可以轻松实现网页交互效果。而其中一种常见效果是图片手动拖拽。以下是设置图片手动拖拽的JQuery代码。$(document).ready(function(){varisDragging=false;varmousePos={x:0,y:0};varelemPos={x:0,y:0};var$elem=$......
  • 基础算法:快速排序、归并排序
    1、快速排序#include<iostream>usingnamespacestd;constintN=1e5+10;intn,q[N];voidqksort(intq[],intl,intr){if(l>=r)return;intx=q[l],i=l-1,j=r+1;while(i<j){doi++;while(q[i]<......
  • 拓扑排序
    有向图的拓扑排序算法JAVA实现 一,问题描述给定一个有向图G=(V,E),将之进行拓扑排序,如果图有环,则提示异常。要想实现图的算法,如拓扑排序、最短路径……并运行看输出结果,首先就得构造一个图。由于构造图的方式有很多种,这里假设图的数据存储在一个文件中,每一行包含如下的信息:Lin......
  • 38-列表-排序-revered逆序-max_min_sum
               迭代器只能用一次,以时间换空间      ......