通过layout实现可拖拽自动排序的UICollectionView
原文链接:http://www.jianshu.com/p/8d1bf1838882
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
Translate from http://blog.karmadust.com/drag-and-rearrange-uicollectionviews-through-layouts/
(Github上的代码 - 使用XCode6.3编译)
我们将会在UICollectionView上添加很多功能。使得CollectionViewCell具备能够被拖拽并重新在上面找到新的位置的功能。
为了实现这些需求,我们需要:
1. 添加一些手势,在这个例子中,我们使用长按手势,这个手势能够很明显的辨别出用户想要拖拽哪个Cell
2. 设置一个引用这个CollectionView的对象,用于处理手势的代理(UIGestureDelegate)和拖拽的动作(Dragging Action)
3. 创建一个Cell的截图(是一个UIImageView),隐藏原始的那个Cell,这样我们就能够只操作它的截图。然后我们把这个截图田间驾到一个父View上,我们把这个View叫做canvas
4. 当我们拖拽经过另一个Cell的时候,我们应该先交换CollectionView的对应的两个数据源(如果你的CollectionView是数据驱动的,那这是非常重要的一点)并交换两个Cell的位置
5. 当用户放掉Cell的时候,我们从canvas上面移除这个截图,并且显示出原来的那个Cell
设计
首先我们必须要做的决定是手势识别我们应该放在哪里。有很多的选择,这其实很随意,但是这次我们选择将这些放在UICollectionView的Layout类中。这使得为我们的代码耦合度更低,我们只需要将一个文件拖拽到工程中,简单的使用这个类的对象代替原本的CollectionView的Layout属性,就能够达到拖拽并重新排序的功能。
我们创建KDRearrangeableCollectionViewFlowLayout.swift
class KDRearrangeableCollectionViewFlowLayout: UICollectionViewFlowLayout, UIGestureRecognizerDelegate {
var canvas : UIView? {
didSet {
if canvas != nil {
self.calculateBorders()
}
}
}
override init() {
super.init()
self.setup()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
self.setup()
}
func setup() {
if let collectionView = self.collectionView {
let gesture = UILongPressGestureRecognizer(target: self,
action: "handleGesture:")
gesture.minimumPressDuration = 0.2
gesture.delegate = self
collectionView.addGestureRecognizer(gesture)
}
}
override func prepareLayout() {
super.prepareLayout()
if self.canvas == nil {
self.canvas = self.collectionView!.superview
}
self.calculateBorders()
}
}
我们需要决定我们在哪里绘制这个移动的截图,如果这个View没有被明确地声明,我们就使用CollectionView的父View,这看起来是我们想要的。(这句实在翻译的不好)
当我们拖拽的时候,我们需要引用这个被拖拽的Cell的原Cell,它的截图,和从点击开始移动的距离。
同时我们应该追踪当前的Cell的位置,所以我们最好顶一个叫做Bundle的结构体来保存这些信息,并把它加入到Layout类中
struct Bundle {
var offset : CGPoint = CGPointZero
var sourceCell : UICollectionViewCell
var representationImageView : UIView
var currentIndexPath : NSIndexPath
var canvas : UIView
}
var bundle : Bundle?
offset是cell原始位置到用户手指位置的距离,这记录了我们拖拽的距离,而不是截图的位置到手指的位置的距离UIGestureRecognizerDelegate
定义了gestureRecognizerShouldBegin
方法,让我们有机会能够在发现手势没有发生在Cell上的时候停止手势。我们遍历CollectionView中的所有的Cell,转换它们的frame到Canvas的坐标,并对这些坐标和我们的手势坐标经行碰撞检测。当我们发现我们所进行操作的Cell之后,我们初始化bundl的对象,是否初始化了这个对象将成为将来我们判断是否要对这次手势处理的标志。
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
if let ca = self.canvas {
if let cv = self.collectionView {
let pointPressedInCanvas = gestureRecognizer.locationInView(ca)
for cell in cv.visibleCells() as [UICollectionViewCell] {
let cellInCanvasFrame = ca.convertRect(cell.frame, fromView: cv)
if CGRectContainsPoint(cellInCanvasFrame, pointPressedInCanvas ) {
let representationImage = cell.snapshotViewAfterScreenUpdates(true)
representationImage.frame = cellInCanvasFrame
let offset = CGPointMake(pointPressedInCanvas.x - cellInCanvasFrame.origin.x, pointPressedInCanvas.y - cellInCanvasFrame.origin.y)
let indexPath : NSIndexPath = cv.indexPathForCell(cell as UICollectionViewCell)!
self.bundle = Bundle(offset: offset, sourceCell: cell, representationImageView:representationImage, currentIndexPath: indexPath)
break
}
}
}
}
return (self.bundle != nil)
}
拖拽Cell
现在,又出现了新的问题。UILongPressGestureRecognizer
有3个状态是我们该注意的,分别是Began
, Changed
和 Ended
,首先,我们隐藏拖拽的Cell,并将截图的View添加到Canvas上
if gesture.state == UIGestureRecognizerState.Began {
bundle.sourceCell.hidden = true
bundle.canvas.addSubview(bundle.representationImageView)
}
当我们拖拽的时候我们需要更新截图的View的位置,获取我们手指位置当前的indexPath并检验是否是最后的一个Cell然后将结果存入Bundle中,如果indexPath变化了,我们需要交换两个Cell的位置
if gesture.state == UIGestureRecognizerState.Changed {
// Update the representation image
var imageViewFrame = bundle.representationImageView.frame
var point = CGPointZero
point.x = dragPointOnCanvas.x - bundle.offset.x
point.y = dragPointOnCanvas.y - bundle.offset.y
imageViewFrame.origin = point
bundle.representationImageView.frame = imageViewFrame
if let indexPath = self.collectionView?.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) {
self.checkForDraggingAtTheEdgeAndAnimatePaging(gesture)
if indexPath.isEqual(bundle.currentIndexPath) == false {
if let delegate = self.collectionView!.delegate as? KDRearrangeableCollectionViewDelegate {
delegate.moveDataItem(bundle.currentIndexPath, toIndexPath: indexPath)
}
self.collectionView!.moveItemAtIndexPath(bundle.currentIndexPath, toIndexPath: indexPath)
self.bundle!.currentIndexPath = indexPath
}
}
}
在最后我们会将bundle的值更新,我们注意到我们有行代码moveDataItem
能够交换cell对应的data,这个方法是可选的。为了实现这个,我们需要创建一个接口并在里面加入一个可以通过indexPath移动数据的方法
@objc protocol KDRearrangeableCollectionViewDelegate : UICollectionViewDelegate {
func moveDataItem(fromIndexPath : NSIndexPath, toIndexPath: NSIndexPath) -> Void
}
项目中的Controller会像这么实现:
// MARK: - KDRearrangeableCollectionViewDelegate
func moveDataItem(fromIndexPath : NSIndexPath, toIndexPath: NSIndexPath) {
let name = self.data[fromIndexPath.item]
self.data.removeAtIndex(fromIndexPath.item)
self.data.insert(name, atIndex: toIndexPath.item)
}
最后,在End
的时候我们需要移除截图的View并且显示出原始的Cell,我们检查是否实现了delegate
并reloadData
。
if gestureRecognizer.state == UIGestureRecognizerState.Ended {
bundle.sourceCell.hidden = false
bundle.representationImageView.removeFromSuperview()
if let delegate = self.collectionView?.delegate as? KDRearrangeableCollectionViewDelegate {
bundle.sourceCollectionView.reloadData()
}
bundle = nil
}
页面移动
有些东西会消失,会随着页面移动。当我们拖拽到collectionView的边界的时候无论是横向还是竖向我们需要将页面滑动到下一页,我们需要定义一些“零界点”来触发翻页。
我们定义了一个方法checkForDraggingAtTheEdgeAndAnimatePaging
就像他的命名上写的一样,这个方法检验是否在边界并翻页,我们可以提前缓存4个零界区域,将他们保存在一个Dictionary
中,就像这个代码中一样。唯一值得提及的是我们如果要翻很多页。我们就要设置一个计时器并且每次都做检测。如果这个截图的图片还是在这块区域上面,我们就继续翻页。
if !CGRectEqualToRect(nextPageRect, self.collectionView!.bounds){ // animate
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(0.8 * Double(NSEC_PER_SEC)))
dispatch_after(delayTime, dispatch_get_main_queue(), {
self.animating = false
self.handleGesture(gestureRecognizer)
});
self.animating = true
self.collectionView!.scrollRectToVisible(nextPageRect, animated: true)
}
试一试吧!
更多关于UICollectionView的
教程
通过layout实现可拖拽自动排序的UICollectionView的更多相关文章
- VUE +element el-table运用sortable 拖拽table排序,实现行排序,列排序
Sortable.js是一款轻量级的拖放排序列表的js插件(虽然体积小,但是功能很强大) 项目需求是要求能对element中 的table进行拖拽行排序 这里用到了sorttable Sortable ...
- js 利用jquery.gridly.js实现拖拽并且排序
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- iOS | 实现拖拽CollectionViewCell排序
现在很多项目都会用到类似拖动的效果,比如今日头条和网易新闻之类的资讯类产品,都有用该技术设置模块顺序的操作. 在iOS9.0之后,苹果提供相关的方法,非常方便. 设定三个私有属性 @property( ...
- Jquery 多行拖拽图片排序 jq优化
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...
- zTree的拖拽排序
ztree本身是可以支持拖拽的,但是却没有找到明确的支持拖拽的排序,也就是说,在拖拽过程中,需要自定义维护拖拽后的顺序并保存至后台. 在这样一个比较常规的需求情况下,网上也有朋友给出了一些解决方案,比 ...
- vue中基于sortablejs与el-upload实现文件上传后拖拽排序
今天做冒烟测试的时候发现商品发布有一个拖拽图片排序功能没做,赶紧加上 之前别的同事基于 vuedraggable 实现过这个功能,我这里自己深度封装了 el-upload ,用这种方式改动很大,而且感 ...
- Android 仿今日头条频道管理(上)(GridView之间Item的移动和拖拽)
前言 常常逛今日头条.发现它的频道管理功能做的特别赞.交互体验很好.如图: watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fo ...
- mp-vue拖拽组件的实现
作为一个效率还不错的小前端,自己的任务做完之后真的好闲啊,千盼万盼终于盼来了业务的新需求,他要我多加一个排序题,然后用户通过拖拽来排序,项目经理看我是个实习生,说有点复杂做不出来就算了,我这么闲的一个 ...
- 【拖拽可视化大屏】全流程讲解用python的pyecharts库实现拖拽可视化大屏的背后原理,简单粗暴!
"整篇文章较长,干货很多!建议收藏后,分章节阅读." 一.设计方案 整体设计方案思维导图: 整篇文章,也将按照这个结构来讲解. 若有重点关注部分,可点击章节目录直接跳转! 二.项目 ...
随机推荐
- 仿写Windows7桌面和任务栏 HTML5+CSS3+Jquery实现
过去一段时间零零散散的自学了一点点jquery的相关用法,基本上属于用到哪个了,就去查然后就学一点,没有系统的学过,深入的用法也不是特别了解,毕竟javascript基础就比较薄弱.经过一段时间的零敲 ...
- NotImplementedException未实现该方法或操作
使用DevExpress为控件CheckedListBoxControl绑定DataSource时,引发异常“NotImplementedException未实现该方法或操作”,代码如下: this. ...
- div中英文无法自动换行的解决办法
在一个设定好宽度的div中,当我们输入的中文文字长度超过了设定宽度时,会自动换到下一行. 但是,如果输入的是英文字母,那么,无论你div设定宽度为多少,英文字母都是不换行直接在同一行输出,导致di ...
- sql - 修改结构
1,修改表名 语法: sp_rename old_table_name, new_table_name 例如: sp_rename t_review, t_business 2,修改字段: MySQL ...
- SQL Server 分组后取Top N
SQL Server 分组后取Top N(转) 近日,工作中突遇一需求:将一数据表分组,而后取出每组内按一定规则排列的前N条数据.乍想来,这本是寻常查询,无甚难处.可提笔写来,终究是困住了笔者好一会儿 ...
- [转]Windows中的句柄(handle)
1.句柄是什么? 在windows中,句柄是和对象一一对应的32位无符号整数值.对象可以映射到唯一的句柄,句柄也可以映射到唯一的对象.2.为什么我们需要句柄? 更准确地说,是windows需要 ...
- Integer和int的详细比较(转)
Integer与int的区别我们耳熟详的有两点:1.Integer是int的包装类.2.Integer的默认初始值是null,而int的默认初试值是0. 下面通过代码进行详细比较. public cl ...
- .net防止刷新重复提交(转)
net 防止重复提交 微软防止重复提交方案,修改版 Code ; i < cookie.Values.Count; i++) log.Info(" ...
- 大整数算法[09] Comba乘法(原理)
★ 引子 原本打算一篇文章讲完,后来发现篇幅会很大,所以拆成两部分,先讲原理,再讲实现.实现的话相对复杂,要用到内联汇编,要考虑不同平台等等. 在大整数计算中,乘法是非常重要的,因为 ...
- HBase笔记--自定义filter
自定义filter需要继承的类:FilterBase 类里面的方法调用顺序 方法名 作用 1 boolean filterRowKey(Cell cell) 根据row key过滤row.如果需要 ...