通过layout实现可拖拽自动排序的UICollectionView

Translate from http://blog.karmadust.com/drag-and-rearrange-uicollectionviews-through-layouts/</br>

Github上的代码 - 使用XCode6.3编译)</br>
我们将会在UICollectionView上添加很多功能。使得CollectionViewCell具备能够被拖拽并重新在上面找到新的位置的功能。

为了实现这些需求,我们需要:

1. 添加一些手势,在这个例子中,我们使用长按手势,这个手势能够很明显的辨别出用户想要拖拽哪个Cell</br>
  2. 设置一个引用这个CollectionView的对象,用于处理手势的代理(UIGestureDelegate)和拖拽的动作(Dragging Action)</br>
  3. 创建一个Cell的截图(是一个UIImageView),隐藏原始的那个Cell,这样我们就能够只操作它的截图。然后我们把这个截图田间驾到一个父View上,我们把这个View叫做canvas</br>
  4. 当我们拖拽经过另一个Cell的时候,我们应该先交换CollectionView的对应的两个数据源(如果你的CollectionView是数据驱动的,那这是非常重要的一点)并交换两个Cell的位置</br>
  5. 当用户放掉Cell的时候,我们从canvas上面移除这个截图,并且显示出原来的那个Cell

设计

首先我们必须要做的决定是手势识别我们应该放在哪里。有很多的选择,这其实很随意,但是这次我们选择将这些放在UICollectionView的Layout类中。这使得为我们的代码耦合度更低,我们只需要将一个文件拖拽到工程中,简单的使用这个类的对象代替原本的CollectionView的Layout属性,就能够达到拖拽并重新排序的功能。

image
image

我们创建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原始位置到用户手指位置的距离,这记录了我们拖拽的距离,而不是截图的位置到手指的位置的距离</br>
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, ChangedEnded,首先,我们隐藏拖拽的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,我们检查是否实现了delegatereloadData

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的教程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容