UIcollectionView
是開發中最常使用到的組件之一,然而其拖拽排序的功能直到iOS9才引入,iOS9以前的版本并沒有原生的支持。為此我寫了一個小型庫 SortableCollectionView,方便地集成拖拽排序的功能。項目地址https://github.com/luowenxing/SortableCollectionView 只需要直接使用或者子類化SortableCollectionView
,并實現簡單的代理方法,就能方便地實現拖拽排序。同時支持自定義拖拽的視圖,比如對拖拽中的Cell進行放大、設置背景顏色等等。
效果展示
-
普通的流式布局效果,仿支付寶主頁的排序,可以看到對拖拽的視圖做了一個放大1.2倍和設置背景為灰色兩個操作。
demoFlow.gif -
瀑布流效果
demoWaterfall.gif
使用
- 在你的工程中引入
SortableCollectionView.swift
- 直接使用或者子類化
SortableCollectionView
,并在xib/storyboard中指定。如果是純代碼方式創建的,把awakeFromNib
方法的代碼作為初始化代碼就行。這個問題之后會進行優化。 - 像往常一樣實現
UICollectionView
的代理方法和布局類。 - 實現
SortableCollectionViewDelegate
并設置sortableDelegate
@objc protocol SortableCollectionViewDelegate:NSObjectProtocol {
// 排序開始,定制拖拽排序的視圖
optional func beginDragAndInitDragCell(collectionView:SortableCollectionView,dragCell:UIView)
// 排序結束,重設排序視圖
optional func endDragAndResetDragCell(collectionView:SortableCollectionView,dragCell:UIView)
// 排序結束,對排序完成后的真實Cell進行操作,比如類似支付寶的首頁排序,對沒有移動的Cell顯示刪除按鈕
optional func endDragAndOperateRealCell(collectionView:SortableCollectionView,realCell:UICollectionViewCell,isMoved:Bool)
// 排序完成,交換數據源
func exchangeDataSource(fromIndex:NSIndexPath,toIndex:NSIndexPath)
}
- 推薦在你的
UICollectionViewCell
中實現NSCopying
協議,這樣就可以更好地定制化拖拽的view。否則則會使用截圖方法snapshotViewAfterScreenUpdates
作為拖拽的view,它只是一個單獨的view,沒有相應的view層次結構。 - 如果是iOS9系統,并且你實現了
func collectionView(collectionView: UICollectionView, canMoveItemAtIndexPath indexPath: NSIndexPath) -> Bool
func collectionView(collectionView: UICollectionView, moveItemAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath)
則會使用系統原生的拖拽排序,目前原生排序是一個簡單的版本,不支持自定義的拖拽view。你可以在Demo里面試一試,比較兩種方式。
原理分析
- 使用長按手勢識別
UILongPressGestureRecognizer
,這個方法可以監聽長按、移動、結束,符合我們的需求。 - 長按后,拷貝或者截圖選中的Cell,加入到
superview
,因為這樣會方便處理之后的滾動。 - 移動過程中調用
moveItemAtIndexPath
方法交換相應的Cell。 - 比較難處理的地方就是滾動了。滾動原理其實比較簡單,就是使用
NSTimer
并加入到RunLoop
中,但是有一些細節的問題比較抓狂,比如流暢度優化,計算frame等,需要對UIScrollView
有一定的理解,核心代碼如下。
func scrollAtEdge(){
//計算拖動視圖里邊緣的距離,正比于滾動速度,并且判斷是往上還是往下滾動
let pinTop = dragView.frame.origin.y
let pinBottom = self.frame.height - (dragView.frame.origin.y + dragView.frame.height)
var speed:CGFloat = 0
var isTop:Bool = true
if pinTop < 0 {
speed = -pinTop
isTop = true
} else if pinBottom < 0 {
speed = -pinBottom
isTop = false
} else {
self.timer?.invalidate()
self.timer = nil
return
}
if let originTimer = self.timer,originSpeed = (originTimer.userInfo as? [String:AnyObject])?["speed"] as? CGFloat{
//計算滾動速度和原來相差是否過大,目的是防止頻繁的創建定時器而使滾動卡頓
if abs(speed - originSpeed) > 10 {
originTimer.invalidate()
NSLog("speed:\(speed)")
// 60fps,滾動才能流暢
let timer = NSTimer(timeInterval: 1/60.0, target: self, selector: #selector(SortableCollectionView.autoScroll(_:)), userInfo: ["top":isTop,"speed": speed] , repeats: true)
self.timer = timer
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
}
} else {
let timer = NSTimer(timeInterval: 1/60.0, target: self, selector: #selector(SortableCollectionView.autoScroll(_:)), userInfo: ["top":isTop,"speed": speed] , repeats: true)
self.timer = timer
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
}
}
func autoScroll(timer:NSTimer) {
if let userInfo = timer.userInfo as? [String:AnyObject] {
if let top = userInfo["top"] as? Bool,speed = userInfo["speed"] as? CGFloat {
//計算滾動位置,更新contentOffset
let offset = speed / 5
let contentOffset = self.contentOffset
if top {
self.contentOffset.y -= offset
self.contentOffset.y = self.contentOffset.y < 0 ? 0 : self.contentOffset.y
}else {
self.contentOffset.y += offset
self.contentOffset.y = self.contentOffset.y > self.contentSize.height - self.frame.height ? self.contentSize.height - self.frame.height : self.contentOffset.y
}
let point = CGPoint(x: dragView.center.x, y: dragView.center.y + contentOffset.y)
//滾動過程中,拖拽視圖位置不變,因此手勢識別代理不會調用,需要手動調用移動item
self.moveItemToPoint(point)
}
}
}
待改進
- 增加代理方法,對顯示中的Cell進行操作,比如增加抖動動畫等。
- 極少數情況下滾動會短時間掉幀,可能是RunLoop操作的問題。
目前正在完善中,各位小伙伴有好的意見建議都可以留言評論,喜歡的小伙伴給個Star唄!~