編輯:關於Android編程
項目地址
DraggableTableView
所有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
前言本文給大家分享一個使用Android開發寫字板功能Dem、簡單操作內存中的圖像、對圖像進行簡單的處理、繪制直線、以達到寫字板的效果效果圖如下XML布局代碼<Re
Gradle之管理多個Module編譯在一個工程項目中,我們可能會有多個Module,如:多個app,library。我們來看下一個最簡單的多個Module的build文
首先附上運行結果:如果你沒有學過listview請你先看一看基本知識。不想再說的那麼細了 太多了。首先是listview布局 <!--{cke_prote
最近博主開始在項目中實踐MVP模式,卻意外發現內存洩漏比較嚴重,但卻很少人談到這個問題,促使了本文的發布,本文假設讀者已了解MVP架構。MVP簡介M-Modle,數據,邏