注册
iOS

iOS中UICollectionView的item增删、拖拽和排序动画

效果图


faaa98d32b5abf00a80454c9474e99c7.gif


这个是前段时间项目新增的一个功能,刚刚开始组员是用UIScrollView + UIView 实现的,但这种实现方式属实是有点low,后续闲暇时笔者用UICollectionView简单实现了下。


思路


简单理一下思路,首先是把整个页面先布局出来,这里涉及到一个UICollectionViewSection背景色的问题,有需要的可以点这里,有详细的介绍。移动动画也很简单,先获取起点cell和终点cell,再新建一个动画的AnimationItem,根据获取的起点和终点cell动画就行,最后再实现拖拽排序效果。


大致总结一下:

  • 布局

  • 移动动画

  • 拖拽排序


下面就根据思路一步步来。我们一定要有一个意识,不管是多么复杂的动画,只要把它分解开来,按步骤一步一步实现就很简单。


实现


我们就跟上面的思路一步一步实现。


布局


首先想到肯定是新建一个Model来管理这个数据,新建Model也要有点技巧。

struct ItemModel {
    var section: Int = -1 // cell的section索引
    var item: Int = -1 // cell的item索引
    var name: String = "" // 名称
    var isAdded: Bool = false // 是否添加到首页应用(第0区)
    var id: String { // 唯一标识,可以用这个来命名图片的名称,也可以用来作判断
        get {
            "\(section)_\(item)"
        }
    }
    init(){}
}

看得出来,ItemModel的属性section + item = IndexPath,可以根据 model 知道当前cell的所在位置了。


笔者这用的是 struct ,感觉用 class 会更好点,因为后续会改变数组中Model的属性值。已经写了就懒得再改了。


存在2个数组数据:

  • var editItems = [ItemModel](),由前一页传入的、可编辑、拖拽的数据,位于UICollectionView的第0个Section。

  • var datas = [[ItemModel]](),按照Section的顺序,存放所有的数据。


注意:Section要从1开始,因为第0个Section是可以编辑拖拽的区域。


datas中存放全部的数据:

for i in 0..<names.count {
let subNames = names[i]
var items = [ItemModel]()
for j in 0..<subNames.count {
var model = ItemModel()
model.section = i+1 // 注意这里的Section要从1开始
model.item = j
model.name = subNames[j]
model.isAdded = editItems.contains(where: { $0.id == model.id})
items.append(model)
}
datas.append(items)
}

根据数据布局UICollectionView


移动动画


移动只要2个操作,添加应用和删除应用。


添加


笔者这里规定了最多可以添加8个应用。


大致思路:

  • 获取当前点击的 cell,为了得到其坐标作为动画起始位置

  • 在 collectionView 中插入一个空白的 cell 占位,此举是为了增加或减少行数的动画过渡更自然;对应也应该在 editItems 中添加一个空白的 model 作为数据源,等移动动画结束后再给model重新赋值。

  • 获取新插入的空白 cell,为了得到其坐标作为动画的结束位置

  • 生成动画的 cell,起始 -> 结束 动画。

  • 更新数据,刷新


删除


思路与添加雷同,且比之更简单


具体的思路和步骤,代码中都有一步步的注释,可自行查阅。


拖拽排序


这个拖拽排序,在iOS11之前的比较麻烦,都是靠自己计算,这里也简单说下思路:


iOS11.0之前的实现思路

  1. 在UICollectionView上添加一个长按的手势

  2. 在UICollectionView上面添加一个浮动隐藏的cell,便于拖拽

  3. 通过长按操作找到需要被拖动的cellA

  4. 通过拖动cellA找到找到和它交换位置的cellB

  5. 交换cellA和cellB的位置

  6. 替换数据源,把起始位置的数据模型删除,然后将起始位置的数据模型插入到拖拽位置


这种比较复杂的是结合位置判断需要交换的cell。但是在iOS11之后,UICollectionView新增了dragDelegatedropDelegate,用来实现拖拽排序的效果。


dragDelegate、dropDelegate


直接上代码:

collectionView.dragDelegate = self
collectionView.dropDelegate = self
collectionView.dragInteractionEnabled = true
collectionView.reorderingCadence = .immediate
collectionView.isSpringLoaded = true

  • dragInteractionEnabled 属性要设置为 true,才可以进行 drag 操作。此属性在 iPad 默认是 true,在 iPhone 默认是 false。

  • reorderingCadence 重排序节奏,可以调节集合视图重排序的响应性。

  • UICollectionViewReorderingCadenceImmediate 默认值。当开始移动的时候就立即回流集合视图布局,实时的重新排序。

  • UICollectionViewReorderingCadenceFast 快速移动,不会立即重新布局,只有在停止移动的时候才会重新布局

  • UICollectionViewReorderingCadenceSlow 停止移动再过一会儿才会开始回流,重新布局

  • isSpringLoaded 弹性加载效果,也可以使用代理方法:func collectionView(_ collectionView: UICollectionView, shouldSpringLoadItemAt indexPath: IndexPath, with context: UISpringLoadedInteractionContext) -> Bool。

需要实现UICollectionViewDropDelegateUICollectionViewDragDelegate协议方法。下面是常用的几个方法,按照调用的先后顺序说明一下:

/*
* 识别到拖动,一次拖动一个;若一次拖动多个,则需要选中多个
* 提供一个给定 indexPath 的可进行 drag 操作的 item
* NSItemProvider, 拖放处理时,携带数据的容器,通过对象初始化,该对象需满足 NSItemProviderWriting 协议
*/
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem]

/*
* 使用自定义预览,如果该方法没有实现或者返回nil,那么整个 cell 将用于预览
* UIDragPreviewParameters有2个属性:backgroundColor设置背景颜色;visiblePath设置视图的可见区域
* 笔者使用这个方法除去了拖拽过程中item的阴影
*/
func collectionView(_ collectionView: UICollectionView, dragPreviewParametersForItemAt indexPath: IndexPath) -> UIDragPreviewParameters?

/*
* 开始拖拽后,继续添加拖拽的任务,处理雷同`itemsForBeginning`方法
*/
func collectionView(_ collectionView: UICollectionView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem]

/*
* 拖拽开始,可自行处理
*/
func collectionView(_ collectionView: UICollectionView, dragSessionWillBegin session: UIDragSession)

/*
* 判断对应的 item 能否被执行drop会话,是否能放置
*/
func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool

/*
* 处理拖动中放置的策略,此方法会 频繁调用,在此方法中应尽可能减少工作量。
* 四种分别:move移动;copy拷贝;forbidden禁止,即不能放置;cancel用户取消。
* 效果一般使用2种:.insertAtDestinationIndexPath 挤压移动;.insertIntoDestinationIndexPath 取代。
* 在某些情况下,目标索引路径可能为空(比如拖到一个没有cell的空白区域)你可以通过 session.locationInView 做你自己的命中测试
*/
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal

/*
* 当drop会话进入到 collectionView 的坐标区域内就会调用
*/
func collectionView(_ collectionView: UICollectionView, dropSessionDidEnter session: UIDropSession)

/*
* 结束放置时的处理
* 如果该方法不做任何事,将会执行默认的动画
*/
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator)

/*
* 拖拽开始,可自行处理
*/
func collectionView(_ collectionView: UICollectionView, dragSessionDidEnd session: UIDragSession)

/*
* 当dropSession 完成时会被调用,不管结果如何。一般进行清理或刷新操作
*/
func collectionView(_ collectionView: UICollectionView, dropSessionDidEnd session: UIDropSession)

这里大概的拖拽动画就差不多了,代码中会有更详细的注释。


总结


将一个大功能拆分成一个个小模块,按部就班一步一步实现就不难了。这里只是中间囊括了各种小动画和刷新,设计好思路,不行就多试几次肯定可以。


代码自取:RCDragDropAnimation




若存在什么不对的地方,欢迎指正!


作者:云层之上
链接:https://juejin.cn/post/7246777949100933177
来源:稀土掘金

0 个评论

要回复文章请先登录注册