iOS中UICollectionView的item增删、拖拽和排序动画
效果图
这个是前段时间项目新增的一个功能,刚刚开始组员是用UIScrollView
+ UIView
实现的,但这种实现方式属实是有点low,后续闲暇时笔者用UICollectionView
简单实现了下。
思路
简单理一下思路,首先是把整个页面先布局出来,这里涉及到一个UICollectionView
的Section
背景色的问题,有需要的可以点这里,有详细的介绍。移动动画也很简单,先获取起点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之前的实现思路
在UICollectionView上添加一个长按的手势
在UICollectionView上面添加一个浮动隐藏的cell,便于拖拽
通过长按操作找到需要被拖动的cellA
通过拖动cellA找到找到和它交换位置的cellB
交换cellA和cellB的位置
替换数据源,把起始位置的数据模型删除,然后将起始位置的数据模型插入到拖拽位置
这种比较复杂的是结合位置判断需要交换的cell。但是在iOS11之后,UICollectionView
新增了dragDelegate、dropDelegate,用来实现拖拽排序的效果。
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。
需要实现UICollectionViewDropDelegate
和UICollectionViewDragDelegate
协议方法。下面是常用的几个方法,按照调用的先后顺序说明一下:
/*
* 识别到拖动,一次拖动一个;若一次拖动多个,则需要选中多个
* 提供一个给定 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
来源:稀土掘金