通過(guò)layout實(shí)現(xiàn)可拖拽自動(dòng)排序的UICollectionView

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

Github上的代碼 - 使用XCode6.3編譯)</br>
我們將會(huì)在UICollectionView上添加很多功能。使得CollectionViewCell具備能夠被拖拽并重新在上面找到新的位置的功能。

為了實(shí)現(xiàn)這些需求宁改,我們需要:

1. 添加一些手勢(shì),在這個(gè)例子中队询,我們使用長(zhǎng)按手勢(shì),這個(gè)手勢(shì)能夠很明顯的辨別出用戶想要拖拽哪個(gè)Cell</br>
??2. 設(shè)置一個(gè)引用這個(gè)CollectionView的對(duì)象,用于處理手勢(shì)的代理(UIGestureDelegate)和拖拽的動(dòng)作(Dragging Action)</br>
??3. 創(chuàng)建一個(gè)Cell的截圖(是一個(gè)UIImageView),隱藏原始的那個(gè)Cell擦耀,這樣我們就能夠只操作它的截圖。然后我們把這個(gè)截圖田間駕到一個(gè)父View上甲馋,我們把這個(gè)View叫做canvas</br>
??4. 當(dāng)我們拖拽經(jīng)過(guò)另一個(gè)Cell的時(shí)候埂奈,我們應(yīng)該先交換CollectionView的對(duì)應(yīng)的兩個(gè)數(shù)據(jù)源(如果你的CollectionView是數(shù)據(jù)驅(qū)動(dòng)的迄损,那這是非常重要的一點(diǎn))并交換兩個(gè)Cell的位置</br>
??5. 當(dāng)用戶放掉Cell的時(shí)候定躏,我們從canvas上面移除這個(gè)截圖,并且顯示出原來(lái)的那個(gè)Cell

設(shè)計(jì)

首先我們必須要做的決定是手勢(shì)識(shí)別我們應(yīng)該放在哪里。有很多的選擇痊远,這其實(shí)很隨意垮抗,但是這次我們選擇將這些放在UICollectionView的Layout類中。這使得為我們的代碼耦合度更低碧聪,我們只需要將一個(gè)文件拖拽到工程中冒版,簡(jiǎn)單的使用這個(gè)類的對(duì)象代替原本的CollectionView的Layout屬性,就能夠達(dá)到拖拽并重新排序的功能逞姿。

image
image

我們創(chuàng)建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()
    }
}

我們需要決定我們?cè)谀睦锢L制這個(gè)移動(dòng)的截圖辞嗡,如果這個(gè)View沒(méi)有被明確地聲明,我們就使用CollectionView的父View滞造,這看起來(lái)是我們想要的续室。(這句實(shí)在翻譯的不好)

當(dāng)我們拖拽的時(shí)候,我們需要引用這個(gè)被拖拽的Cell的原Cell谒养,它的截圖挺狰,和從點(diǎn)擊開(kāi)始移動(dòng)的距離。

同時(shí)我們應(yīng)該追蹤當(dāng)前的Cell的位置买窟,所以我們最好頂一個(gè)叫做Bundle的結(jié)構(gòu)體來(lái)保存這些信息丰泊,并把它加入到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方法瞳购,讓我們有機(jī)會(huì)能夠在發(fā)現(xiàn)手勢(shì)沒(méi)有發(fā)生在Cell上的時(shí)候停止手勢(shì)。我們遍歷CollectionView中的所有的Cell亏推,轉(zhuǎn)換它們的frame到Canvas的坐標(biāo)苛败,并對(duì)這些坐標(biāo)和我們的手勢(shì)坐標(biāo)經(jīng)行碰撞檢測(cè)。當(dāng)我們發(fā)現(xiàn)我們所進(jìn)行操作的Cell之后径簿,我們初始化bundl的對(duì)象罢屈,是否初始化了這個(gè)對(duì)象將成為將來(lái)我們判斷是否要對(duì)這次手勢(shì)處理的標(biāo)志。

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

現(xiàn)在篇亭,又出現(xiàn)了新的問(wèn)題缠捌。UILongPressGestureRecognizer有3個(gè)狀態(tài)是我們?cè)撟⒁獾模謩e是Began, ChangedEnded译蒂,首先曼月,我們隱藏拖拽的Cell,并將截圖的View添加到Canvas上

if gesture.state == UIGestureRecognizerState.Began {                
     bundle.sourceCell.hidden = true
     bundle.canvas.addSubview(bundle.representationImageView)
}

當(dāng)我們拖拽的時(shí)候我們需要更新截圖的View的位置柔昼,獲取我們手指位置當(dāng)前的indexPath并檢驗(yàn)是否是最后的一個(gè)Cell然后將結(jié)果存入Bundle中哑芹,如果indexPath變化了,我們需要交換兩個(gè)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
                        
        }          
    }            
}

在最后我們會(huì)將bundle的值更新捕透,我們注意到我們有行代碼moveDataItem能夠交換cell對(duì)應(yīng)的data聪姿,這個(gè)方法是可選的碴萧。為了實(shí)現(xiàn)這個(gè),我們需要?jiǎng)?chuàng)建一個(gè)接口并在里面加入一個(gè)可以通過(guò)indexPath移動(dòng)數(shù)據(jù)的方法

@objc protocol KDRearrangeableCollectionViewDelegate : UICollectionViewDelegate {
func moveDataItem(fromIndexPath : NSIndexPath, toIndexPath: NSIndexPath) -> Void
}

項(xiàng)目中的Controller會(huì)像這么實(shí)現(xiàn):

// 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的時(shí)候我們需要移除截圖的View并且顯示出原始的Cell破喻,我們檢查是否實(shí)現(xiàn)了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          
}
頁(yè)面移動(dòng)

有些東西會(huì)消失盟榴,會(huì)隨著頁(yè)面移動(dòng)曹质。當(dāng)我們拖拽到collectionView的邊界的時(shí)候無(wú)論是橫向還是豎向我們需要將頁(yè)面滑動(dòng)到下一頁(yè),我們需要定義一些“零界點(diǎn)”來(lái)觸發(fā)翻頁(yè)擎场。

我們定義了一個(gè)方法checkForDraggingAtTheEdgeAndAnimatePaging就像他的命名上寫(xiě)的一樣羽德,這個(gè)方法檢驗(yàn)是否在邊界并翻頁(yè),我們可以提前緩存4個(gè)零界區(qū)域迅办,將他們保存在一個(gè)Dictionary中玩般,就像這個(gè)代碼中一樣。唯一值得提及的是我們?nèi)绻芏囗?yè)礼饱。我們就要設(shè)置一個(gè)計(jì)時(shí)器并且每次都做檢測(cè)坏为。如果這個(gè)截圖的圖片還是在這塊區(qū)域上面,我們就繼續(xù)翻頁(yè)镊绪。

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)
                
}

試一試吧匀伏!
更多關(guān)于UICollectionView的教程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蝴韭,隨后出現(xiàn)的幾起案子够颠,更是在濱河造成了極大的恐慌,老刑警劉巖榄鉴,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件履磨,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡庆尘,警方通過(guò)查閱死者的電腦和手機(jī)剃诅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)驶忌,“玉大人矛辕,你說(shuō)我怎么就攤上這事「赌В” “怎么了聊品?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)几苍。 經(jīng)常有香客問(wèn)我翻屈,道長(zhǎng),這世上最難降的妖魔是什么妻坝? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任伸眶,我火速辦了婚禮惊窖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘赚抡。我一直安慰自己爬坑,他們只是感情好纠屋,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布涂臣。 她就那樣靜靜地躺著,像睡著了一般售担。 火紅的嫁衣襯著肌膚如雪赁遗。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,185評(píng)論 1 284
  • 那天族铆,我揣著相機(jī)與錄音岩四,去河邊找鬼。 笑死哥攘,一個(gè)胖子當(dāng)著我的面吹牛剖煌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播逝淹,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼耕姊,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了栅葡?” 一聲冷哼從身側(cè)響起茉兰,我...
    開(kāi)封第一講書(shū)人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎欣簇,沒(méi)想到半個(gè)月后规脸,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡熊咽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年莫鸭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片横殴。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡黔龟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出滥玷,到底是詐尸還是另有隱情氏身,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布惑畴,位于F島的核電站蛋欣,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏如贷。R本人自食惡果不足惜陷虎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一到踏、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧尚猿,春花似錦窝稿、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至庄萎,卻和暖如春踪少,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背糠涛。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工援奢, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人忍捡。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓集漾,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親砸脊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子具篇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容