让UITableView支持长按拖动排序

来自Leo的原创博客,转载请著名出处

我的StackOverflow

我的Github

https://github.com/LeoMobileDeveloper

注意:本文的代码是用Swift 2.3写的


效果

项目地址

DraggableTableView

所有Cell都可以拖拽。

固定第一个Cell

限制长按区域


实现原理

  • 对UITableView添加LongPress手势
  • 在longPress的时候,对选中的Cell进行截图,添加到TableView作为subView,并且隐藏当前的选中的Cell
  • 随着手势移动,调整截图的位置,根据移动的位置,决定是否需要交换两个Cell的位置
  • 当截图移动到顶部/底部的时候,调用CADisplayLink来向上/向下滚动TableView

接口设计

最直接的方式,可能是继承UITableView,然后在子类中增加相关的逻辑来调整。但是,这种方式有明显的缺陷:对现有的代码影响较大。引入了由继承引起的耦合。

Swift中,继承并不是一个很好的设计方式,因为Swift是一个面向协议的语言。

本文采用extensionprotocol的方式,来设计接口。

定义一个协议

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

手势开始

整个处理过程如下

  1. 获取当前Cell
  2. 截图,作为subView添加到tableView中,设置好初始位置
  3. 设置transform和alpha,设置阴影
  4. 隐藏当前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

手势拖动

拖动的过程中,处理如下

  1. 调用方法adjusFloatImageViewCenterY方法.这个方法会调整截图ImageView的中心,并且根据位置,决定是否要交换两个Cell
  2. 根据拖动的位置,来设置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)
}

手势结束

  1. 停止CADisplayLink
  2. 截图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

时间: 2024-11-08 19:00:41

让UITableView支持长按拖动排序的相关文章

avalon js实现仿google plus图片多张拖动排序

转载请注明: TheViper http://www.cnblogs.com/TheViper  效果 google plus 拖动+响应式效果: 要求 1. 两边对齐布局,即图片间间距一致,但左右两边的图片与边界的间距不一定等于图片间间距,兼容ie7,8,firefox,chrome.2. 浏览器尺寸变化,在大于一定尺寸时,每行自动增加或减少图片,自动调整图片间间距,以满足两边对齐布局,这时每张图片尺寸固定(这里是200*200px):而小于一定尺寸时,每行图片数量固定(这里最小列数是3),这

IOS UITableView拖动排序功能

UITbableView作为列表展示信息,除了展示的功能,有时还会用到删除,排序等功能,下面就来讲解一下如何实现排序. 排序是当表格进入编辑状态后,在单元格的右侧会出现一个按钮,点击按钮,就可以拖动单元格,移动位置,进行手动排序. 使用系统自带拖动排序功能的步骤: 1.让tableView进入编辑状态,也就是设置它的editing为YES 2.返回编辑模式,也就是实现UITableViewDelegate中的tableview:editingStyleForRowAtIndexPath:方法,在

解析---DragGridView长按动态排序

曾经项目需求,要实现一个类似以前ZAKER新闻阅读的DragGridView特效.不过,没要求实现横向跨屏,而是竖向跨屏.网上资源也很多,有实现横向跨屏的,可是没有找到竖向的.后来只好结合人家代码,修改一二. 贴代码! /** * @blog http://blog.csdn.net/xiaanming * * @author xiaanming&&wangxuanao * */ public class DragGridView extends GridView { /** * Drag

android可拖动排序GridView实现

经常使用今日头条.网易新闻的同学们应该都会注意到用于管理多个频道的可拖动排序GridView,下面介绍一下可拖动的DragGridView的实现方法.代码放在GitHub上https://github.com/zhaoyu87/DragGridView,需要的同学可以下载 DragGridView继承自GridView,当长按选中某个item进行拖动,放手更新GridView顺序: 1.重写onTouchEvent响应拖动事件:被按下时记录按下坐标:拖动时更新被拖动视图显示:放开时更新排序 2.

可拖动排序的ListView

一.上图 二.简述 1.需要实现的效果是长按右侧可拖动部分布局实现列表项的拖动排序 2.当点击列表项前面的单选按钮时,在该条目右侧显示删除图标,点击该图标删除当前条目. 三.实现思路 借助github上的开源代码drag-sort-listview-master加以改造. 四.主要源码展示 1.Activity代码 package com.gengducun.dslvdemo; import java.util.ArrayList; import android.app.Activity; im

Android实现GridView的item长按拖动删除实现(带动画效果)

领导这几天让做一个项目,就是可以实现像支付宝首页一样的可以长按拖动,删除的界面,以前没做过,领导让我做的时候觉得简直是老虎吃天,无从下手啊,可是领导的任务还是要实现的,没办法,就自己网上找咯,但是网上的demo五花八门无法满足我的需求,而且bug还很多,所以最后就自己实现了,说实话,这个效果困扰了我好几个星期,因为其中牵扯的知识点太多了,什么事件分发机制,动画效果,互换位置的算法,还有拖动,这些我都没有接触过,所以只有一点一点来做咯,如果大家还没有了解过这些知识点,我建议搭建先去了解一下,毕竟这

记一个react拖动排序中的坑:key

在做一个基于react的应用的时候遇到了对列表拖动排序的需求.当使用sortable对列表添加排序支持后发现一个问题:数据正确排序了,但是dom的顺序却乱了,找了一会儿原因后发现是因为在渲染数据的时候指定了一个动态的key(map((o,i)=>(<li key={i}></li>))),导致了dom顺序混乱.解决方案有:将数据装入模型的时候生成一个不重复的key,或者使用lodash的uniqueId: import uniqueId from 'lodash/unique

jquery 鼠标拖动排序Li或Table

1.前端页面 <%@ Page Language="C#" AutoEventWireup="true" CodeFile="拖动排序Li或Table.aspx.cs" Inherits="拖动排序Li或Table" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.o

锋利的jQuery-7--query ui效果库--拖动排序插件sortable

一个简单的拖动排序效果,具体请参看jQuery ui官网demo. jquery ui :http://jqueryui.com/ sortable例子:http://jqueryui.com/sortable/#portlets 效果如图: html代码: <style type="text/css"> #myList{width: 80px;background: #EEE;padding: 5px;list-style: none;} #myList a{text-d