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á)到拖拽并重新排序的功能逞姿。
我們創(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
, Changed
和 Ended
译蒂,首先曼月,我們隱藏拖拽的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)了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
}
頁(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的
教程