iOS 下拉刷新

基本上所有的 APP 都會有 tableView,那一般情況下就會有下拉刷新這個(gè)功能,就想著自己也來自定義一個(gè)下拉刷新的控件岸售。

先看一下要實(shí)現(xiàn)的效果:

Refresh.gif

這是一部分的動(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的圖姑宽。

ContentSize.png

ContentInset.png

ContentOffset.png

知道了這三個(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í)存在有效的只有pathpath是作用于positionanchorPoint的团南,所有記得設(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í)間局义,那么就會得到更多的知識。
不積跬步冗疮,無以至千里萄唇,不積小流,無以成江海赌厅。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末穷绵,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子特愿,更是在濱河造成了極大的恐慌仲墨,老刑警劉巖勾缭,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異目养,居然都是意外死亡俩由,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門癌蚁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來幻梯,“玉大人,你說我怎么就攤上這事努释〉馍遥” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵伐蒂,是天一觀的道長煞躬。 經(jīng)常有香客問我,道長逸邦,這世上最難降的妖魔是什么恩沛? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮缕减,結(jié)果婚禮上雷客,老公的妹妹穿的比我還像新娘。我一直安慰自己桥狡,他們只是感情好搅裙,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著总放,像睡著了一般呈宇。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上局雄,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天甥啄,我揣著相機(jī)與錄音,去河邊找鬼炬搭。 笑死蜈漓,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的宫盔。 我是一名探鬼主播融虽,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼灼芭!你這毒婦竟也來了有额?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎巍佑,沒想到半個(gè)月后茴迁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡萤衰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年堕义,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脆栋。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡倦卖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出椿争,到底是詐尸還是另有隱情怕膛,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布秦踪,位于F島的核電站嘉竟,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏洋侨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一倦蚪、第九天 我趴在偏房一處隱蔽的房頂上張望希坚。 院中可真熱鬧,春花似錦陵且、人聲如沸裁僧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽聊疲。三九已至,卻和暖如春沪悲,著一層夾襖步出監(jiān)牢的瞬間获洲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工殿如, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留贡珊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓涉馁,卻偏偏與公主長得像门岔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子烤送,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容