Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 讓UITableView支持長按拖動排序

讓UITableView支持長按拖動排序

編輯:關於Android編程

效果

項目地址
DraggableTableView

所有Cell都可以拖拽。

固定第一個Cell

限制長按區域


實現原理

對UITableView添加LongPress手勢 在longPress的時候,對選中的Cell進行截圖,添加到TableView作為subView,並且隱藏當前的選中的Cell 隨著手勢移動,調整截圖的位置,根據移動的位置,決定是否需要交換兩個Cell的位置 當截圖移動到頂部/底部的時候,調用CADisplayLink來向上/向下滾動TableView

接口設計

最直接的方式,可能是繼承UITableView,然後在子類中增加相關的邏輯來調整。但是,這種方式有明顯的缺陷:對現有的代碼影響較大。引入了由繼承引起的耦合。

Swift中,繼承並不是一個很好的設計方式,因為Swift是一個面向協議的語言。

本文采用extension和protocol的方式,來設計接口。

定義一個協議

@objc public protocol DragableTableDelegate:AnyObject{
     //因為Cell拖動,必然要同步DataSource,所以這是個必須實現的方法
    func tableView(tableView:UITableView,dragCellFrom fromIndexPath:NSIndexPath,toIndexPath:NSIndexPath)

    //可選,返回長按的Cell是否可拖拽。用touchPoint來實現長按Cell的某一區域實現拖拽
    optional func tableView(tableView: UITableView,canDragCellFrom indexPath: NSIndexPath, withTouchPoint point:CGPoint) -> Bool

    //可選,返回cell是否可以停止在indexPath
    optional func tableView(tableView: UITableView,canDragCellTo indexPath: NSIndexPath) -> Bool

}

然後,我們用Objective C的關聯屬性,來給用戶提用接口。

public extension UITableView{
    //關聯屬性用到的Key
    private struct OBJC_Key{
        static var dragableDelegateKey = 0
        static var dragableHelperKey = 1
        static var dragableKey = 2
    }
    // MARK: - 關聯屬性 -
    var dragableDelegate:DragableTableDelegate?{
        get{
            return objc_getAssociatedObject(self, &OBJC_Key.dragableDelegateKey) as? DragableTableDelegate
        }
        set{
            objc_setAssociatedObject(self, &OBJC_Key.dragableDelegateKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_ASSIGN)
        }
    }
    //是否可拖拽
    var dragable:Bool{
        get{
            let number = objc_getAssociatedObject(self, &OBJC_Key.dragableKey) as! NSNumber
            return number.boolValue
        }
        set{
            if newValue.boolValue {
                //進行必要的初始化
                setupDragable()
            }else{
                //清理必要的信息
                cleanDragable()
            }
            let number = NSNumber(bool: newValue)
            objc_setAssociatedObject(self, &OBJC_Key.dragableDelegateKey, number, objc_AssociationPolicy.OBJC_ASSOCIATION_ASSIGN)
        }
    }
    //因為拖拽的過程中,要存儲ImageView,CADispalyLink等信息,所以需要一個輔助類
    private var dragableHelper:DragableHelper?{
        get{
            return objc_getAssociatedObject(self, &OBJC_Key.dragableHelperKey) as? DragableHelper
        }
        set{
            objc_setAssociatedObject(self, &OBJC_Key.dragableHelperKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
        }
    }
    //...
 }

Tips:

dragableDelegate在關聯的時候,是OBJC_ASSOCIATION_ASSIGN,和weak var是一個效果。防止循環引用。

輔助類DragableHelper

因為用關聯對象來存儲數據,不得不為每一個屬性提供get/set方法。所以,我們把需要的屬性,存儲到一個類DragableHelper。
這個類如下

private class DragableHelper:NSObject,UIGestureRecognizerDelegate{

    //存儲當前拖動的Cell
    weak var draggingCell:UITableViewCell?
    //這裡的_DisplayLink是一個私有類,用來封裝CADisplayLink
    let displayLink: _DisplayLink
    //長按手勢
    let gesture: UILongPressGestureRecognizer
    //浮動的截圖ImageView
    let floatImageView: UIImageView
    //當前操作的UITableView
    weak var attachTableView:UITableView?
    //當拖動到頂部/底部的時候,tableView向上或者向下滾動的速度
    var scrollSpeed: CGFloat = 0.0
    //初始化方法
    init(tableView: UITableView, displayLink:_DisplayLink, gesture:UILongPressGestureRecognizer,floatImageView:UIImageView) {
        self.displayLink = displayLink
        self.gesture = gesture
        self.floatImageView = floatImageView
        self.attachTableView = tableView
        super.init()
        self.gesture.delegate = self
    }
    //判斷手勢是否要begin,用來限制長按區域的
    @objc func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
        guard let attachTableView = attachTableView else{
            return false
        }
        return 
//返回值代理給TableView本身        attachTableView.lh_gestureRecognizerShouldBegin(gestureRecognizer)
    }
}

_DisplayLink類

CADisplayLink是一個用來在每一幀到來的時候,調整視圖狀態來生成動畫的類。但是,有一點要注意,就是CADisplayLink必須顯示的調用

_link.invalidate()

才能斷掉循環引用,相關資源才能得到釋放?
那麼,能在dealloc中調用,來保證釋放嗎?
正常情況下是不行的,因為都沒被釋放,dealloc也就不會被調用.

那麼,如何破壞掉這種循環引用呢?OC中,我們可以使用NSProxy,詳情可見我這篇博客。
Swift中,則可以這麼實現一個基於block的displayLink

private class _DisplayLink{
    var paused:Bool{
        get{
            return _link.paused
        }
        set{
           _link.paused = newValue
        }
    }
    private init (_ callback: Void -> Void) {
        _callback = callback
        _link = CADisplayLink(target: _DisplayTarget(self), selector: #selector(_DisplayTarget._callback))
        _link.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
        _link.paused = true
    }

    private let _callback: Void -> Void

    private var _link: CADisplayLink!

    deinit {
        _link.invalidate()
    }
}

/// 弱引用CADisplayLink,斷掉循環引用
private class _DisplayTarget {

    init (_ link: _DisplayLink) {
        _link = link
    }

    weak var _link: _DisplayLink!

    @objc func _callback () {
        _link?._callback()
    }
}

Cell截圖

最基礎的CoreGraphics

private extension UIView{
    /**
     Get the screenShot of a UIView

     - returns: Image of self
     */
    func lh_screenShot()->UIImage?{
        UIGraphicsBeginImageContextWithOptions(CGSize(width: frame.width, height: frame.height), false, 0.0)
        layer.renderInContext(UIGraphicsGetCurrentContext()!)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext();
        return image
    }
}

初始化和清除

上文講到了,我們在設置dragable的時候,會進行必要的設置和初始化工作

private func setupDragable(){
    if dragableHelper != nil{
        cleanDragable()
    }
    //初始化手勢
    let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(UITableView.handleLongPress))
    addGestureRecognizer(longPressGesture)
    //初始化_DisplayLink
    let displayLink = _DisplayLink{ [unowned self] in
      //displayLink的回調
    }
    //初始化顯示截圖的UIImageView
    let imageView = UIImageView()
    let helper = DragableHelper(tableView:self,displayLink: displayLink, gesture: longPressGesture, floatImageView: imageView)
    dragableHelper = helper
}
private func cleanDragable(){
    guard let helper = dragableHelper else{
        return
    }
    removeGestureRecognizer(helper.gesture)
    dragableHelper = nil
}

處理長按手勢

是否開始手勢

通過這個代理方法,來限制長按的區域

func lh_gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
    let location = gestureRecognizer.locationInView(self)
    guard let currentIndexPath = indexPathForRowAtPoint(location),let currentCell = cellForRowAtIndexPath(currentIndexPath) else{
        return false
    }
    let pointInCell = convertPoint(location, toView: currentCell)
    //通過代理,檢查是否需要觸發手勢
    guard let canDrag = dragableDelegate?.tableView?(self, canDragCellFrom: currentIndexPath, withTouchPoint: pointInCell) else{
        return true
    }
    return canDrag
}

手勢開始

整個處理過程如下

獲取當前Cell 截圖,作為subView添加到tableView中,設置好初始位置 設置transform和alpha,設置陰影 隱藏當前Cell
guard let currentIndexPath = indexPathForRowAtPoint(location),let currentCell = cellForRowAtIndexPath(currentIndexPath)else{
    return
}
if let selectedRow = indexPathForSelectedRow{
    deselectRowAtIndexPath(selectedRow, animated: false)
}
allowsSelection = false
currentCell.highlighted = false
dragableHelper.draggingCell = currentCell
//Configure imageview
let screenShot = currentCell.lh_screenShot()
dragableHelper.floatImageView.image = screenShot

dragableHelper.floatImageView.frame = currentCell.bounds
dragableHelper.floatImageView.center = currentCell.center

dragableHelper.floatImageView.layer.shadowRadius = 5.0
dragableHelper.floatImageView.layer.shadowOpacity = 0.2
dragableHelper.floatImageView.layer.shadowOffset = CGSizeZero
dragableHelper.floatImageView.layer.shadowPath = UIBezierPath(rect: dragableHelper.floatImageView.bounds).CGPath
addSubview(dragableHelper.floatImageView)

UIView.animateWithDuration(0.2, animations: { 
    dragableHelper.floatImageView.transform = CGAffineTransformMakeScale(1.1, 1.1)
    dragableHelper.floatImageView.alpha = 0.5
})
currentCell.hidden =  true

手勢拖動

拖動的過程中,處理如下

調用方法adjusFloatImageViewCenterY方法.這個方法會調整截圖ImageView的中心,並且根據位置,決定是否要交換兩個Cell 根據拖動的位置,來設置dragableHelper.scrollSpeed.和displayLink是否停止,當displayLink啟動的時候,會以一定的速度,來調整contentOffset.y。從而,看起來顯示tableView,向上或則向下滾動
adjusFloatImageViewCenterY(location.y)
dragableHelper.scrollSpeed = 0.0
if contentSize.height > frame.height {
    let halfCellHeight = dragableHelper.floatImageView.frame.size.height / 2.0
    let cellCenterToTop = dragableHelper.floatImageView.center.y - bounds.origin.y
    if cellCenterToTop < halfCellHeight {
        dragableHelper.scrollSpeed = 5.0*(cellCenterToTop/halfCellHeight - 1.1)
    }
    else if cellCenterToTop > frame.height - halfCellHeight {
        dragableHelper.scrollSpeed = 5.0*((cellCenterToTop - frame.height)/halfCellHeight + 1.1)
    }
    dragableHelper.displayLink.paused = (dragableHelper.scrollSpeed == 0)
}

手勢結束

停止CADisplayLink 截圖ImageView移動到終止位置,並且移除
allowsSelection = true
dragableHelper.displayLink.paused = true
UIView.animateWithDuration(0.2,
                           animations: { 
        dragableHelper.floatImageView.transform = CGAffineTransformIdentity
        dragableHelper.floatImageView.alpha = 1.0
        dragableHelper.floatImageView.frame = dragableHelper.draggingCell!.frame
    },
                           completion: { (completed) in
        dragableHelper.floatImageView.removeFromSuperview()
        dragableHelper.draggingCell?.hidden = false
        dragableHelper.draggingCell = nil
})

adjusFloatImageViewCenterY 方法

這個方法首先會將截圖ImageView移動到觸摸中心,然後檢查是否需要交換cell。

// MARK: - Private method -
func adjusFloatImageViewCenterY(newY:CGFloat){
    guard let floatImageView = dragableHelper?.floatImageView else{
        return
    }
    floatImageView.center.y = min(max(newY, bounds.origin.y), bounds.origin.y + bounds.height)
    adjustCellOrderIfNecessary()
}

func adjustCellOrderIfNecessary(){
    guard let dragableDelegate = dragableDelegate,floatImageView = dragableHelper?.floatImageView,toIndexPath = indexPathForRowAtPoint(floatImageView.center) else{
        return
    }
    guard let draggingCell = dragableHelper?.draggingCell,dragingIndexPath = indexPathForCell(draggingCell) else{
        return
    }
    guard dragingIndexPath.compare(toIndexPath) != NSComparisonResult.OrderedSame else{
        return
    }
    if let canDragTo = dragableDelegate.tableView?(self, canDragCellTo: toIndexPath){
        if !canDragTo {

            return
        }
    }
    draggingCell.hidden = true
    beginUpdates()
    dragableDelegate.tableView(self, dragCellFrom: dragingIndexPath, toIndexPath: toIndexPath)
    moveRowAtIndexPath(dragingIndexPath, toIndexPath: toIndexPath)
    endUpdates()
}

總結

看到這裡了,給個Star吧。項目地址
DraggableTableView

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved