基本上所有的 APP 都會(huì)有 tableView,那一般情況下就會(huì)有下拉刷新這個(gè)功能,就想著自己也來(lái)自定義一個(gè)下拉刷新的控件。
先看一下要實(shí)現(xiàn)的效果:

這是一部分的動(dòng)畫,實(shí)際上在這里我將觸摸位置分成了三部分,左邊,中間,右邊,拖拽的位置不同,曲線的形變也不一樣。
觀察動(dòng)畫,首先是拖拽的時(shí)候會(huì)根據(jù)拖拽的幅度進(jìn)行曲線形變,這就需要監(jiān)聽滑動(dòng)手勢(shì),我在這里的做法是獲取 ScrollView的引用,并且設(shè)置KVO監(jiān)聽 ContentOffset的改變。
ScrollView 有一個(gè)屬性,panGesture 滑動(dòng)手勢(shì),所以能得到觸摸位置。
//設(shè)置相關(guān)屬性
self.superScrollView.addSubview(self)
self.superScrollView = superScrollView
//設(shè)置kvo
self.superScrollView.addObserver(self, forKeyPath:"contentOffset", options: NSKeyValueObservingOptions.new, context:nil)
在ScrollView拖動(dòng)的時(shí)候都會(huì)改變 ContentOffset,然后回調(diào)override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?),在這個(gè)方法里面進(jìn)行操作。
直接上代碼
開始先判斷一下是否是向下拖動(dòng),向下拖動(dòng)的話因?yàn)闆]有添加上拉加載的功能,所以不做處理。
if self.superScrollView.contentOffset.y > 0 {
return
}
拖動(dòng)的時(shí)候也會(huì)有狀態(tài),如果正處于刷新狀態(tài)的話,不應(yīng)該被再出拖動(dòng),重新加載,所以定義一個(gè)Struct。
enum MXRefreshStatus {
case refreshing
case none
}
回到KVO的監(jiān)聽方法
if self.refreshStatus == .none {
//獲取點(diǎn)擊位置
if self.touchPositionX == 0 {
self.touchPositionX =
self.superScrollView.panGestureRecognizer.location(in: self.superScrollView).x
}
let contentOffsetY = abs(self.superScrollView.contentOffset.y)
//是否還在拖動(dòng)
if self.superScrollView.isDragging {
//繼續(xù)拖動(dòng)
//最高點(diǎn)坐標(biāo)
let highPointY = contentOffsetY - 64.0
let path = self.updateWavePath(highPointY: highPointY, position: nil)
self.waveLayer.path = path.cgPath
}else{
//沒有拖動(dòng)了,判斷是否直接刷新
if contentOffsetY >= 150{
//改變狀態(tài)
self.refreshStatus = .refreshing
//執(zhí)行彈性動(dòng)畫
self.waveLayer.add(self.waveLayerAnimation, forKey: "WaveAnimation")
//固定住
var contentInset = self.superScrollView.contentInset
contentInset.top = 214
self.superScrollView.contentInset = contentInset
//開始執(zhí)行 block
self.operation(true)
//設(shè)置延時(shí)操作
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + self.duration, execute: {
//去除所有動(dòng)畫
self.removeAllAnimtion()
//修改狀態(tài)
self.refreshStatus = .none
//回收刷新 View
var contentInset = self.superScrollView.contentInset
contentInset.top = 64
self.superScrollView.contentInset = contentInset
//修改點(diǎn)擊位置
self.touchPositionX = 0
})
}else{
//不做操作,直接縮放回去
if self.waveLayer.path != self.rectPath.cgPath {
self.waveLayer.path = self.rectPath.cgPath
}
//修改點(diǎn)擊位置
self.touchPositionX = 0
//修改狀態(tài)
self.refreshStatus = .none
}
}
}else{
//正處于刷新狀態(tài),直接返回
return
}
減去64是因?yàn)榭紤]了導(dǎo)航欄的存在,有了導(dǎo)航欄之后,所有的TableView都會(huì)下移64,并且ContentInset.top屬性會(huì)為64。用以固定住 tableView不會(huì)回滾。
這里放幾張斯坦福大學(xué)解釋ContentOffset,ContentInset,ContentSize的圖。



知道了這三個(gè)attribute之后,應(yīng)該就知道了刷新過程中如何將 ScrollView固定住,只需要設(shè)置ContentInset.top的值就行,同理,以后要在其他方向固定,也是設(shè)置這個(gè)屬性。
在拖拽的過程中,曲線一直在形變,在調(diào)用updateWavePath
let path = self.updateWavePath(highPointY: highPointY, position: nil)
self.waveLayer.path = path.cgPath
這就是繪制曲線形變的方法
//MARK: wavePath Stroke
private func updateWavePath(highPointY : CGFloat,position : MXRefreshPosition?)->UIBezierPath{
let path = UIBezierPath.init()
let lineY = self.waveLayer.bounds.size.height
path.move(to: CGPoint.init(x: 0, y: 0))
path.addLine(to: CGPoint.init(x: self.waveLayer.frame.width, y: 0.0))
path.addLine(to: CGPoint.init(x: self.waveLayer.frame.width, y: self.waveLayer.frame.height))
//使用貝塞爾曲線
//控制點(diǎn)
var controlPoint : CGPoint!
//觸摸
controlPoint = CGPoint.init(x: self.touchPositionX, y: highPointY + lineY)
//繪制路徑
if (self.touchPositionX != 0 && self.touchPositionX <= self.superScrollView.frame.width / 3.0) || (position != nil && position == .left) {
//左邊
let destinationPointX = self.waveLayer.frame.width / 3.0 * 2.0
path.addLine(to: CGPoint.init(x: destinationPointX, y: lineY))
path.addQuadCurve(to: CGPoint.init(x: 0, y: lineY), controlPoint: controlPoint)
}else if (self.touchPositionX != 0 && self.touchPositionX >= (self.superScrollView.frame.width - self.superScrollView.frame.width / 3.0)) || (position != nil && position == .right) {
//右邊
let destinationPointX = self.waveLayer.frame.width / 3.0
path.addQuadCurve(to: CGPoint.init(x: destinationPointX, y: lineY), controlPoint: controlPoint)
path.addLine(to: CGPoint.init(x: 0, y: lineY))
}else{
//中間
let leftStartPositionX = self.waveLayer.frame.width / 4.0
let rightEndPositionX = self.waveLayer.frame.width / 4.0 * 3.0
path.addLine(to: CGPoint.init(x: rightEndPositionX, y: lineY))
path.addQuadCurve(to: CGPoint.init(x: leftStartPositionX, y: lineY), controlPoint: controlPoint)
path.addLine(to: CGPoint.init(x: 0, y: lineY))
}
//閉合路徑,連接首尾
path.close()
return path
}
這個(gè)是根據(jù)拖動(dòng)Y的程度,去設(shè)置曲線的Control Point,原理是貝塞爾曲線,這里就不多說了,可以去查閱,有許多的資料專門介紹這個(gè)曲線。
同時(shí)監(jiān)聽手指松開的時(shí)候也只需要判斷ScrollView.isDragging屬性,拖拽結(jié)束時(shí)候判斷已經(jīng)拖動(dòng)的距離,達(dá)到刷新條件就刷新,沒有就直接縮回去。
達(dá)到刷新條件之后的彈性效果,我是采用CAKeyframeAnimation做的,還有一部分是使用CADisplayLink去實(shí)現(xiàn),在每一幀去重新繪制,我覺得這個(gè)動(dòng)畫是一直會(huì)需要使用,不如就直接實(shí)例化,作為屬性,每一次都只需add就行。
self.waveLayerAnimation = CAKeyframeAnimation.init(keyPath: "path")
self.waveLayerAnimation.values = [
self.updateWavePath(highPointY: 100.0, position: .left).cgPath,
self.updateWavePath(highPointY: -80.0, position: .left).cgPath,
self.updateWavePath(highPointY: 60.0, position: .left).cgPath,
self.updateWavePath(highPointY: -40.0, position: .left).cgPath,
self.updateWavePath(highPointY: 10.0, position: .left).cgPath,
self.updateWavePath(highPointY: -5.0, position: .left).cgPath,
self.updateWavePath(highPointY: 1.0, position: .left).cgPath,
self.rectPath.cgPath
]
self.waveLayerAnimation.isRemovedOnCompletion = false
self.waveLayerAnimation.fillMode = kCAFillModeForwards
self.waveLayerAnimation.duration = 0.5
self.waveLayerAnimation.autoreverses = false
動(dòng)畫的原理就是在duration內(nèi)設(shè)置曲線的ControlPoint一直是在上下改變,曲線的彎曲方向也就會(huì)改變,同時(shí)慢慢減少,也就形成了bounce效果。
這里的曲線是單獨(dú)設(shè)置的,不能和觸摸繪制關(guān)聯(lián)起來(lái),所以在update里面。
if position != nil {
let controlX : CGFloat = self.waveLayer.frame.width / 2.0
controlPoint = CGPoint.init(x: controlX, y: highPointY + lineY)
//Path
path.addQuadCurve(to: CGPoint.init(x: 0, y: lineY), controlPoint: controlPoint)
}
圓的動(dòng)畫是在曲線的動(dòng)畫完成之后才執(zhí)行的,所以就設(shè)置曲線的delegate。
//Delegate
self.waveLayerAnimation.delegate = self
self.waveLayerAnimation.setValue("WaveAnimation", forKey: "identifier")
CAAnimationDelegate的回調(diào)是深拷貝,所以如果動(dòng)畫多的話,不能直接去用==比較,要單獨(dú)區(qū)分開,我認(rèn)為使用KVC比較好。
extension MXRefreshView : CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
switch anim.value(forKey: "identifier") as! String {
case "WaveAnimation":
//執(zhí)行圓圈動(dòng)畫
self.refreshLoadingImageView.startAnimation()
break
default:
break
}
}
}
圓圈的動(dòng)畫很簡(jiǎn)單,只是設(shè)置CAKeyframeAnimation.path,這個(gè)值和values只能有一個(gè),同時(shí)存在有效的只有path,path是作用于position和anchorPoint的,所有記得設(shè)置keypath。
直接給代碼了
//BigCircle
self.bigLoadingCircleAnimation = CAKeyframeAnimation.init(keyPath: "path")
self.bigLoadingCircleAnimation.values = [
UIBezierPath.init(arcCenter: CGPoint.init(x: self.bigLoadingCircle.frame.width / 2.0, y: self.bigLoadingCircle.frame.width / 2.0), radius: self.bigLoadingCircle.frame.width / 3.0, startAngle: 0, endAngle:CGFloat(M_PI * 2.0), clockwise: true).cgPath,
UIBezierPath.init(arcCenter: CGPoint.init(x: self.bigLoadingCircle.frame.width / 2.0, y: self.bigLoadingCircle.frame.width / 2.0), radius: self.bigLoadingCircle.frame.width / 4.0, startAngle: 0, endAngle:CGFloat(M_PI * 2.0), clockwise: true).cgPath,
UIBezierPath.init(arcCenter: CGPoint.init(x: self.bigLoadingCircle.frame.width / 2.0, y: self.bigLoadingCircle.frame.width / 2.0), radius: self.bigLoadingCircle.frame.width / 5.0, startAngle: 0, endAngle:CGFloat(M_PI * 2.0), clockwise: true).cgPath,
UIBezierPath.init(arcCenter: CGPoint.init(x: self.bigLoadingCircle.frame.width / 2.0, y: self.bigLoadingCircle.frame.width / 2.0), radius: self.bigLoadingCircle.frame.width / 6.0, startAngle: 0, endAngle:CGFloat(M_PI * 2.0), clockwise: true).cgPath,
UIBezierPath.init(arcCenter: CGPoint.init(x: self.bigLoadingCircle.frame.width / 2.0, y: self.bigLoadingCircle.frame.width / 2.0), radius: 2.5, startAngle: 0, endAngle:CGFloat(M_PI * 2.0), clockwise: true).cgPath
]
self.bigLoadingCircleAnimation.timingFunctions = [CAMediaTimingFunction.init(name: kCAMediaTimingFunctionLinear)]
self.bigLoadingCircleAnimation.isRemovedOnCompletion = false
//相當(dāng)于無(wú)限循環(huán)
self.bigLoadingCircleAnimation.repeatCount = Float.infinity
self.bigLoadingCircleAnimation.autoreverses = true
self.bigLoadingCircleAnimation.duration = 2.0
//MinCircle
//有多少個(gè)小圓,就有多少個(gè)動(dòng)畫,因?yàn)槊總€(gè)圓的動(dòng)畫有時(shí)延
for index in 0..<self.minLoadingCircles.count {
let minLoadingCirclesAnimation = CAKeyframeAnimation.init(keyPath: "position")
let circleMovePath = CGMutablePath.init()
circleMovePath.addArc(center: CGPoint.init(x: self.frame.width / 2.0, y: self.frame.height + 6.0), radius: self.frame.width / 2.0 + 6.0, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: false)
minLoadingCirclesAnimation.path = circleMovePath
minLoadingCirclesAnimation.isRemovedOnCompletion = false
minLoadingCirclesAnimation.repeatCount = Float.infinity
minLoadingCirclesAnimation.autoreverses = false
minLoadingCirclesAnimation.duration = 2.0
self.minLoadingCirclesAnimations.append(minLoadingCirclesAnimation)
//Delegate
minLoadingCirclesAnimation.setValue(String.init(format: "MinCircleAnimation%d", index), forKey: "identifier")
minLoadingCirclesAnimation.delegate = self
}
完整的代碼放在GitHub
每一天都去學(xué)習(xí)一些東西,最后都會(huì)幫助到自己的。
得與失是平衡的,你放下了娛樂和休息的時(shí)間,那么就會(huì)得到更多的知識(shí)。
不積跬步,無(wú)以至千里,不積小流,無(wú)以成江海。