基本上所有的 APP
都會有 tableView
,那一般情況下就會有下拉刷新這個(gè)功能,就想著自己也來自定義一個(gè)下拉刷新的控件岸售。
先看一下要實(shí)現(xiàn)的效果:
這是一部分的動(dòng)畫,實(shí)際上在這里我將觸摸位置分成了三部分瞎领,左邊黎做,中間,右邊阳堕,拖拽的位置不同跋理,曲線的形變也不一樣。
觀察動(dòng)畫恬总,首先是拖拽的時(shí)候會根據(jù)拖拽的幅度進(jìn)行曲線形變前普,這就需要監(jiān)聽滑動(dòng)手勢,我在這里的做法是獲取 ScrollView
的引用壹堰,并且設(shè)置KVO
監(jiān)聽 ContentOffset
的改變拭卿。
ScrollView
有一個(gè)屬性骡湖,panGesture
滑動(dòng)手勢,所以能得到觸摸位置峻厚。
//設(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í)候都會改變 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í)候也會有狀態(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
都會下移64
续膳,并且ContentInset.top
屬性會為64
。用以固定住 tableView
不會回滾俗慈。
這里放幾張斯坦福大學(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)畫是一直會需要使用,不如就直接實(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
一直是在上下改變打毛,曲線的彎曲方向也就會改變晦雨,同時(shí)慢慢減少,也就形成了bounce
效果隘冲。
這里的曲線是單獨(dú)設(shè)置的,不能和觸摸繪制關(guān)聯(lián)起來,所以在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)畫很簡單,只是設(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)于無限循環(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í)一些東西正歼,最后都會幫助到自己的。
得與失是平衡的拷橘,你放下了娛樂和休息的時(shí)間局义,那么就會得到更多的知識。
不積跬步冗疮,無以至千里萄唇,不積小流,無以成江海赌厅。