├── .gitignore ├── .swift-version ├── CRRefresh.png ├── CRRefresh.podspec ├── CRRefresh.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── CRRefresh.xcscheme ├── CRRefresh ├── CRRefresh │ ├── Animators │ │ ├── FastAnimator │ │ │ ├── FastAnimator.swift │ │ │ ├── FastArrowLayer.swift │ │ │ ├── FastCheckLayer.swift │ │ │ ├── FastCircleLayer.swift │ │ │ └── FastLayer.swift │ │ ├── NormalAnimator │ │ │ ├── NormalFooter.bundle │ │ │ │ ├── en.lproj │ │ │ │ │ └── Localizable.strings │ │ │ │ └── zh.lproj │ │ │ │ │ └── Localizable.strings │ │ │ ├── NormalFooterAnimator.swift │ │ │ ├── NormalHeader.bundle │ │ │ │ ├── en.lproj │ │ │ │ │ └── Localizable.strings │ │ │ │ ├── images │ │ │ │ │ ├── refresh_arrow@2x.png │ │ │ │ │ └── refresh_arrow@3x.png │ │ │ │ └── zh.lproj │ │ │ │ │ └── Localizable.strings │ │ │ └── NormalHeaderAnimator.swift │ │ ├── RamotionAnimator │ │ │ ├── RamotionAnimator.swift │ │ │ ├── RamotionBallLayer.swift │ │ │ ├── RamotionBounceLayer.swift │ │ │ └── RamotionWaveLayer.swift │ │ └── SlackLoadingAnimator │ │ │ ├── SlackLoadingAnimator.swift │ │ │ └── WCLLoadingView.swift │ ├── CRRefreshAnimator.swift │ ├── CRRefreshBundle.swift │ ├── CRRefreshComponent.swift │ ├── CRRefreshExtension.swift │ ├── CRRefreshFooterView.swift │ ├── CRRefreshHeaderView.swift │ ├── CRRefreshProtocol.swift │ └── UIColorExtension.swift └── Info.plist ├── CRRefresh1.gif ├── CRRefresh2.gif ├── CRRefresh3.gif ├── CRRefresh4.gif ├── Demo ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-57x57@1x.png │ │ ├── Icon-App-57x57@2x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-ios-App-1024x1024@1x.png │ │ ├── Icon-ipad-App-20x20@1x.png │ │ ├── Icon-ipad-App-20x20@2x.png │ │ ├── Icon-ipad-App-29x29@1x.png │ │ ├── Icon-ipad-App-29x29@2x.png │ │ ├── Icon-ipad-App-40x40@1x.png │ │ ├── Icon-ipad-App-40x40@2x.png │ │ ├── Icon-ipad-App-72x72@1x.png │ │ ├── Icon-ipad-App-72x72@2x.png │ │ ├── Icon-ipad-App-76x76@1x.png │ │ ├── Icon-ipad-App-76x76@2x.png │ │ ├── Icon-ipad-App-83.5x83.5@2x.png │ │ ├── Icon-ipad-Small-50x50@1x.png │ │ └── Icon-ipad-Small-50x50@2x.png │ ├── Image_1.imageset │ │ ├── 3(1).png │ │ └── Contents.json │ ├── Image_2.imageset │ │ ├── 2(1).png │ │ └── Contents.json │ ├── Image_3.imageset │ │ ├── 1.png │ │ └── Contents.json │ └── nav_back.imageset │ │ ├── Contents.json │ │ ├── icon-返回@2x.png │ │ └── icon-返回@3x.png ├── Base.lproj │ └── LaunchScreen.storyboard ├── BaseViewController.swift ├── Cells │ ├── NormalCell.swift │ ├── NormalCell.xib │ ├── RefreshCell.swift │ └── RefreshCell.xib ├── Info.plist ├── NavigationController.swift ├── RefreshController.swift └── ViewController.swift ├── LICENSE ├── README.md └── README_CN.md /.gitignore: -------------------------------------------------------------------------------- 1 | ld/ 2 | *.pbxuser 3 | !default.pbxuser 4 | *.mode1v3 5 | !default.mode1v3 6 | *.mode2v3 7 | !default.mode2v3 8 | *.perspectivev3 9 | !default.perspectivev3 10 | xcuserdata 11 | *.xccheckout 12 | *.moved-aside 13 | DerivedData 14 | *.hmap 15 | *.ipa 16 | *.xcuserstate 17 | *.xcuserdatad 18 | 19 | Podfile.lock 20 | */Pods 21 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.2 -------------------------------------------------------------------------------- /CRRefresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/CRRefresh.png -------------------------------------------------------------------------------- /CRRefresh.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint CRRefresh.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | 11 | s.name = "CRRefresh" 12 | s.version = "1.1.3" 13 | s.summary = "An easy way to use pull-to-refresh" 14 | s.homepage = "https://github.com/CRAnimation/CRRefresh" 15 | s.license = 'MIT' 16 | s.author = { "W_C__L" => "wangchonglei93@icloud.com" } 17 | s.platform = :ios, "8.0" 18 | s.swift_version = '4.2' 19 | s.source = { :git => "https://github.com/CRAnimation/CRRefresh.git", :tag => s.version.to_s } 20 | s.source_files = ['CRRefresh/CRRefresh/*.{swift}','CRRefresh/CRRefresh/Animators/**/*.{swift}'] 21 | s.resources = 'CRRefresh/CRRefresh/Animators/**/*.{bundle}' 22 | s.frameworks = "UIKit" 23 | s.requires_arc = true 24 | 25 | end 26 | -------------------------------------------------------------------------------- /CRRefresh.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CRRefresh.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CRRefresh.xcodeproj/xcshareddata/xcschemes/CRRefresh.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/FastAnimator/FastAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FastAnimator.swift 3 | // FastAnimator 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:http://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class FastAnimator 22 | // @abstract FastAnimator 23 | // @discussion FastAnimator 24 | // 25 | 26 | import UIKit 27 | 28 | open class FastAnimator: UIView, CRRefreshProtocol { 29 | 30 | open var view: UIView { return self } 31 | 32 | open var insets: UIEdgeInsets = .zero 33 | 34 | open var trigger: CGFloat = 55.0 35 | 36 | open var execute: CGFloat = 55.0 37 | 38 | open var endDelay: CGFloat = 1.5 39 | 40 | open var hold: CGFloat = 55.0 41 | 42 | private(set) var color: UIColor = .init(rgb: (214, 214, 214)) 43 | 44 | private(set) var arrowColor: UIColor = .init(rgb: (165, 165, 165)) 45 | 46 | private(set) var lineWidth: CGFloat = 1 47 | 48 | private(set) var fastLayer: FastLayer? 49 | 50 | 51 | //MARK: CRRefreshProtocol 52 | /// 开始刷新 53 | open func refreshBegin(view: CRRefreshComponent) { 54 | fastLayer?.arrow?.startAnimation().animationEnd = { [weak self] in 55 | self?.fastLayer?.circle?.startAnimation() 56 | } 57 | } 58 | 59 | /// 结束刷新 60 | open func refreshEnd(view: CRRefreshComponent, finish: Bool) { 61 | if finish { 62 | fastLayer?.arrow?.endAnimation() 63 | fastLayer?.circle?.endAnimation(finish: finish) 64 | fastLayer?.arrow?.setAffineTransform(CGAffineTransform.identity) 65 | } 66 | } 67 | 68 | open func refreshWillEnd(view: CRRefreshComponent) { 69 | fastLayer?.circle?.endAnimation(finish: false) 70 | } 71 | 72 | /// 刷新进度的变化 73 | open func refresh(view: CRRefreshComponent, progressDidChange progress: CGFloat) { 74 | if progress >= 1 { 75 | let transform = CGAffineTransform.identity.rotated(by: CGFloat(Double.pi)) 76 | fastLayer?.arrow?.setAffineTransform(transform) 77 | } else { 78 | let transform = CGAffineTransform.identity.rotated(by: CGFloat(2 * Double.pi)) 79 | fastLayer?.arrow?.setAffineTransform(transform) 80 | } 81 | } 82 | 83 | /// 刷新状态的变化 84 | open func refresh(view: CRRefreshComponent, stateDidChange state: CRRefreshState) { 85 | 86 | } 87 | 88 | //MARK: Override 89 | override init(frame: CGRect) { 90 | super.init(frame: frame) 91 | backgroundColor = UIColor.clear 92 | } 93 | 94 | open override func layoutSubviews() { 95 | super.layoutSubviews() 96 | if fastLayer == nil { 97 | let width = frame.width 98 | let height = frame.height 99 | fastLayer = FastLayer(frame: .init(x: width/2 - 14, 100 | y: height/2 - 14, 101 | width: 28, height: 28), 102 | color: color, 103 | arrowColor: arrowColor, 104 | lineWidth: lineWidth) 105 | layer.addSublayer(fastLayer!) 106 | } 107 | } 108 | 109 | //MARK: Initial Methods 110 | public init(frame: CGRect, 111 | color: UIColor = .init(rgb: (214, 214, 214)), 112 | arrowColor: UIColor = .init(rgb: (165, 165, 165)), 113 | lineWidth: CGFloat = 1) { 114 | self.color = color 115 | self.arrowColor = arrowColor 116 | self.lineWidth = lineWidth 117 | super.init(frame: frame) 118 | backgroundColor = UIColor.clear 119 | } 120 | 121 | required public init?(coder aDecoder: NSCoder) { 122 | fatalError("init(coder:) has not been implemented") 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/FastAnimator/FastArrowLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FastArrowLayer.swift 3 | // FastAnimator 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:https://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class FastArrowLayer 22 | // @abstract 箭头的layer 23 | // @discussion 箭头的layer 24 | // 25 | 26 | import UIKit 27 | 28 | class FastArrowLayer: CALayer, 29 | CAAnimationDelegate { 30 | 31 | var color: UIColor = UIColor.init(rgb: (165, 165, 165)) 32 | 33 | var lineWidth: CGFloat = 1 34 | 35 | private var lineLayer: CAShapeLayer? 36 | 37 | private var arrowLayer: CAShapeLayer? 38 | 39 | private let animationDuration: Double = 0.2 40 | 41 | var animationEnd: (()->Void)? 42 | 43 | //MARK: Initial Methods 44 | init(frame: CGRect, 45 | color: UIColor = .init(rgb: (165, 165, 165)), 46 | lineWidth: CGFloat = 1) { 47 | self.color = color 48 | self.lineWidth = lineWidth 49 | super.init() 50 | self.frame = frame 51 | backgroundColor = UIColor.clear.cgColor 52 | initLineLayer() 53 | initArrowLayer() 54 | } 55 | 56 | required init?(coder aDecoder: NSCoder) { 57 | fatalError("init(coder:) has not been implemented") 58 | } 59 | 60 | //MARK: Privater Methods 61 | private func initLineLayer() { 62 | let width = frame.size.width 63 | let height = frame.size.height 64 | let path = UIBezierPath() 65 | path.move(to: .init(x: width/2, y: 0)) 66 | path.addLine(to: .init(x: width/2, y: height/2 + height/3)) 67 | lineLayer = CAShapeLayer() 68 | lineLayer?.lineWidth = lineWidth*2 69 | lineLayer?.strokeColor = color.cgColor 70 | lineLayer?.fillColor = UIColor.clear.cgColor 71 | lineLayer?.lineCap = CAShapeLayerLineCap.round 72 | lineLayer?.path = path.cgPath 73 | lineLayer?.strokeStart = 0.5 74 | addSublayer(lineLayer!) 75 | } 76 | 77 | private func initArrowLayer() { 78 | let width = frame.size.width 79 | let height = frame.size.height 80 | let path = UIBezierPath() 81 | path.move(to: .init(x: width/2 - height/6, y: height/2 + height/6)) 82 | path.addLine(to: .init(x: width/2, y: height/2 + height/3)) 83 | path.addLine(to: .init(x: width/2 + height/6, y: height/2 + height/6)) 84 | arrowLayer = CAShapeLayer() 85 | arrowLayer?.lineWidth = lineWidth*2 86 | arrowLayer?.strokeColor = color.cgColor 87 | arrowLayer?.lineCap = CAShapeLayerLineCap.round 88 | arrowLayer?.lineJoin = CAShapeLayerLineJoin.round 89 | arrowLayer?.fillColor = UIColor.clear.cgColor 90 | arrowLayer?.path = path.cgPath 91 | addSublayer(arrowLayer!) 92 | } 93 | 94 | //MARK: public Methods 95 | @discardableResult 96 | func startAnimation() -> Self { 97 | let start = CABasicAnimation(keyPath: "strokeStart") 98 | start.duration = animationDuration 99 | start.fromValue = 0 100 | start.toValue = 0.5 101 | start.isRemovedOnCompletion = false 102 | start.fillMode = CAMediaTimingFillMode.forwards 103 | start.delegate = self 104 | start.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 105 | 106 | let end = CABasicAnimation(keyPath: "strokeEnd") 107 | end.duration = animationDuration 108 | end.fromValue = 1 109 | end.toValue = 0.5 110 | end.isRemovedOnCompletion = false 111 | end.fillMode = CAMediaTimingFillMode.forwards 112 | end.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 113 | 114 | arrowLayer?.add(start, forKey: "strokeStart") 115 | arrowLayer?.add(end, forKey: "strokeEnd") 116 | 117 | return self 118 | } 119 | 120 | func endAnimation() { 121 | arrowLayer?.isHidden = false 122 | lineLayer?.isHidden = false 123 | arrowLayer?.removeAllAnimations() 124 | lineLayer?.removeAllAnimations() 125 | } 126 | 127 | private func addLineAnimation() { 128 | let start = CABasicAnimation(keyPath: "strokeStart") 129 | start.fromValue = 0.5 130 | start.toValue = 0 131 | start.isRemovedOnCompletion = false 132 | start.fillMode = CAMediaTimingFillMode.forwards 133 | start.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 134 | start.duration = animationDuration/2 135 | lineLayer?.add(start, forKey: "strokeStart") 136 | 137 | let end = CABasicAnimation(keyPath: "strokeEnd") 138 | end.beginTime = CACurrentMediaTime() + animationDuration/3 139 | end.duration = animationDuration/2 140 | end.fromValue = 1 141 | end.toValue = 0.03 142 | end.isRemovedOnCompletion = false 143 | end.fillMode = CAMediaTimingFillMode.forwards 144 | end.delegate = self 145 | end.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 146 | lineLayer?.add(end, forKey: "strokeEnd") 147 | } 148 | 149 | func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { 150 | if let anim = anim as? CABasicAnimation { 151 | if anim.keyPath == "strokeStart" { 152 | arrowLayer?.isHidden = true 153 | addLineAnimation() 154 | }else { 155 | lineLayer?.isHidden = true 156 | animationEnd?() 157 | } 158 | } 159 | } 160 | 161 | override init(layer: Any) { 162 | super.init(layer: layer) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/FastAnimator/FastCheckLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FastCheckLayer.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:https://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class FastCheckLayer 22 | // @abstract FastCheckLayer 23 | // @discussion FastCheckLayer 24 | // 25 | 26 | import UIKit 27 | 28 | class FastCheckLayer: CALayer { 29 | 30 | private(set) var check: CAShapeLayer? 31 | 32 | let color: UIColor 33 | 34 | let lineWidth: CGFloat 35 | 36 | //MARK: Public Methods 37 | func startAnimation() { 38 | let start = CAKeyframeAnimation(keyPath: "strokeStart") 39 | start.values = [0, 0.4, 0.3] 40 | start.isRemovedOnCompletion = false 41 | start.fillMode = CAMediaTimingFillMode.forwards 42 | start.duration = 0.5 43 | start.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 44 | 45 | let end = CAKeyframeAnimation(keyPath: "strokeEnd") 46 | end.values = [0, 1, 0.9] 47 | 48 | end.isRemovedOnCompletion = false 49 | end.fillMode = CAMediaTimingFillMode.forwards 50 | end.duration = 0.8 51 | end.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 52 | 53 | check?.add(start, forKey: "start") 54 | check?.add(end, forKey: "end") 55 | } 56 | 57 | func endAnimation() { 58 | check?.removeAllAnimations() 59 | } 60 | 61 | //MARK: Initial Methods 62 | init(frame: CGRect, color: UIColor = .init(rgb: (165, 165, 165)), lineWidth: CGFloat = 1) { 63 | self.color = color 64 | self.lineWidth = lineWidth*2 65 | super.init() 66 | self.frame = frame 67 | backgroundColor = UIColor.clear.cgColor 68 | drawCheck() 69 | } 70 | 71 | required init?(coder aDecoder: NSCoder) { 72 | fatalError("init(coder:) has not been implemented") 73 | } 74 | 75 | //MARK: Privater Methods 76 | private func drawCheck() { 77 | let width = Double(frame.size.width) 78 | check = CAShapeLayer() 79 | check?.lineCap = CAShapeLayerLineCap.round 80 | check?.lineJoin = CAShapeLayerLineJoin.round 81 | check?.lineWidth = lineWidth 82 | check?.fillColor = UIColor.clear.cgColor 83 | check?.strokeColor = color.cgColor 84 | check?.strokeStart = 0 85 | check?.strokeEnd = 0 86 | let path = UIBezierPath() 87 | let a = sin(0.4) * (width/2) 88 | let b = cos(0.4) * (width/2) 89 | path.move(to: CGPoint.init(x: width/2 - b, y: width/2 - a)) 90 | path.addLine(to: CGPoint.init(x: width/2 - width/20 , y: width/2 + width/8)) 91 | path.addLine(to: CGPoint.init(x: width - width/5, y: width/2 - a)) 92 | check?.path = path.cgPath 93 | addSublayer(check!) 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/FastAnimator/FastCircleLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FastCircleLayer.swift 3 | // FastAnimator 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:https://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class FastCircleLayer 22 | // @abstract 外层圆环的layer 23 | // @discussion 外层圆环的layer 24 | // 25 | 26 | import UIKit 27 | 28 | class FastCircleLayer: CALayer { 29 | 30 | let color: UIColor 31 | 32 | let pointColor: UIColor 33 | 34 | let lineWidth: CGFloat 35 | 36 | let circle = CAShapeLayer() 37 | 38 | let point = CAShapeLayer() 39 | 40 | private let pointBack = CALayer() 41 | 42 | private var rotated: CGFloat = 0 43 | 44 | private var rotatedSpeed: CGFloat = 0 45 | 46 | private var speedInterval: CGFloat = 0 47 | 48 | private var stop: Bool = false 49 | 50 | private(set) var check: FastCheckLayer? 51 | 52 | var codeTimer: DispatchSourceTimer? 53 | 54 | //MARK: Initial Methods 55 | init(frame: CGRect, 56 | color: UIColor = .init(rgb: (214, 214, 214)), 57 | pointColor: UIColor = .init(rgb: (165, 165, 165)), 58 | lineWidth: CGFloat = 1) { 59 | self.color = color 60 | self.lineWidth = lineWidth 61 | self.pointColor = pointColor 62 | pointBack.frame = CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height) 63 | super.init() 64 | self.frame = frame 65 | backgroundColor = UIColor.clear.cgColor 66 | pointBack.backgroundColor = UIColor.clear.cgColor 67 | drawCircle() 68 | addSublayer(pointBack) 69 | drawPoint() 70 | addCheckLayer() 71 | } 72 | 73 | required init?(coder aDecoder: NSCoder) { 74 | fatalError("init(coder:) has not been implemented") 75 | } 76 | 77 | //MARK: Public Methods 78 | func startAnimation() { 79 | circle.isHidden = false 80 | point.isHidden = false 81 | 82 | codeTimer = DispatchSource.makeTimerSource(queue: DispatchQueue.global()) 83 | codeTimer?.schedule(deadline: .now(), repeating: .milliseconds(42)) 84 | codeTimer?.setEventHandler(handler: { [weak self] in 85 | guard let `self` = self else { 86 | return 87 | } 88 | self.rotated = self.rotated - self.rotatedSpeed 89 | if self.stop { 90 | let count = Int(self.rotated / CGFloat(Double.pi * 2)) 91 | if (CGFloat(Double.pi * 2 * Double(count)) - self.rotated) >= 1.1 { 92 | var transform = CGAffineTransform.identity 93 | transform = transform.rotated(by: -1.1) 94 | DispatchQueue.main.async { 95 | self.pointBack.setAffineTransform(transform) 96 | self.point.isHidden = true 97 | self.check?.startAnimation() 98 | } 99 | self.codeTimer?.cancel() 100 | return 101 | } 102 | } 103 | if self.rotatedSpeed < 0.65 { 104 | if self.speedInterval < 0.02 { 105 | self.speedInterval = self.speedInterval + 0.001 106 | } 107 | self.rotatedSpeed = self.rotatedSpeed + self.speedInterval 108 | } 109 | var transform = CGAffineTransform.identity 110 | transform = transform.rotated(by: self.rotated) 111 | DispatchQueue.main.async { 112 | self.pointBack.setAffineTransform(transform) 113 | } 114 | }) 115 | codeTimer?.resume() 116 | 117 | addPointAnimation() 118 | } 119 | 120 | func endAnimation(finish: Bool) { 121 | if finish { 122 | stop = false 123 | rotated = 0 124 | rotatedSpeed = 0 125 | speedInterval = 0 126 | pointBack.setAffineTransform(CGAffineTransform.identity) 127 | circle.isHidden = true 128 | point.isHidden = true 129 | codeTimer?.cancel() 130 | check?.endAnimation() 131 | }else { 132 | DispatchQueue.global().async { 133 | self.stop = true 134 | } 135 | } 136 | } 137 | 138 | //MARK: Privater Methods 139 | private func drawCircle() { 140 | let width = frame.size.width 141 | let height = frame.size.height 142 | let path = UIBezierPath() 143 | path.addArc(withCenter: .init(x: width/2, 144 | y: height/2), 145 | radius: height/2, 146 | startAngle: 0, 147 | endAngle: CGFloat(Double.pi * 2.0), 148 | clockwise: false) 149 | circle.lineWidth = lineWidth 150 | circle.strokeColor = color.cgColor 151 | circle.fillColor = UIColor.clear.cgColor 152 | circle.path = path.cgPath 153 | addSublayer(circle) 154 | circle.isHidden = true 155 | } 156 | 157 | private func drawPoint() { 158 | let width = frame.size.width 159 | let path = UIBezierPath() 160 | path.addArc(withCenter: .init(x: width/2, y: width/2), 161 | radius: width/2, 162 | startAngle: CGFloat(Double.pi * 0.5), 163 | endAngle: CGFloat((Double.pi * 0.5) - 0.1), 164 | clockwise: false) 165 | point.lineCap = CAShapeLayerLineCap.round 166 | point.lineWidth = lineWidth*2 167 | point.fillColor = UIColor.clear.cgColor 168 | point.strokeColor = pointColor.cgColor 169 | point.path = path.cgPath 170 | pointBack.addSublayer(point) 171 | point.isHidden = true 172 | } 173 | 174 | private func addPointAnimation() { 175 | let width = frame.size.width 176 | let path = CABasicAnimation(keyPath: "path") 177 | path.beginTime = CACurrentMediaTime() + 1 178 | path.fromValue = point.path 179 | let toPath = UIBezierPath() 180 | toPath.addArc(withCenter: .init(x: width/2, y: width/2), 181 | radius: width/2, 182 | startAngle: CGFloat(Double.pi * 0.5), 183 | endAngle: CGFloat((Double.pi * 0.5) - 0.3), 184 | clockwise: false) 185 | path.toValue = toPath.cgPath 186 | path.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) 187 | path.duration = 2 188 | path.isRemovedOnCompletion = false 189 | path.fillMode = CAMediaTimingFillMode.forwards 190 | point.add(path, forKey: "path") 191 | } 192 | 193 | private func addCheckLayer() { 194 | check = FastCheckLayer(frame: CGRect(x: 0, 195 | y: 0, 196 | width: frame.size.width, 197 | height: frame.size.height), 198 | color: pointColor, 199 | lineWidth: lineWidth) 200 | addSublayer(check!) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/FastAnimator/FastLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FastLayer.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:https://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class FastLayer 22 | // @abstract FastLayer 23 | // @discussion FastLayer 24 | // 25 | 26 | import UIKit 27 | 28 | class FastLayer: CALayer { 29 | 30 | private (set)var circle: FastCircleLayer? 31 | 32 | private (set)var arrow: FastArrowLayer? 33 | 34 | let color: UIColor 35 | 36 | let arrowColor: UIColor 37 | 38 | let lineWidth: CGFloat 39 | 40 | //MARK: Public Methods 41 | 42 | 43 | //MARK: Override 44 | 45 | 46 | //MARK: Initial Methods 47 | init(frame: CGRect, color: UIColor = .init(rgb: (214, 214, 214)), arrowColor: UIColor = .init(rgb: (165, 165, 165)), lineWidth: CGFloat = 1) { 48 | self.color = color 49 | self.arrowColor = arrowColor 50 | self.lineWidth = lineWidth 51 | super.init() 52 | self.frame = frame 53 | backgroundColor = UIColor.clear.cgColor 54 | initCircle() 55 | initArrowLayer() 56 | } 57 | 58 | required init?(coder aDecoder: NSCoder) { 59 | fatalError("init(coder:) has not been implemented") 60 | } 61 | 62 | //MARK: Privater Methods 63 | private func initCircle() { 64 | circle = FastCircleLayer(frame: bounds, color: color, pointColor: arrowColor, lineWidth: lineWidth) 65 | addSublayer(circle!) 66 | } 67 | 68 | private func initArrowLayer() { 69 | arrow = FastArrowLayer(frame: bounds, color: arrowColor, lineWidth: lineWidth) 70 | addSublayer(arrow!) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/NormalAnimator/NormalFooter.bundle/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "CRRefreshFooterIdleText" = "Loading more"; 2 | "CRRefreshFooterNoMoreText" = "No more data"; 3 | "CRRefreshFooterRefreshingText" = "Loading..."; 4 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/NormalAnimator/NormalFooter.bundle/zh.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "CRRefreshFooterIdleText" = "上拉加载更多"; 2 | "CRRefreshFooterNoMoreText" = "没有更多数据"; 3 | "CRRefreshFooterRefreshingText" = "正在加载更多的数据..."; 4 | 5 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/NormalAnimator/NormalFooterAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NormalFooterAnimator.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:http://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class NormalFooterAnimator 22 | // @abstract 普通底部加载Animator 23 | // @discussion 普通底部加载Animator 24 | // 25 | 26 | import UIKit 27 | 28 | open class NormalFooterAnimator: UIView, CRRefreshProtocol { 29 | 30 | static let crBundle = CRRefreshBundle.bundle(name: "NormalFooter", for: NormalFooterAnimator.self) 31 | 32 | open var loadingMoreDescription = crBundle?.localizedString(key: "CRRefreshFooterIdleText") 33 | open var noMoreDataDescription = crBundle?.localizedString(key: "CRRefreshFooterNoMoreText") 34 | open var loadingDescription = crBundle?.localizedString(key: "CRRefreshFooterRefreshingText") 35 | 36 | open var view: UIView { return self } 37 | open var duration: TimeInterval = 0.3 38 | open var insets: UIEdgeInsets = .zero 39 | open var trigger: CGFloat = 50.0 40 | open var execute: CGFloat = 50.0 41 | open var endDelay: CGFloat = 0 42 | open var hold: CGFloat = 50 43 | 44 | fileprivate lazy var titleLabel: UILabel = { 45 | let label = UILabel.init(frame: CGRect.zero) 46 | label.font = UIFont.systemFont(ofSize: 14.0) 47 | label.textColor = UIColor.init(white: 160.0 / 255.0, alpha: 1.0) 48 | label.textAlignment = .center 49 | return label 50 | }() 51 | 52 | fileprivate lazy var indicatorView: UIActivityIndicatorView = { 53 | let indicatorView = UIActivityIndicatorView.init(style: .gray) 54 | indicatorView.isHidden = true 55 | return indicatorView 56 | }() 57 | 58 | public override init(frame: CGRect) { 59 | super.init(frame: frame) 60 | titleLabel.text = loadingMoreDescription 61 | addSubview(titleLabel) 62 | addSubview(indicatorView) 63 | } 64 | 65 | public required init(coder aDecoder: NSCoder) { 66 | fatalError("init(coder:) has not been implemented") 67 | } 68 | 69 | open func refreshBegin(view: CRRefreshComponent) { 70 | indicatorView.startAnimating() 71 | titleLabel.text = loadingDescription 72 | indicatorView.isHidden = false 73 | } 74 | 75 | public func refreshWillEnd(view: CRRefreshComponent) { 76 | 77 | } 78 | 79 | open func refreshEnd(view: CRRefreshComponent, finish: Bool) { 80 | indicatorView.stopAnimating() 81 | titleLabel.text = loadingMoreDescription 82 | indicatorView.isHidden = true 83 | } 84 | 85 | open func refresh(view: CRRefreshComponent, progressDidChange progress: CGFloat) { 86 | 87 | } 88 | 89 | open func refresh(view: CRRefreshComponent, stateDidChange state: CRRefreshState) { 90 | switch state { 91 | case .idle: 92 | titleLabel.text = loadingMoreDescription 93 | case .refreshing : 94 | titleLabel.text = loadingDescription 95 | break 96 | case .noMoreData: 97 | titleLabel.text = noMoreDataDescription 98 | break 99 | case .pulling: 100 | titleLabel.text = loadingMoreDescription 101 | break 102 | default: 103 | break 104 | } 105 | setNeedsLayout() 106 | } 107 | 108 | open override func layoutSubviews() { 109 | super.layoutSubviews() 110 | let s = self.bounds.size 111 | let w = s.width 112 | let h = s.height 113 | titleLabel.sizeToFit() 114 | titleLabel.center = CGPoint.init(x: w / 2.0, y: h / 2.0 - 5.0 + insets.top) 115 | indicatorView.center = CGPoint.init(x: titleLabel.frame.origin.x - 18.0, y: titleLabel.center.y) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/NormalAnimator/NormalHeader.bundle/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "CRRefreshHeaderIdleText" = "Pull down to refresh"; 2 | "CRRefreshHeaderPullingText" = "Release to refresh"; 3 | "CRRefreshHeaderRefreshingText" = "Loading..."; 4 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/NormalAnimator/NormalHeader.bundle/images/refresh_arrow@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/CRRefresh/CRRefresh/Animators/NormalAnimator/NormalHeader.bundle/images/refresh_arrow@2x.png -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/NormalAnimator/NormalHeader.bundle/images/refresh_arrow@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/CRRefresh/CRRefresh/Animators/NormalAnimator/NormalHeader.bundle/images/refresh_arrow@3x.png -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/NormalAnimator/NormalHeader.bundle/zh.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "CRRefreshHeaderIdleText" = "下拉可以刷新"; 2 | "CRRefreshHeaderPullingText" = "松开立即刷新"; 3 | "CRRefreshHeaderRefreshingText" = "正在刷新数据中..."; 4 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/NormalAnimator/NormalHeaderAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NormalHeaderAnimator.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:http://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class NormalFootAnimator 22 | // @abstract 普通顶部加载Animator 23 | // @discussion 普通顶部加载Animator 24 | // 25 | 26 | import UIKit 27 | 28 | open class NormalHeaderAnimator: UIView, CRRefreshProtocol { 29 | 30 | static let crBundle = CRRefreshBundle.bundle(name: "NormalHeader", for: NormalHeaderAnimator.self) 31 | 32 | open var pullToRefreshDescription = crBundle?.localizedString(key: "CRRefreshHeaderIdleText") { 33 | didSet { 34 | if pullToRefreshDescription != oldValue { 35 | titleLabel.text = pullToRefreshDescription; 36 | } 37 | } 38 | } 39 | open var releaseToRefreshDescription = crBundle?.localizedString(key: "CRRefreshHeaderPullingText") 40 | open var loadingDescription = crBundle?.localizedString(key: "CRRefreshHeaderRefreshingText") 41 | 42 | open var view: UIView { return self } 43 | open var insets: UIEdgeInsets = .zero 44 | open var trigger: CGFloat = 60.0 45 | open var execute: CGFloat = 60.0 46 | open var endDelay: CGFloat = 0 47 | public var hold: CGFloat = 60 48 | 49 | fileprivate let imageView: UIImageView = { 50 | let imageView = UIImageView.init() 51 | imageView.image = crBundle?.imageFromBundle("refresh_arrow") 52 | return imageView 53 | }() 54 | 55 | fileprivate let titleLabel: UILabel = { 56 | let label = UILabel.init(frame: CGRect.zero) 57 | label.font = UIFont.systemFont(ofSize: 14.0) 58 | label.textColor = UIColor.init(white: 0.625, alpha: 1.0) 59 | label.textAlignment = .left 60 | return label 61 | }() 62 | 63 | fileprivate let indicatorView: UIActivityIndicatorView = { 64 | let indicatorView = UIActivityIndicatorView.init(style: .gray) 65 | indicatorView.isHidden = true 66 | return indicatorView 67 | }() 68 | 69 | public override init(frame: CGRect) { 70 | super.init(frame: frame) 71 | titleLabel.text = pullToRefreshDescription 72 | self.addSubview(imageView) 73 | self.addSubview(titleLabel) 74 | self.addSubview(indicatorView) 75 | } 76 | 77 | public required init(coder aDecoder: NSCoder) { 78 | fatalError("init(coder:) has not been implemented") 79 | } 80 | 81 | open func refreshBegin(view: CRRefreshComponent) { 82 | indicatorView.startAnimating() 83 | indicatorView.isHidden = false 84 | imageView.isHidden = true 85 | titleLabel.text = loadingDescription 86 | imageView.transform = CGAffineTransform(rotationAngle: 0.000001 - CGFloat(Double.pi)) 87 | } 88 | 89 | open func refreshEnd(view: CRRefreshComponent, finish: Bool) { 90 | if finish { 91 | indicatorView.stopAnimating() 92 | indicatorView.isHidden = true 93 | imageView.isHidden = false 94 | imageView.transform = CGAffineTransform.identity 95 | }else { 96 | titleLabel.text = pullToRefreshDescription 97 | setNeedsLayout() 98 | } 99 | } 100 | 101 | public func refreshWillEnd(view: CRRefreshComponent) { 102 | 103 | } 104 | 105 | open func refresh(view: CRRefreshComponent, progressDidChange progress: CGFloat) { 106 | 107 | } 108 | 109 | open func refresh(view: CRRefreshComponent, stateDidChange state: CRRefreshState) { 110 | switch state { 111 | case .refreshing: 112 | titleLabel.text = loadingDescription 113 | setNeedsLayout() 114 | break 115 | case .pulling: 116 | titleLabel.text = releaseToRefreshDescription 117 | self.setNeedsLayout() 118 | UIView.animate(withDuration: 0.2, delay: 0.0, options: UIView.AnimationOptions(), animations: { 119 | [weak self] in 120 | self?.imageView.transform = CGAffineTransform(rotationAngle: 0.000001 - CGFloat(Double.pi)) 121 | }) { (animated) in } 122 | break 123 | case .idle: 124 | titleLabel.text = pullToRefreshDescription 125 | self.setNeedsLayout() 126 | UIView.animate(withDuration: 0.2, delay: 0.0, options: UIView.AnimationOptions(), animations: { 127 | [weak self] in 128 | self?.imageView.transform = CGAffineTransform.identity 129 | }) { (animated) in } 130 | break 131 | default: 132 | break 133 | } 134 | } 135 | 136 | open override func layoutSubviews() { 137 | super.layoutSubviews() 138 | let s = bounds.size 139 | let w = s.width 140 | let h = s.height 141 | 142 | UIView.performWithoutAnimation { 143 | titleLabel.sizeToFit() 144 | titleLabel.center = .init(x: w / 2.0, y: h / 2.0) 145 | indicatorView.center = .init(x: titleLabel.frame.origin.x - 16.0, y: h / 2.0) 146 | imageView.frame = CGRect.init(x: titleLabel.frame.origin.x - 28.0, y: (h - 18.0) / 2.0, width: 18.0, height: 18.0) 147 | } 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/RamotionAnimator/RamotionAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RamotionAnimator.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:http://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class RamotionAnimator 22 | // @abstract Ramotion动画的Animator 23 | // @discussion Ramotion动画的Animator 24 | // 25 | 26 | import UIKit 27 | 28 | open class RamotionAnimator: UIView, CRRefreshProtocol { 29 | 30 | open var view: UIView { return self } 31 | 32 | open var insets: UIEdgeInsets = .zero 33 | 34 | open var trigger: CGFloat = 140 35 | 36 | open var execute: CGFloat = 90 37 | 38 | open var endDelay: CGFloat = 0 39 | 40 | open var hold: CGFloat = 90 41 | 42 | 43 | var bounceLayer: RamotionBounceLayer? 44 | /// 上方wave的颜色 45 | let waveColor: UIColor 46 | /// 球的颜色 47 | let ballColor: UIColor 48 | 49 | deinit { 50 | bounceLayer?.clear() 51 | } 52 | 53 | /// 初始化方法 54 | /// 55 | /// - Parameters: 56 | /// - ballColor: 小球的颜色 57 | /// - waveColor: 上方wave的颜色 58 | public init(ballColor: UIColor = .white, 59 | waveColor: UIColor = .init(rgb: (140, 141, 178))) { 60 | self.ballColor = ballColor 61 | self.waveColor = waveColor 62 | super.init(frame: .zero) 63 | } 64 | 65 | func bounceLayer(view: CRRefreshComponent) -> RamotionBounceLayer? { 66 | if bounceLayer?.superlayer == nil { 67 | if let scrollView = view.scrollView { 68 | bounceLayer = RamotionBounceLayer(frame: scrollView.bounds, execute: execute, ballColor: ballColor, waveColor: waveColor) 69 | if let superView = scrollView.superview { 70 | superView.layer.addSublayer(bounceLayer!) 71 | } 72 | } 73 | } 74 | return bounceLayer 75 | } 76 | 77 | open func refreshBegin(view: CRRefreshComponent) { 78 | bounceLayer(view: view)?.wave(execute) 79 | bounceLayer(view: view)?.startAnimation() 80 | } 81 | 82 | open func refreshEnd(view: CRRefreshComponent, finish: Bool) { 83 | if !finish { 84 | bounceLayer(view: view)?.endAnimation() 85 | }else { 86 | bounceLayer(view: view)?.ballLayer.isHidden = true 87 | bounceLayer(view: view)?.linkLayer.isHidden = true 88 | bounceLayer(view: view)?.wavelayer.endAnimation() 89 | } 90 | } 91 | 92 | open func refresh(view: CRRefreshComponent, progressDidChange progress: CGFloat) { 93 | let offY = trigger * progress 94 | bounceLayer(view: view)?.wave(offY) 95 | } 96 | 97 | open func refresh(view: CRRefreshComponent, stateDidChange state: CRRefreshState) { 98 | 99 | } 100 | 101 | open func refreshWillEnd(view: CRRefreshComponent) { 102 | 103 | } 104 | 105 | public override init(frame: CGRect) { 106 | self.ballColor = .white 107 | self.waveColor = .init(rgb: (140, 141, 178)) 108 | super.init(frame: frame) 109 | } 110 | 111 | required public init?(coder aDecoder: NSCoder) { 112 | fatalError("init(coder:) has not been implemented") 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/RamotionAnimator/RamotionBallLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RamotionBallLayer.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:https://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class RamotionBallLayer 22 | // @abstract RamotionBallLayer 23 | // @discussion RamotionBallLayer 24 | // 25 | 26 | import UIKit 27 | 28 | private var timeFunc = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) 29 | 30 | private var upDuration: Double = 0.5 31 | 32 | class RamotionBallLayer: CALayer { 33 | 34 | deinit { 35 | circleLayer.stopAnimation() 36 | } 37 | 38 | var circleLayer: CircleLayer! 39 | 40 | init(frame: CGRect, duration: CFTimeInterval, moveUpDist: CGFloat, color: UIColor = .white) { 41 | upDuration = duration 42 | super.init() 43 | self.frame = frame 44 | let circleWidth = min(frame.size.width, frame.size.height) 45 | circleLayer = CircleLayer(size: circleWidth, moveUpDist: moveUpDist, frame: frame, color: color) 46 | addSublayer(circleLayer) 47 | isHidden = true 48 | } 49 | 50 | override init(layer: Any) { 51 | super.init(layer: layer) 52 | } 53 | 54 | required init?(coder aDecoder: NSCoder) { 55 | fatalError("init(coder:) has not been implemented") 56 | } 57 | 58 | func startAnimation() { 59 | circleLayer.startAnimation() 60 | } 61 | 62 | func endAnimation(_ complition: (()-> Void)? = nil) { 63 | circleLayer.endAnimation(complition) 64 | } 65 | } 66 | 67 | class CircleLayer :CAShapeLayer, CAAnimationDelegate { 68 | 69 | deinit { 70 | spiner?.stopAnimation() 71 | } 72 | 73 | var moveUpDist: CGFloat = 0 74 | var spiner: SpinerLayer? 75 | var didEndAnimation: (() -> Void)? 76 | 77 | init(size: CGFloat, moveUpDist: CGFloat, frame: CGRect, color: UIColor = UIColor.white) { 78 | self.moveUpDist = moveUpDist 79 | let selfFrame = CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height) 80 | spiner = SpinerLayer(superLayerFrame: selfFrame, ballSize: size, color: color) 81 | super.init() 82 | 83 | addSublayer(spiner!) 84 | 85 | let radius:CGFloat = size / 2 86 | self.frame = selfFrame 87 | let center = CGPoint(x: frame.size.width / 2, y: frame.size.height/2) 88 | let startAngle = 0 - Double.pi/2 89 | let endAngle = Double.pi * 2 - Double.pi/2 90 | let clockwise: Bool = true 91 | self.path = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: clockwise).cgPath 92 | self.fillColor = color.withAlphaComponent(1).cgColor 93 | self.strokeColor = self.fillColor 94 | self.lineWidth = 0 95 | self.strokeEnd = 1 96 | } 97 | 98 | override init(layer: Any) { 99 | super.init(layer: layer) 100 | } 101 | 102 | required init?(coder aDecoder: NSCoder) { 103 | fatalError("init(coder:) has not been implemented") 104 | } 105 | 106 | func startAnimation() { 107 | moveUp(moveUpDist) 108 | DispatchQueue.main.asyncAfter(deadline: .now() + upDuration) { [weak self] in 109 | self?.spiner?.animation() 110 | } 111 | } 112 | 113 | func endAnimation(_ complition:(()->())? = nil) { 114 | spiner?.stopAnimation() 115 | moveDown(moveUpDist) 116 | didEndAnimation = complition 117 | } 118 | 119 | func stopAnimation() { 120 | spiner?.stopAnimation() 121 | removeAllAnimations() 122 | didEndAnimation?() 123 | } 124 | 125 | func moveUp(_ distance: CGFloat) { 126 | let move = CABasicAnimation(keyPath: "position") 127 | 128 | move.fromValue = NSValue(cgPoint: position) 129 | move.toValue = NSValue(cgPoint: CGPoint(x: position.x, 130 | y: position.y - distance)) 131 | 132 | move.duration = upDuration 133 | move.timingFunction = timeFunc 134 | 135 | move.fillMode = CAMediaTimingFillMode.forwards 136 | move.isRemovedOnCompletion = false 137 | add(move, forKey: move.keyPath) 138 | } 139 | 140 | func moveDown(_ distance: CGFloat) { 141 | let move = CABasicAnimation(keyPath: "position") 142 | 143 | move.fromValue = NSValue(cgPoint: CGPoint(x: position.x, y: position.y - distance)) 144 | move.toValue = NSValue(cgPoint: position) 145 | 146 | move.duration = upDuration 147 | move.timingFunction = timeFunc 148 | 149 | move.fillMode = CAMediaTimingFillMode.forwards 150 | move.isRemovedOnCompletion = false 151 | move.delegate = self 152 | add(move, forKey: move.keyPath) 153 | } 154 | 155 | func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { 156 | didEndAnimation?() 157 | } 158 | } 159 | 160 | 161 | class SpinerLayer: CAShapeLayer, CAAnimationDelegate { 162 | 163 | init(superLayerFrame: CGRect, ballSize: CGFloat, color: UIColor = UIColor.white) { 164 | super.init() 165 | 166 | let radius:CGFloat = (ballSize / 2) * 1.2//1.45 167 | self.frame = CGRect(x: 0, y: 0, width: superLayerFrame.height, height: superLayerFrame.height) 168 | let center = CGPoint(x: superLayerFrame.size.width / 2, y: superLayerFrame.origin.y + superLayerFrame.size.height/2) 169 | let startAngle = 0 - Double.pi/2 170 | let endAngle = (Double.pi * 2 - Double.pi/2) + Double.pi / 8 171 | let clockwise: Bool = true 172 | self.path = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: clockwise).cgPath 173 | 174 | self.fillColor = nil 175 | self.strokeColor = color.cgColor 176 | self.lineWidth = 2 177 | self.lineCap = CAShapeLayerLineCap.round 178 | 179 | self.strokeStart = 0 180 | self.strokeEnd = 0 181 | self.isHidden = true 182 | 183 | } 184 | 185 | required init?(coder aDecoder: NSCoder) { 186 | fatalError("init(coder:) has not been implemented") 187 | } 188 | 189 | func animation() { 190 | self.isHidden = false 191 | let rotate = CABasicAnimation(keyPath: "transform.rotation.z") 192 | rotate.fromValue = 0 193 | rotate.toValue = Double.pi * 2 194 | rotate.duration = 1 195 | rotate.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) 196 | rotate.repeatCount = HUGE 197 | rotate.fillMode = CAMediaTimingFillMode.forwards 198 | rotate.isRemovedOnCompletion = false 199 | self.add(rotate, forKey: rotate.keyPath) 200 | 201 | strokeEndAnimation() 202 | } 203 | 204 | func strokeEndAnimation() { 205 | let endPoint = CABasicAnimation(keyPath: "strokeEnd") 206 | endPoint.fromValue = 0 207 | endPoint.toValue = 1 208 | endPoint.duration = 1.8 209 | endPoint.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) 210 | endPoint.repeatCount = HUGE 211 | endPoint.fillMode = CAMediaTimingFillMode.forwards 212 | endPoint.isRemovedOnCompletion = false 213 | endPoint.delegate = self 214 | add(endPoint, forKey: endPoint.keyPath) 215 | } 216 | 217 | func strokeStartAnimation() { 218 | let startPoint = CABasicAnimation(keyPath: "strokeStart") 219 | startPoint.fromValue = 0 220 | startPoint.toValue = 1 221 | startPoint.duration = 0.8 222 | startPoint.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) 223 | startPoint.repeatCount = HUGE 224 | startPoint.delegate = self 225 | add(startPoint, forKey: startPoint.keyPath) 226 | } 227 | 228 | func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { 229 | if self.isHidden == false { 230 | let a:CABasicAnimation = anim as! CABasicAnimation 231 | if a.keyPath == "strokeStart" { 232 | strokeEndAnimation() 233 | }else if a.keyPath == "strokeEnd" { 234 | strokeStartAnimation() 235 | } 236 | } 237 | } 238 | 239 | func stopAnimation() { 240 | isHidden = true 241 | removeAllAnimations() 242 | } 243 | } 244 | 245 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/RamotionAnimator/RamotionBounceLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RamotionBounceLayer.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:https://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class RamotionBounceLayer 22 | // @abstract 背景layer 23 | // @discussion 背景layer 24 | // 25 | 26 | import UIKit 27 | 28 | class RamotionBounceLayer: CALayer { 29 | // 动画时间 30 | var animDuration: CFTimeInterval = 0.45 31 | // 白色渐变的背景 32 | let backLayer = CALayer() 33 | // 上方的变动的背景 34 | let wavelayer: RamotionWaveLayer 35 | // 球 36 | let ballLayer: RamotionBallLayer 37 | // 连接线 38 | let linkLayer: CAShapeLayer = CAShapeLayer() 39 | // 执行动画的高度 40 | var execute: CGFloat = 0 41 | // 计时器 42 | var displayLink: CADisplayLink? 43 | // 记录之前的offY 44 | private var previousOffY: CGFloat = 0 45 | /// 上方wave的颜色 46 | let waveColor: UIColor 47 | /// 球的颜色 48 | let ballColor: UIColor 49 | 50 | deinit { 51 | displayLink?.invalidate() 52 | displayLink = nil 53 | } 54 | 55 | //MARK: Initial Methods 56 | init(frame: CGRect, execute: CGFloat, ballColor: UIColor, waveColor: UIColor) { 57 | self.waveColor = waveColor 58 | self.ballColor = ballColor 59 | self.execute = execute 60 | wavelayer = .init(frame: .init(origin: .zero, size: frame.size), execute: execute, bounceDuration: animDuration, color: waveColor) 61 | ballLayer = .init(frame: .init(x: frame.width/2 - 20, y: execute + 40, width: 40, height: 40), duration: animDuration, moveUpDist: 60 + execute/2, color: ballColor) 62 | linkLayer.fillColor = ballColor.cgColor 63 | super.init() 64 | backgroundColor = UIColor.clear.cgColor 65 | backLayer.frame = .init(origin: .zero, size: frame.size) 66 | backLayer.backgroundColor = ballColor.cgColor 67 | backLayer.opacity = 0 68 | addSublayer(backLayer) 69 | addSublayer(wavelayer) 70 | addSublayer(ballLayer) 71 | addSublayer(linkLayer) 72 | } 73 | 74 | required init?(coder aDecoder: NSCoder) { 75 | fatalError("init(coder:) has not been implemented") 76 | } 77 | 78 | 79 | //MARK: Public Methods 80 | func wave(_ y: CGFloat) { 81 | wavelayer.wave(y, execute: execute) 82 | let progress = y/execute 83 | backLayer.opacity = Float(progress) 84 | } 85 | 86 | func startAnimation() { 87 | ballLayer.isHidden = false 88 | linkLayer.isHidden = false 89 | addDisPlay() 90 | wavelayer.startAnimation() 91 | ballLayer.startAnimation() 92 | } 93 | 94 | func endAnimation() { 95 | ballLayer.endAnimation { [weak self] in 96 | self?.removeDisPlay() 97 | self?.wave(0) 98 | } 99 | } 100 | 101 | func clear() { 102 | displayLink?.invalidate() 103 | displayLink = nil 104 | } 105 | 106 | //MARK: Private Methods 107 | @objc private func displayAction() { 108 | let offY = ballLayer.circleLayer.presentation()?.frame.origin.y 109 | let frame1 = ballLayer.frame 110 | let frame2 = wavelayer.reference.layer.presentation()?.frame 111 | if let offY = offY, let frame2 = frame2 { 112 | DispatchQueue.global().async { 113 | // 判断是球是向上还是下,false为上,速度快时,获取的位置不及时,向下时需要调整位置 114 | let isIncrement = (offY - self.previousOffY) > 0 115 | let path = UIBezierPath() 116 | let x1 = frame1.origin.x + (isIncrement ? 4 : 0) 117 | let y1 = frame1.origin.y + offY 118 | let w1 = frame1.size.width - (isIncrement ? 8 : 0) 119 | let h1 = frame1.size.height 120 | let x2 = frame2.origin.x 121 | let y2 = frame2.origin.y 122 | let w2 = frame2.size.width 123 | let h2 = frame2.size.height 124 | let subY = y2 - y1 125 | // y1和y2的间距 126 | let subScale = subY/self.execute/2 127 | // 断开的距离为10 128 | let executeSub = self.ballLayer.circleLayer.moveUpDist + offY 129 | if executeSub < 10 { 130 | if !isIncrement { 131 | let executeSubScale = executeSub/10 132 | path.move(to: .init(x: x1 - 15, y: y2 + h2/2 + 15)) 133 | path.addLine(to: .init(x: x1 + w1 + 15, y: y2 + h2/2 + 15)) 134 | path.addQuadCurve(to: .init(x: x1 - 15, y: y2 + h2/2 + 15), controlPoint: .init(x: x1 + w1/2, y: y2 + h2/2 - self.execute/6 * executeSubScale)) 135 | } 136 | }else { 137 | path.move(to: .init(x: x2 , y: y2 + h2)) 138 | path.addLine(to: .init(x: x2 + w2, y: y2 + h2)) 139 | path.addQuadCurve(to: .init(x: x1 + w1, y: y1 + h1/2), controlPoint: .init(x: x1 + w1 - w1*2*subScale, y: y1 + (y2 - y1)/2 + h1/2 + h2/2)) 140 | path.addLine(to: .init(x: x1, y: y1 + h1/2)) 141 | path.addQuadCurve(to: .init(x: x2 , y: y2 + h2), controlPoint: .init(x: x1 + w1*2*subScale, y: y1 + (y2 - y1)/2 + h1/2 + h2/2)) 142 | if y1 + h1 <= self.execute, isIncrement { 143 | DispatchQueue.main.async { 144 | self.wavelayer.startDownAnimation() 145 | } 146 | } 147 | } 148 | DispatchQueue.main.async { 149 | self.linkLayer.path = path.cgPath 150 | } 151 | self.previousOffY = offY 152 | } 153 | } 154 | } 155 | 156 | private func addDisPlay() { 157 | displayLink = CADisplayLink(target: self, selector: #selector(displayAction)) 158 | displayLink?.add(to: .main, forMode: .common) 159 | } 160 | 161 | private func removeDisPlay() { 162 | displayLink?.invalidate() 163 | displayLink = nil 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/RamotionAnimator/RamotionWaveLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RamotionWaveLayer.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:https://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class RamotionWaveLayer 22 | // @abstract 上方的wavelayer 23 | // @discussion 上方的wavelayer 24 | // 25 | 26 | import UIKit 27 | 28 | private let referenceWitdh: CGFloat = 150 29 | private let referenceHeight: CGFloat = 50 30 | 31 | class RamotionWaveLayer: CALayer, CAAnimationDelegate { 32 | 33 | // 上方的wavelayer 34 | let waveLayer: CAShapeLayer = CAShapeLayer() 35 | // 参考的UIView 36 | let reference: UIView = UIView(frame: .init(x: 0, y: 0, width: referenceWitdh, height: referenceHeight)) 37 | // 动画时间 38 | var bounceDuration: CFTimeInterval 39 | // layer的颜色 40 | let color: UIColor 41 | // 执行动画的高度 42 | var execute: CGFloat 43 | // 是否在动画中 44 | var isAnimation: Bool = false 45 | // 计时器 46 | var displayLink: CADisplayLink? 47 | 48 | deinit { 49 | displayLink?.invalidate() 50 | displayLink = nil 51 | } 52 | 53 | //MARK: Initial Methods 54 | init(frame: CGRect, execute: CGFloat, bounceDuration: CFTimeInterval = 0.45, color: UIColor = .init(rgb: (140, 141, 178))) { 55 | self.bounceDuration = bounceDuration 56 | self.color = color 57 | self.execute = execute 58 | super.init() 59 | self.frame = frame 60 | backgroundColor = UIColor.clear.cgColor 61 | initWave() 62 | initReferenceLayer() 63 | } 64 | 65 | required init?(coder aDecoder: NSCoder) { 66 | fatalError("init(coder:) has not been implemented") 67 | } 68 | 69 | func initWave() { 70 | waveLayer.lineWidth = 0 71 | waveLayer.path = wavePath(x: 0.0, y: 0.0) 72 | waveLayer.strokeColor = color.cgColor 73 | waveLayer.fillColor = color.cgColor 74 | addSublayer(waveLayer) 75 | } 76 | 77 | func initReferenceLayer() { 78 | let w = frame.size.width 79 | reference.isUserInteractionEnabled = false 80 | reference.frame = .init(x: w/2 - referenceWitdh/2, y: -referenceHeight/2, width: referenceWitdh, height: referenceHeight) 81 | reference.backgroundColor = UIColor.clear 82 | waveLayer.addSublayer(reference.layer) 83 | var trans = CGAffineTransform.identity 84 | trans = trans.translatedBy(x: 0, y: execute) 85 | reference.transform = trans 86 | } 87 | 88 | //MARK: Public Methods 89 | func wave(_ y: CGFloat, execute: CGFloat) { 90 | self.execute = execute 91 | waveLayer.path = wavePath(x: 0, y: y) 92 | if !isAnimation { 93 | var trans = CGAffineTransform.identity 94 | trans = trans.translatedBy(x: 0, y: y) 95 | reference.transform = trans 96 | } 97 | } 98 | 99 | func startAnimation() { 100 | isAnimation = true 101 | addDisPlay() 102 | boundAnimation(x: 0, y: execute) 103 | } 104 | 105 | func startDownAnimation() { 106 | if !isAnimation { 107 | isAnimation = true 108 | addDisPlay() 109 | boundDownAnimation(x: 0, y: execute) 110 | } 111 | } 112 | 113 | func endAnimation() { 114 | endBoundAnimation() 115 | } 116 | 117 | //MARK: Privater Methods 118 | private func wavePath(x: CGFloat, y: CGFloat) -> CGPath { 119 | let w = frame.width 120 | let path = UIBezierPath() 121 | if y < execute { 122 | path.move(to: .zero) 123 | path.addLine(to: .init(x: w, y: 0)) 124 | path.addLine(to: .init(x: w, y: y)) 125 | path.addLine(to: .init(x: 0, y: y)) 126 | path.addLine(to: .zero) 127 | } else { 128 | path.move(to: .zero) 129 | path.addLine(to: .init(x: w, y: 0)) 130 | path.addLine(to: .init(x: w, y: execute)) 131 | path.addQuadCurve(to: .init(x: 0, y: execute), controlPoint: .init(x: w/2, y: y)) 132 | path.addLine(to: .zero) 133 | } 134 | return path.cgPath 135 | } 136 | 137 | private func displayWavePath(x: CGFloat, y: CGFloat) -> CGPath { 138 | let w = frame.width 139 | let path = UIBezierPath() 140 | path.move(to: .zero) 141 | path.addLine(to: .init(x: w, y: 0)) 142 | path.addLine(to: .init(x: w, y: execute)) 143 | path.addQuadCurve(to: .init(x: 0, y: execute), controlPoint: .init(x: w/2, y: y)) 144 | path.addLine(to: .zero) 145 | return path.cgPath 146 | } 147 | 148 | @objc private func displayAction() { 149 | if let frame = reference.layer.presentation()?.frame { 150 | DispatchQueue.global().async { 151 | let path = self.displayWavePath(x: 0, y: frame.origin.y + referenceHeight/2) 152 | DispatchQueue.main.async { 153 | self.waveLayer.path = path 154 | } 155 | } 156 | } 157 | } 158 | 159 | private func addDisPlay() { 160 | displayLink = CADisplayLink(target: self, selector: #selector(displayAction)) 161 | displayLink?.add(to: .main, forMode: .common) 162 | } 163 | 164 | private func removeDisPlay() { 165 | displayLink?.invalidate() 166 | displayLink = nil 167 | } 168 | 169 | private func endBoundAnimation() { 170 | let end = CABasicAnimation(keyPath: "path") 171 | end.duration = 0.25 172 | end.fromValue = wavePath(x: 0, y: execute) 173 | end.toValue = wavePath(x: 0, y: 0) 174 | waveLayer.add(end, forKey: "end") 175 | } 176 | 177 | private func boundAnimation(x: CGFloat, y: CGFloat) { 178 | let bounce = CAKeyframeAnimation(keyPath: "transform.translation.y") 179 | bounce.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) 180 | bounce.duration = bounceDuration 181 | bounce.values = [ 182 | reference.frame.origin.y, 183 | y * 0.5, 184 | y * 1.2, 185 | y * 0.8, 186 | y * 1.1, 187 | y 188 | ] 189 | bounce.isRemovedOnCompletion = true 190 | bounce.fillMode = CAMediaTimingFillMode.forwards 191 | bounce.delegate = self 192 | reference.layer.add(bounce, forKey: "return") 193 | } 194 | 195 | private func boundDownAnimation(x: CGFloat, y: CGFloat) { 196 | let bounce = CAKeyframeAnimation(keyPath: "transform.translation.y") 197 | bounce.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) 198 | bounce.duration = bounceDuration/2 199 | bounce.values = [ 200 | y, 201 | y * 1.1, 202 | y 203 | ] 204 | bounce.isRemovedOnCompletion = true 205 | bounce.fillMode = CAMediaTimingFillMode.forwards 206 | bounce.delegate = self 207 | reference.layer.add(bounce, forKey: "returnDown") 208 | } 209 | 210 | //MARK: CAAnimationDelegate 211 | func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { 212 | removeDisPlay() 213 | isAnimation = false 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/SlackLoadingAnimator/SlackLoadingAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SlackLoadingHeaderAnimator.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:http://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class SlackLoadingHeaderAnimator 22 | // @abstract SlackLoading的刷新效果 23 | // @discussion SlackLoading的刷新效果 24 | // 25 | 26 | import UIKit 27 | 28 | open class SlackLoadingAnimator: UIView, CRRefreshProtocol { 29 | 30 | open var view: UIView { return self } 31 | 32 | open var insets: UIEdgeInsets = .zero 33 | 34 | open var trigger: CGFloat = 60 35 | 36 | open var execute: CGFloat = 60 37 | 38 | open var endDelay: CGFloat = 0 39 | 40 | open var hold: CGFloat = 60 41 | 42 | var loadingView: WCLLoadingView = { 43 | let loadView = WCLLoadingView(frame: .init(x: 0, y: 0, width: 40, height: 40)) 44 | loadView.isUserInteractionEnabled = false 45 | return loadView 46 | }() 47 | 48 | open func refreshBegin(view: CRRefreshComponent) { 49 | loadingView.startAnimation() 50 | } 51 | 52 | open func refreshEnd(view: CRRefreshComponent, finish: Bool) { 53 | loadingView.stopAnimation() 54 | } 55 | 56 | open func refresh(view: CRRefreshComponent, progressDidChange progress: CGFloat) { 57 | 58 | } 59 | 60 | open func refresh(view: CRRefreshComponent, stateDidChange state: CRRefreshState) { 61 | 62 | } 63 | 64 | open func refreshWillEnd(view: CRRefreshComponent) { 65 | 66 | } 67 | 68 | override init(frame: CGRect) { 69 | super.init(frame: frame) 70 | addSubview(loadingView) 71 | } 72 | 73 | required public init?(coder aDecoder: NSCoder) { 74 | fatalError("init(coder:) has not been implemented") 75 | } 76 | 77 | open override func layoutSubviews() { 78 | super.layoutSubviews() 79 | let s = bounds.size 80 | let w = s.width 81 | let h = s.height 82 | loadingView.center = .init(x: w / 2.0, y: h / 2.0) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/Animators/SlackLoadingAnimator/WCLLoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WCLLoadingView.swift 3 | // WCL 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:https://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class WCLLoadingView 22 | // @abstract Slack 的 Loading 动画 23 | // @discussion Slack 的 Loading 动画 24 | // 25 | // 动画步骤解析:http://blog.csdn.net/wang631106979/article/details/52473985 26 | 27 | import UIKit 28 | 29 | class WCLLoadingView: UIView, CAAnimationDelegate { 30 | 31 | //线的宽度 32 | var lineWidth:CGFloat = 0 33 | //线的长度 34 | var lineLength:CGFloat = 0 35 | //边距 36 | var margin:CGFloat = 0 37 | //动画时间 38 | var duration:Double = 2 39 | //动画的间隔时间 40 | var interval:Double = 1 41 | //四条线的颜色 42 | var colors:[UIColor] = [UIColor(rgb: (157, 212, 233)) , UIColor(rgb: (245, 189, 88)), UIColor(rgb: (255, 49, 126)) , UIColor(rgb: (111, 201, 181))] 43 | //动画的状态 44 | private(set) var status:AnimationStatus = .normal 45 | //四条线 46 | private var lines:[CAShapeLayer] = [] 47 | 48 | enum AnimationStatus { 49 | //普通状态 50 | case normal 51 | //动画中 52 | case animating 53 | //暂停 54 | case pause 55 | } 56 | 57 | //MARK: Public Methods 58 | /** 59 | 开始动画 60 | */ 61 | func startAnimation() { 62 | angleAnimation() 63 | lineAnimationOne() 64 | lineAnimationTwo() 65 | lineAnimationThree() 66 | } 67 | 68 | /** 69 | 暂停动画 70 | */ 71 | func pauseAnimation() { 72 | layer.pauseAnimation() 73 | for lineLayer in lines { 74 | lineLayer.pauseAnimation() 75 | } 76 | status = .pause 77 | } 78 | 79 | /** 80 | 继续动画 81 | */ 82 | func resumeAnimation() { 83 | layer.resumeAnimation() 84 | for lineLayer in lines { 85 | lineLayer.resumeAnimation() 86 | } 87 | status = .animating 88 | } 89 | 90 | /** 91 | 结束动画 92 | */ 93 | func stopAnimation() { 94 | layer.removeAllAnimations() 95 | for lineLayer in lines { 96 | lineLayer.removeAllAnimations() 97 | } 98 | status = .normal 99 | } 100 | 101 | //MARK: Initial Methods 102 | convenience init(frame: CGRect , colors: [UIColor]) { 103 | self.init() 104 | self.frame = frame 105 | self.colors = colors 106 | config() 107 | } 108 | 109 | override init(frame: CGRect) { 110 | super.init(frame: frame) 111 | config() 112 | } 113 | 114 | required init?(coder aDecoder: NSCoder) { 115 | super.init(coder: aDecoder) 116 | config() 117 | } 118 | 119 | //MARK: Animation Delegate 120 | func animationDidStart(_ anim: CAAnimation) { 121 | if let animation = anim as? CABasicAnimation { 122 | if animation.keyPath == "transform.rotation.z" { 123 | status = .animating 124 | } 125 | } 126 | } 127 | 128 | func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { 129 | if let animation = anim as? CABasicAnimation { 130 | if animation.keyPath == "strokeEnd" { 131 | if flag { 132 | status = .normal 133 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(interval) * Int64(NSEC_PER_SEC)) / Double(NSEC_PER_SEC), execute: { 134 | if self.status != .animating { 135 | self.startAnimation() 136 | } 137 | }) 138 | } 139 | } 140 | } 141 | } 142 | 143 | //MARK: Privater Methods 144 | //MARK: 绘制线 145 | /** 146 | 绘制四条线 147 | */ 148 | private func drawLineShapeLayer() { 149 | //开始点 150 | let startPoint = [point(lineWidth/2, y: margin), 151 | point(lineLength - margin, y: lineWidth/2), 152 | point(lineLength - lineWidth/2, y: lineLength - margin), 153 | point(margin, y: lineLength - lineWidth/2)] 154 | //结束点 155 | let endPoint = [point(lineLength - lineWidth/2, y: margin) , 156 | point(lineLength - margin, y: lineLength - lineWidth/2) , 157 | point(lineWidth/2, y: lineLength - margin) , 158 | point(margin, y: lineWidth/2)] 159 | for i in 0...3 { 160 | let line:CAShapeLayer = CAShapeLayer() 161 | line.lineWidth = lineWidth 162 | line.lineCap = CAShapeLayerLineCap.round 163 | line.opacity = 0.8 164 | line.strokeColor = colors[i].cgColor 165 | line.path = getLinePath(startPoint[i], endPoint: endPoint[i]).cgPath 166 | layer.addSublayer(line) 167 | lines.append(line) 168 | } 169 | 170 | } 171 | 172 | /** 173 | 获取线的路径 174 | 175 | - parameter startPoint: 开始点 176 | - parameter endPoint: 结束点 177 | 178 | - returns: 线的路径 179 | */ 180 | private func getLinePath(_ startPoint: CGPoint, endPoint: CGPoint) -> UIBezierPath { 181 | let path = UIBezierPath() 182 | path.move(to: startPoint) 183 | path.addLine(to: endPoint) 184 | return path 185 | } 186 | 187 | //MARK: 动画步骤 188 | /** 189 | 旋转的动画,旋转两圈 190 | */ 191 | private func angleAnimation() { 192 | let angleAnimation = CABasicAnimation.init(keyPath: "transform.rotation.z") 193 | angleAnimation.beginTime = CACurrentMediaTime() 194 | angleAnimation.fromValue = angle(-30) 195 | angleAnimation.toValue = angle(690) 196 | angleAnimation.fillMode = CAMediaTimingFillMode.forwards 197 | angleAnimation.isRemovedOnCompletion = false 198 | angleAnimation.duration = duration 199 | angleAnimation.delegate = self 200 | layer.add(angleAnimation, forKey: "angleAnimation") 201 | } 202 | 203 | 204 | /** 205 | 线的第一步动画,线长从长变短 206 | */ 207 | private func lineAnimationOne() { 208 | let lineAnimationOne = CABasicAnimation.init(keyPath: "strokeEnd") 209 | lineAnimationOne.beginTime = CACurrentMediaTime() 210 | lineAnimationOne.duration = duration/2 211 | lineAnimationOne.fillMode = CAMediaTimingFillMode.forwards 212 | lineAnimationOne.isRemovedOnCompletion = false 213 | lineAnimationOne.fromValue = 1 214 | lineAnimationOne.toValue = 0 215 | for i in 0...3 { 216 | let lineLayer = lines[i] 217 | lineLayer.add(lineAnimationOne, forKey: "lineAnimationOne") 218 | } 219 | } 220 | 221 | /** 222 | 线的第二步动画,线向中间平移 223 | */ 224 | private func lineAnimationTwo() { 225 | for i in 0...3 { 226 | var keypath = "transform.translation.x" 227 | if i%2 == 1 { 228 | keypath = "transform.translation.y" 229 | } 230 | let lineAnimationTwo = CABasicAnimation.init(keyPath: keypath) 231 | lineAnimationTwo.beginTime = CACurrentMediaTime() + duration/2 232 | lineAnimationTwo.duration = duration/4 233 | lineAnimationTwo.fillMode = CAMediaTimingFillMode.forwards 234 | lineAnimationTwo.isRemovedOnCompletion = false 235 | lineAnimationTwo.autoreverses = true 236 | lineAnimationTwo.fromValue = 0 237 | if i < 2 { 238 | lineAnimationTwo.toValue = lineLength/4 239 | }else { 240 | lineAnimationTwo.toValue = -lineLength/4 241 | } 242 | let lineLayer = lines[i] 243 | lineLayer.add(lineAnimationTwo, forKey: "lineAnimationTwo") 244 | } 245 | 246 | //三角形两边的比例 247 | let scale = (lineLength - 2*margin)/(lineLength - lineWidth) 248 | for i in 0...3 { 249 | var keypath = "transform.translation.y" 250 | if i%2 == 1 { 251 | keypath = "transform.translation.x" 252 | } 253 | let lineAnimationTwo = CABasicAnimation.init(keyPath: keypath) 254 | lineAnimationTwo.beginTime = CACurrentMediaTime() + duration/2 255 | lineAnimationTwo.duration = duration/4 256 | lineAnimationTwo.fillMode = CAMediaTimingFillMode.forwards 257 | lineAnimationTwo.isRemovedOnCompletion = false 258 | lineAnimationTwo.autoreverses = true 259 | lineAnimationTwo.fromValue = 0 260 | if i == 0 || i == 3 { 261 | lineAnimationTwo.toValue = lineLength/4 * scale 262 | }else { 263 | lineAnimationTwo.toValue = -lineLength/4 * scale 264 | } 265 | let lineLayer = lines[i] 266 | lineLayer.add(lineAnimationTwo, forKey: "lineAnimationThree") 267 | } 268 | } 269 | 270 | /** 271 | 线的第三步动画,线由短变长 272 | */ 273 | private func lineAnimationThree() { 274 | //线移动的动画 275 | let lineAnimationFour = CABasicAnimation.init(keyPath: "strokeEnd") 276 | lineAnimationFour.beginTime = CACurrentMediaTime() + duration 277 | lineAnimationFour.duration = duration/4 278 | lineAnimationFour.fillMode = CAMediaTimingFillMode.forwards 279 | lineAnimationFour.isRemovedOnCompletion = false 280 | lineAnimationFour.fromValue = 0 281 | lineAnimationFour.toValue = 1 282 | for i in 0...3 { 283 | if i == 3 { 284 | lineAnimationFour.delegate = self 285 | } 286 | let lineLayer = lines[i] 287 | lineLayer.add(lineAnimationFour, forKey: "lineAnimationFour") 288 | } 289 | } 290 | 291 | //MARK: Private Methods 292 | private func point(_ x:CGFloat , y:CGFloat) -> CGPoint { 293 | return CGPoint(x: x, y: y) 294 | } 295 | 296 | private func angle(_ angle: Double) -> CGFloat { 297 | return CGFloat(angle * (Double.pi/180)) 298 | } 299 | 300 | private func config() { 301 | layoutIfNeeded() 302 | lineLength = max(frame.width, frame.height) 303 | lineWidth = lineLength/6.0 304 | margin = lineLength/4.5 + lineWidth/2 305 | drawLineShapeLayer() 306 | //调整角度 307 | transform = CGAffineTransform.identity.rotated(by: angle(-30)) 308 | } 309 | } 310 | 311 | extension CALayer { 312 | //暂停动画 313 | func pauseAnimation() { 314 | // 将当前时间CACurrentMediaTime转换为layer上的时间, 即将parent time转换为localtime 315 | let pauseTime = convertTime(CACurrentMediaTime(), from: nil) 316 | // 设置layer的timeOffset, 在继续操作也会使用到 317 | timeOffset = pauseTime 318 | // localtime与parenttime的比例为0, 意味着localtime暂停了 319 | speed = 0 320 | } 321 | 322 | //继续动画 323 | func resumeAnimation() { 324 | let pausedTime = timeOffset 325 | speed = 1 326 | timeOffset = 0 327 | beginTime = 0 328 | // 计算暂停时间 329 | let sincePause = convertTime(CACurrentMediaTime(), from: nil) - pausedTime 330 | // local time相对于parent time时间的beginTime 331 | beginTime = sincePause 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/CRRefreshAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CRRefreshAnimator.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:http://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class CRRefreshAnimator 22 | // @abstract 默认的Animator 23 | // @discussion 默认的Animator 24 | // 25 | 26 | import UIKit 27 | 28 | open class CRRefreshAnimator: CRRefreshProtocol { 29 | 30 | open var view: UIView 31 | 32 | open var insets: UIEdgeInsets 33 | 34 | open var trigger: CGFloat = 60.0 35 | 36 | open var execute: CGFloat = 60.0 37 | 38 | open var endDelay: CGFloat = 0 39 | 40 | public var hold: CGFloat = 60 41 | 42 | public init() { 43 | view = UIView() 44 | insets = UIEdgeInsets.zero 45 | } 46 | 47 | open func refreshBegin(view: CRRefreshComponent) {} 48 | 49 | open func refreshWillEnd(view: CRRefreshComponent) {} 50 | 51 | open func refreshEnd(view: CRRefreshComponent, finish: Bool) {} 52 | 53 | open func refresh(view: CRRefreshComponent, progressDidChange progress: CGFloat) {} 54 | 55 | open func refresh(view: CRRefreshComponent, stateDidChange state: CRRefreshState) {} 56 | } 57 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/CRRefreshBundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CRRefreshBundle.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:http://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class CRRefreshBundle 22 | // @abstract 从Bundle中获取数据的类 23 | // @discussion 从Bundle中获取数据的类 24 | // 25 | 26 | import UIKit 27 | 28 | class CRRefreshBundle { 29 | 30 | var crBundle: Bundle 31 | 32 | init(bundle: Bundle) { 33 | crBundle = bundle 34 | } 35 | 36 | @discardableResult 37 | static func bundle(name: String, for aClass: Swift.AnyClass) -> CRRefreshBundle? { 38 | let bundle = Bundle(for: aClass) 39 | if let path = bundle.path(forResource: name, ofType: "bundle") { 40 | if let bundle = Bundle(path: path) { 41 | return CRRefreshBundle(bundle: bundle) 42 | } 43 | } 44 | return nil 45 | } 46 | 47 | func imageFromBundle(_ imageName: String) -> UIImage? { 48 | var imageName = imageName 49 | if UIScreen.main.scale == 2 { 50 | imageName = imageName + "@2x" 51 | }else if UIScreen.main.scale == 3 { 52 | imageName = imageName + "@3x" 53 | } 54 | let bundle = Bundle(path: crBundle.bundlePath + "/images") 55 | if let path = bundle?.path(forResource: imageName, ofType: "png") { 56 | let image = UIImage(contentsOfFile: path) 57 | return image 58 | } 59 | return nil 60 | } 61 | 62 | func localizedString(key: String) -> String { 63 | if let current = Locale.current.languageCode { 64 | var language = "" 65 | switch current { 66 | case "zh": 67 | language = "zh" 68 | default: 69 | language = "en" 70 | } 71 | if let path = crBundle.path(forResource: language, ofType: "lproj") { 72 | if let bundle = Bundle(path: path) { 73 | let value = bundle.localizedString(forKey: key, value: nil, table: nil) 74 | return Bundle.main.localizedString(forKey: key, value: value, table: nil) 75 | } 76 | } 77 | } 78 | return key 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/CRRefreshComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CRRefreshComponent.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:http://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class CRRefreshComponent 22 | // @abstract 刷新控件的基类 23 | // @discussion 刷新控件的基类 24 | // 25 | 26 | import UIKit 27 | 28 | public typealias CRRefreshHandler = (() -> ()) 29 | 30 | public enum CRRefreshState { 31 | /// 普通闲置状态 32 | case idle 33 | /// 松开就可以进行刷新的状态 34 | case pulling 35 | /// 正在刷新中的状态 36 | case refreshing 37 | /// 即将刷新的状态 38 | case willRefresh 39 | /// 所有数据加载完毕,没有更多的数据了 40 | case noMoreData 41 | } 42 | 43 | open class CRRefreshComponent: UIView { 44 | 45 | open weak var scrollView: UIScrollView? 46 | 47 | open var scrollViewInsets: UIEdgeInsets = .zero 48 | 49 | open var handler: CRRefreshHandler? 50 | 51 | open var animator: CRRefreshProtocol! 52 | 53 | open var state: CRRefreshState = .idle { 54 | didSet { 55 | if state != oldValue { 56 | DispatchQueue.main.async { 57 | self.animator.refresh(view: self, stateDidChange: self.state) 58 | } 59 | } 60 | } 61 | } 62 | 63 | fileprivate var isObservingScrollView = false 64 | 65 | fileprivate var isIgnoreObserving = false 66 | 67 | fileprivate(set) var isRefreshing = false 68 | 69 | public override init(frame: CGRect) { 70 | super.init(frame: frame) 71 | autoresizingMask = [.flexibleLeftMargin, .flexibleWidth, .flexibleRightMargin] 72 | } 73 | 74 | public convenience init(animator: CRRefreshProtocol = CRRefreshAnimator(), handler: @escaping CRRefreshHandler) { 75 | self.init(frame: .zero) 76 | self.handler = handler 77 | self.animator = animator 78 | } 79 | 80 | public required init?(coder aDecoder: NSCoder) { 81 | fatalError("init(coder:) has not been implemented") 82 | } 83 | 84 | open override func willMove(toSuperview newSuperview: UIView?) { 85 | super.willMove(toSuperview: newSuperview) 86 | // 旧的父控件移除监听 87 | removeObserver() 88 | if let newSuperview = newSuperview as? UIScrollView { 89 | // 记录UIScrollView最开始的contentInset 90 | scrollViewInsets = newSuperview.contentInset 91 | DispatchQueue.main.async { [weak self, newSuperview] in 92 | guard let weakSelf = self else { return } 93 | weakSelf.addObserver(newSuperview) 94 | } 95 | } 96 | } 97 | 98 | open override func didMoveToSuperview() { 99 | super.didMoveToSuperview() 100 | scrollView = superview as? UIScrollView 101 | let view = animator.view 102 | if view.superview == nil { 103 | let inset = animator.insets 104 | addSubview(view) 105 | view.frame = CGRect(x: inset.left, 106 | y: inset.top, 107 | width: bounds.size.width - inset.left - inset.right, 108 | height: bounds.size.height - inset.top - inset.bottom) 109 | view.autoresizingMask = [ 110 | .flexibleWidth, 111 | .flexibleTopMargin, 112 | .flexibleHeight, 113 | .flexibleBottomMargin 114 | ] 115 | } 116 | } 117 | 118 | //MARK: Public Methods 119 | public final func beginRefreshing() -> Void { 120 | guard isRefreshing == false else { return } 121 | if self.window != nil { 122 | state = .refreshing 123 | start() 124 | }else { 125 | if state != .refreshing { 126 | state = .willRefresh 127 | // 预防view还没显示出来就调用了beginRefreshing 128 | DispatchQueue.main.async { 129 | self.scrollViewInsets = self.scrollView?.contentInset ?? .zero 130 | if self.state == .willRefresh { 131 | self.state = .refreshing 132 | self.start() 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | public final func endRefreshing() -> Void { 140 | guard isRefreshing else { return } 141 | self.stop() 142 | } 143 | 144 | public func ignoreObserver(_ ignore: Bool = false) { 145 | isIgnoreObserving = ignore 146 | } 147 | 148 | public func start() { 149 | isRefreshing = true 150 | } 151 | 152 | public func stop() { 153 | isRefreshing = false 154 | } 155 | 156 | public func sizeChange(change: [NSKeyValueChangeKey : Any]?) {} 157 | 158 | public func offsetChange(change: [NSKeyValueChangeKey : Any]?) {} 159 | } 160 | 161 | 162 | 163 | //MARK: Observer Methods 164 | extension CRRefreshComponent { 165 | 166 | fileprivate static var context = "CRRefreshContext" 167 | fileprivate static let offsetKeyPath = "contentOffset" 168 | fileprivate static let contentSizeKeyPath = "contentSize" 169 | public static let animationDuration = 0.25 170 | 171 | fileprivate func removeObserver() { 172 | if let scrollView = superview as? UIScrollView, isObservingScrollView { 173 | scrollView.removeObserver(self, forKeyPath: CRRefreshComponent.offsetKeyPath, context: &CRRefreshComponent.context) 174 | scrollView.removeObserver(self, forKeyPath: CRRefreshComponent.contentSizeKeyPath, context: &CRRefreshComponent.context) 175 | isObservingScrollView = false 176 | } 177 | } 178 | 179 | fileprivate func addObserver(_ view: UIView?) { 180 | if let scrollView = view as? UIScrollView, !isObservingScrollView { 181 | scrollView.addObserver(self, forKeyPath: CRRefreshComponent.offsetKeyPath, options: [.initial, .new], context: &CRRefreshComponent.context) 182 | scrollView.addObserver(self, forKeyPath: CRRefreshComponent.contentSizeKeyPath, options: [.initial, .new], context: &CRRefreshComponent.context) 183 | isObservingScrollView = true 184 | } 185 | } 186 | 187 | open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 188 | if context == &CRRefreshComponent.context { 189 | guard isUserInteractionEnabled == true && isHidden == false else { 190 | return 191 | } 192 | if keyPath == CRRefreshComponent.contentSizeKeyPath { 193 | if isIgnoreObserving == false { 194 | sizeChange(change: change) 195 | } 196 | } else if keyPath == CRRefreshComponent.offsetKeyPath { 197 | if isIgnoreObserving == false { 198 | offsetChange(change: change) 199 | } 200 | } 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/CRRefreshExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CRRefreshExtension.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:http://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class CRRefreshExtension 22 | // @abstract CRRefreshExtension 23 | // @discussion CRRefreshExtension 24 | // 25 | 26 | import UIKit 27 | 28 | private var kCRRefreshHeaderKey = "kCRRefreshHeaderKey" 29 | private var kCRRefreshFooterKey = "kCRRefreshFooterKey" 30 | 31 | public typealias CRRefreshView = UIScrollView 32 | 33 | extension CRRefreshView { 34 | public var cr: CRRefreshDSL { 35 | return CRRefreshDSL(scroll: self) 36 | } 37 | } 38 | 39 | public struct CRRefreshDSL: CRRefreshViewProtocol { 40 | 41 | public var scroll: CRRefreshView 42 | 43 | internal init(scroll: CRRefreshView) { 44 | self.scroll = scroll 45 | } 46 | /// 添加上拉刷新控件 47 | @discardableResult 48 | public func addHeadRefresh(animator: CRRefreshProtocol = NormalHeaderAnimator(), handler: @escaping CRRefreshHandler) -> CRRefreshHeaderView { 49 | return CRRefreshMake.addHeadRefreshTo(refresh: scroll, animator: animator, handler: handler) 50 | } 51 | 52 | public func beginHeaderRefresh() { 53 | header?.beginRefreshing() 54 | } 55 | 56 | public func endHeaderRefresh() { 57 | header?.endRefreshing() 58 | } 59 | 60 | public func removeHeader() { 61 | var headRefresh = CRRefreshMake(scroll: scroll) 62 | headRefresh.removeHeader() 63 | } 64 | 65 | /// 添加下拉加载控件 66 | @discardableResult 67 | public func addFootRefresh(animator: CRRefreshProtocol = NormalFooterAnimator(), handler: @escaping CRRefreshHandler) -> CRRefreshFooterView { 68 | return CRRefreshMake.addFootRefreshTo(refresh: scroll, animator: animator, handler: handler) 69 | } 70 | 71 | public func noticeNoMoreData() { 72 | footer?.endRefreshing() 73 | footer?.noticeNoMoreData() 74 | } 75 | 76 | public func resetNoMore() { 77 | footer?.resetNoMoreData() 78 | } 79 | 80 | public func endLoadingMore() { 81 | footer?.endRefreshing() 82 | } 83 | 84 | public func removeFooter() { 85 | var footRefresh = CRRefreshMake(scroll: scroll) 86 | footRefresh.removeFooter() 87 | } 88 | } 89 | 90 | 91 | public struct CRRefreshMake: CRRefreshViewProtocol { 92 | 93 | public var scroll: CRRefreshView 94 | 95 | internal init(scroll: CRRefreshView) { 96 | self.scroll = scroll 97 | } 98 | 99 | /// 添加上拉刷新 100 | @discardableResult 101 | internal static func addHeadRefreshTo(refresh: CRRefreshView, animator: CRRefreshProtocol = NormalHeaderAnimator(), handler: @escaping CRRefreshHandler) -> CRRefreshHeaderView { 102 | var make = CRRefreshMake(scroll: refresh) 103 | make.removeHeader() 104 | let header = CRRefreshHeaderView(animator: animator, handler: handler) 105 | let headerH = header.animator.execute 106 | header.frame = .init(x: 0, y: -headerH, width: refresh.bounds.size.width, height: headerH) 107 | refresh.addSubview(header) 108 | make.header = header 109 | return header 110 | } 111 | 112 | public mutating func removeHeader() { 113 | header?.endRefreshing() 114 | header?.removeFromSuperview() 115 | header = nil 116 | } 117 | 118 | /// 添加下拉加载 119 | @discardableResult 120 | internal static func addFootRefreshTo(refresh: CRRefreshView, animator: CRRefreshProtocol = NormalFooterAnimator(), handler: @escaping CRRefreshHandler) -> CRRefreshFooterView { 121 | var make = CRRefreshMake(scroll: refresh) 122 | make.removeFooter() 123 | let footer = CRRefreshFooterView(animator: animator, handler: handler) 124 | let footerH = footer.animator.execute 125 | footer.frame = .init(x: 0, y: refresh.contentSize.height + refresh.contentInset.bottom, width: refresh.bounds.size.width, height: footerH) 126 | refresh.addSubview(footer) 127 | make.footer = footer 128 | return footer 129 | } 130 | 131 | public mutating func removeFooter() { 132 | footer?.endRefreshing() 133 | footer?.removeFromSuperview() 134 | footer = nil 135 | } 136 | } 137 | 138 | public protocol CRRefreshViewProtocol { 139 | var scroll: CRRefreshView {set get} 140 | /// 头部控件 141 | var header: CRRefreshHeaderView? {set get} 142 | /// 头部控件 143 | var footer: CRRefreshFooterView? {set get} 144 | } 145 | 146 | extension CRRefreshViewProtocol { 147 | 148 | public var header: CRRefreshHeaderView? { 149 | get { 150 | return (objc_getAssociatedObject(scroll, &kCRRefreshHeaderKey) as? CRRefreshHeaderView) 151 | } 152 | set { 153 | objc_setAssociatedObject(scroll, &kCRRefreshHeaderKey, newValue, .OBJC_ASSOCIATION_RETAIN) 154 | } 155 | } 156 | 157 | public var footer: CRRefreshFooterView? { 158 | get { 159 | return (objc_getAssociatedObject(scroll, &kCRRefreshFooterKey) as? CRRefreshFooterView) 160 | } 161 | set { 162 | objc_setAssociatedObject(scroll, &kCRRefreshFooterKey, newValue, .OBJC_ASSOCIATION_RETAIN) 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/CRRefreshFooterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CRRefreshFooterView.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:http://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class CRRefreshFooterView 22 | // @abstract 刷新的尾部控件 23 | // @discussion 刷新的尾部控件 24 | // 25 | 26 | import UIKit 27 | 28 | open class CRRefreshFooterView: CRRefreshComponent { 29 | 30 | open var noMoreData = false { 31 | didSet { 32 | if noMoreData != oldValue { 33 | state = .idle 34 | } 35 | } 36 | } 37 | 38 | open override var isHidden: Bool { 39 | didSet { 40 | if isHidden == true { 41 | scrollView?.contentInset.bottom = scrollViewInsets.bottom 42 | var rect = self.frame 43 | rect.origin.y = scrollView?.contentSize.height ?? 0.0 44 | self.frame = rect 45 | } else { 46 | scrollView?.contentInset.bottom = scrollViewInsets.bottom + animator.execute 47 | var rect = self.frame 48 | rect.origin.y = scrollView?.contentSize.height ?? 0.0 49 | self.frame = rect 50 | } 51 | } 52 | } 53 | 54 | public convenience init(animator: CRRefreshProtocol = NormalFooterAnimator(), handler: @escaping CRRefreshHandler) { 55 | self.init(frame: .zero) 56 | self.handler = handler 57 | self.animator = animator 58 | } 59 | 60 | open override func didMoveToSuperview() { 61 | super.didMoveToSuperview() 62 | DispatchQueue.main.async { [weak self] in 63 | guard let weakSelf = self else { return } 64 | weakSelf.scrollViewInsets = weakSelf.scrollView?.contentInset ?? UIEdgeInsets.zero 65 | weakSelf.scrollView?.contentInset.bottom = weakSelf.scrollViewInsets.bottom + weakSelf.bounds.size.height 66 | var rect = weakSelf.frame 67 | rect.origin.y = weakSelf.scrollView?.contentSize.height ?? 0.0 68 | weakSelf.frame = rect 69 | } 70 | } 71 | 72 | open override func start() { 73 | guard let scrollView = scrollView else { return } 74 | super.start() 75 | animator.refreshBegin(view: self) 76 | let x = scrollView.contentOffset.x 77 | let y = max(0.0, scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.bottom) 78 | UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveLinear, animations: { 79 | scrollView.contentOffset = .init(x: x, y: y) 80 | }, completion: { (animated) in 81 | self.handler?() 82 | }) 83 | } 84 | 85 | open override func stop() { 86 | guard let scrollView = scrollView else { return } 87 | animator.refreshEnd(view: self, finish: false) 88 | UIView.animate(withDuration: CRRefreshComponent.animationDuration, delay: 0, options: .curveLinear, animations: { 89 | }, completion: { (finished) in 90 | if self.noMoreData == false { 91 | self.state = .idle 92 | } 93 | super.stop() 94 | self.animator.refreshEnd(view: self, finish: true) 95 | }) 96 | if scrollView.isDecelerating { 97 | var contentOffset = scrollView.contentOffset 98 | contentOffset.y = min(contentOffset.y, scrollView.contentSize.height - scrollView.frame.size.height) 99 | if contentOffset.y < 0.0 { 100 | contentOffset.y = 0.0 101 | UIView.animate(withDuration: 0.1, animations: { 102 | scrollView.setContentOffset(contentOffset, animated: false) 103 | }) 104 | } else { 105 | scrollView.setContentOffset(contentOffset, animated: false) 106 | } 107 | } 108 | } 109 | 110 | open override func sizeChange(change: [NSKeyValueChangeKey : Any]?) { 111 | guard let scrollView = scrollView else { return } 112 | super.sizeChange(change: change) 113 | let targetY = scrollView.contentSize.height + scrollViewInsets.bottom 114 | if self.frame.origin.y != targetY { 115 | var rect = self.frame 116 | rect.origin.y = targetY 117 | self.frame = rect 118 | } 119 | } 120 | 121 | open override func offsetChange(change: [NSKeyValueChangeKey : Any]?) { 122 | guard let scrollView = scrollView else { return } 123 | super.offsetChange(change: change) 124 | guard isRefreshing == false && noMoreData == false && isHidden == false else { 125 | // 正在loading more或者内容为空时不相应变化 126 | return 127 | } 128 | 129 | if scrollView.contentSize.height <= 0.0 || scrollView.contentOffset.y + scrollView.contentInset.top <= 0.0 { 130 | alpha = 0.0 131 | return 132 | } else { 133 | alpha = 1.0 134 | } 135 | 136 | if scrollView.contentSize.height + scrollView.contentInset.top > scrollView.bounds.size.height { 137 | // 内容超过一个屏幕 计算公式,判断是不是在拖在到了底部 138 | if scrollView.contentSize.height - scrollView.contentOffset.y + scrollView.contentInset.bottom <= scrollView.bounds.size.height { 139 | state = .refreshing 140 | beginRefreshing() 141 | } 142 | } else { 143 | //内容没有超过一个屏幕,这时拖拽高度大于1/2footer的高度就表示请求上拉 144 | if scrollView.contentOffset.y + scrollView.contentInset.top >= animator.trigger / 2.0 { 145 | state = .refreshing 146 | beginRefreshing() 147 | } 148 | } 149 | } 150 | 151 | open func noticeNoMoreData() { 152 | noMoreData = true 153 | self.state = .noMoreData 154 | } 155 | 156 | open func resetNoMoreData() { 157 | noMoreData = false 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/CRRefreshHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CRRefreshHeaderView.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:http://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class CRRefreshHeaderView 22 | // @abstract 刷新的头部控件 23 | // @discussion 刷新的头部控件 24 | // 25 | 26 | import UIKit 27 | 28 | open class CRRefreshHeaderView: CRRefreshComponent { 29 | 30 | /// 记录之前的offsetY 31 | fileprivate var previousOffsetY: CGFloat = 0.0 32 | fileprivate var scrollViewBounces: Bool = true 33 | /// 记录结束刷新时需要调整的contentInsetY 34 | fileprivate var insetTDelta: CGFloat = 0.0 35 | /// 记录悬停时需要调整的contentInsetY 36 | fileprivate var holdInsetTDelta: CGFloat = 0.0 37 | /// 是否还在结束中 38 | private var isEnding: Bool = false 39 | 40 | public convenience init(animator: CRRefreshProtocol = NormalHeaderAnimator(), handler: @escaping CRRefreshHandler) { 41 | self.init(frame: .zero) 42 | self.handler = handler 43 | self.animator = animator 44 | } 45 | 46 | open override func didMoveToSuperview() { 47 | super.didMoveToSuperview() 48 | DispatchQueue.main.async { [weak self] in 49 | guard let weakSelf = self else { return } 50 | weakSelf.scrollViewBounces = weakSelf.scrollView?.bounces ?? true 51 | 52 | } 53 | } 54 | 55 | open override func start() { 56 | guard let scrollView = scrollView else { return } 57 | // 动画的时候先忽略监听 58 | ignoreObserver(true) 59 | scrollView.bounces = false 60 | super.start() 61 | // 开始动画 62 | animator.refreshBegin(view: self) 63 | // 调整scrollView的contentInset 64 | var insets = scrollView.contentInset 65 | scrollViewInsets.top = insets.top 66 | insets.top += animator.execute 67 | insetTDelta = -animator.execute 68 | holdInsetTDelta = -(animator.execute - animator.hold) 69 | var point = scrollView.contentOffset; 70 | point.y = -insets.top 71 | UIView.animate(withDuration: CRRefreshComponent.animationDuration, animations: { 72 | 73 | scrollView.contentOffset.y = self.previousOffsetY 74 | scrollView.contentInset = insets 75 | // scrollView.contentOffset.y = -insets.top 76 | scrollView.setContentOffset(point, animated: false); 77 | 78 | 79 | }) { (finished) in 80 | DispatchQueue.main.async { 81 | self.handler?() 82 | self.ignoreObserver(false) 83 | scrollView.bounces = self.scrollViewBounces 84 | } 85 | } 86 | } 87 | 88 | open override func stop() { 89 | guard let scrollView = scrollView else { return } 90 | // 动画的时候先忽略监听 91 | ignoreObserver(true) 92 | animator.refreshWillEnd(view: self) 93 | if self.animator.hold != 0 { 94 | UIView.animate(withDuration: CRRefreshComponent.animationDuration) { 95 | scrollView.contentInset.top += self.holdInsetTDelta 96 | } 97 | } 98 | func beginStop() { 99 | guard isEnding == false, isRefreshing else { 100 | return 101 | } 102 | isEnding = true 103 | // 结束动画 104 | animator.refreshEnd(view: self, finish: false) 105 | // 调整scrollView的contentInset 106 | UIView.animate(withDuration: CRRefreshComponent.animationDuration, animations: { 107 | scrollView.contentInset.top += self.insetTDelta - self.holdInsetTDelta 108 | }) { (finished) in 109 | DispatchQueue.main.async { 110 | self.state = .idle 111 | super.stop() 112 | self.animator.refreshEnd(view: self, finish: true) 113 | self.ignoreObserver(false) 114 | self.isEnding = false 115 | } 116 | } 117 | } 118 | if animator.endDelay > 0 { 119 | if self.isEnding == false { 120 | let delay = DispatchTimeInterval.milliseconds(Int(animator.endDelay * 1000)) 121 | DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: { 122 | beginStop() 123 | }) 124 | } 125 | } else { 126 | beginStop() 127 | } 128 | } 129 | 130 | open override func offsetChange(change: [NSKeyValueChangeKey : Any]?) { 131 | guard let scrollView = scrollView else { return } 132 | super.offsetChange(change: change) 133 | // sectionheader停留的解决方案 134 | guard isRefreshing == false else { 135 | if self.window == nil {return} 136 | let top = scrollViewInsets.top 137 | let offsetY = scrollView.contentOffset.y 138 | let height = frame.size.height 139 | var scrollingTop = (-offsetY > top) ? -offsetY : top 140 | scrollingTop = (scrollingTop > height + top) ? (height + top) : scrollingTop 141 | scrollView.contentInset.top = scrollingTop 142 | insetTDelta = scrollViewInsets.top - scrollingTop 143 | return 144 | } 145 | 146 | // 算出Progress 147 | var isRecordingProgress = false 148 | defer { 149 | if isRecordingProgress == true { 150 | let percent = -(previousOffsetY + scrollViewInsets.top) / animator.trigger 151 | animator.refresh(view: self, progressDidChange: percent) 152 | } 153 | } 154 | 155 | let offsets = previousOffsetY + scrollViewInsets.top 156 | if offsets < -animator.trigger { 157 | if isRefreshing == false { 158 | if scrollView.isDragging == false, state == .pulling { 159 | beginRefreshing() 160 | state = .refreshing 161 | } else { 162 | if scrollView.isDragging { 163 | state = .pulling 164 | isRecordingProgress = true 165 | } 166 | } 167 | } 168 | } else if offsets < 0 { 169 | if isRefreshing == false { 170 | state = .idle 171 | isRecordingProgress = true 172 | } 173 | } 174 | previousOffsetY = scrollView.contentOffset.y 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/CRRefreshProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CRRefreshProtocol.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:http://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class CRRefreshProtocol 22 | // @abstract 自定义animator需要实现的接口 23 | // @discussion 自定义animator需要实现的接口 24 | // 25 | 26 | import UIKit 27 | 28 | public protocol CRRefreshProtocol { 29 | /// 自定义的view 30 | var view: UIView {get} 31 | 32 | /// view的insets 33 | var insets: UIEdgeInsets {set get} 34 | 35 | /// 触发刷新的高度 36 | var trigger: CGFloat {set get} 37 | 38 | /// 动画执行时的高度 39 | var execute: CGFloat {set get} 40 | 41 | /// 动画结束时延迟的时间,单位秒 42 | var endDelay: CGFloat {set get} 43 | 44 | /// 延迟时悬停的高度 45 | var hold: CGFloat {set get} 46 | 47 | /// 开始刷新 48 | mutating func refreshBegin(view: CRRefreshComponent) 49 | 50 | /// 将要开始刷新 51 | mutating func refreshWillEnd(view: CRRefreshComponent) 52 | 53 | /// 结束刷新 54 | mutating func refreshEnd(view: CRRefreshComponent, finish: Bool) 55 | 56 | /// 刷新进度的变化 57 | mutating func refresh(view: CRRefreshComponent, progressDidChange progress: CGFloat) 58 | 59 | /// 刷新状态的变化 60 | mutating func refresh(view: CRRefreshComponent, stateDidChange state: CRRefreshState) 61 | } 62 | -------------------------------------------------------------------------------- /CRRefresh/CRRefresh/UIColorExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColorExtension.swift 3 | // HEXColor 4 | // 5 | // Created by R0CKSTAR on 6/13/14. 6 | // Copyright (c) 2014 P.D.Q. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | public convenience init(rgb: (r: CGFloat, g: CGFloat, b: CGFloat)) { 13 | self.init(red: rgb.r/255, green: rgb.g/255, blue: rgb.b/255, alpha: 1.0) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /CRRefresh/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /CRRefresh1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/CRRefresh1.gif -------------------------------------------------------------------------------- /CRRefresh2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/CRRefresh2.gif -------------------------------------------------------------------------------- /CRRefresh3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/CRRefresh3.gif -------------------------------------------------------------------------------- /CRRefresh4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/CRRefresh4.gif -------------------------------------------------------------------------------- /Demo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo 4 | // 5 | // Created by 王崇磊 on 2017/3/13. 6 | // Copyright © 2017年 王崇磊. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | window = UIWindow.init(frame: UIScreen.main.bounds) 20 | window?.makeKeyAndVisible() 21 | let VC = ViewController() 22 | let NAV = NavigationViewController(rootViewController: VC) 23 | window?.rootViewController = NAV 24 | 25 | UITableView.appearance().estimatedSectionHeaderHeight = 0 26 | UITableView.appearance().estimatedSectionFooterHeight = 0 27 | 28 | return true 29 | } 30 | 31 | func applicationWillResignActive(_ application: UIApplication) { 32 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 33 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 34 | } 35 | 36 | func applicationDidEnterBackground(_ application: UIApplication) { 37 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 38 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 39 | } 40 | 41 | func applicationWillEnterForeground(_ application: UIApplication) { 42 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 43 | } 44 | 45 | func applicationDidBecomeActive(_ application: UIApplication) { 46 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 47 | } 48 | 49 | func applicationWillTerminate(_ application: UIApplication) { 50 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 51 | } 52 | 53 | 54 | } 55 | 56 | -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "57x57", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-57x57@1x.png", 49 | "scale" : "1x" 50 | }, 51 | { 52 | "size" : "57x57", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-57x57@2x.png", 55 | "scale" : "2x" 56 | }, 57 | { 58 | "size" : "60x60", 59 | "idiom" : "iphone", 60 | "filename" : "Icon-App-60x60@2x.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "60x60", 65 | "idiom" : "iphone", 66 | "filename" : "Icon-App-60x60@3x.png", 67 | "scale" : "3x" 68 | }, 69 | { 70 | "size" : "20x20", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-ipad-App-20x20@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "20x20", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-ipad-App-20x20@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "29x29", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-ipad-App-29x29@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "29x29", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-ipad-App-29x29@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "40x40", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-ipad-App-40x40@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "40x40", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-ipad-App-40x40@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "50x50", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-ipad-Small-50x50@1x.png", 109 | "scale" : "1x" 110 | }, 111 | { 112 | "size" : "50x50", 113 | "idiom" : "ipad", 114 | "filename" : "Icon-ipad-Small-50x50@2x.png", 115 | "scale" : "2x" 116 | }, 117 | { 118 | "size" : "72x72", 119 | "idiom" : "ipad", 120 | "filename" : "Icon-ipad-App-72x72@1x.png", 121 | "scale" : "1x" 122 | }, 123 | { 124 | "size" : "72x72", 125 | "idiom" : "ipad", 126 | "filename" : "Icon-ipad-App-72x72@2x.png", 127 | "scale" : "2x" 128 | }, 129 | { 130 | "size" : "76x76", 131 | "idiom" : "ipad", 132 | "filename" : "Icon-ipad-App-76x76@1x.png", 133 | "scale" : "1x" 134 | }, 135 | { 136 | "size" : "76x76", 137 | "idiom" : "ipad", 138 | "filename" : "Icon-ipad-App-76x76@2x.png", 139 | "scale" : "2x" 140 | }, 141 | { 142 | "size" : "83.5x83.5", 143 | "idiom" : "ipad", 144 | "filename" : "Icon-ipad-App-83.5x83.5@2x.png", 145 | "scale" : "2x" 146 | }, 147 | { 148 | "size" : "1024x1024", 149 | "idiom" : "ios-marketing", 150 | "filename" : "Icon-ios-App-1024x1024@1x.png", 151 | "scale" : "1x" 152 | } 153 | ], 154 | "info" : { 155 | "version" : 1, 156 | "author" : "xcode" 157 | } 158 | } -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-ios-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-ios-App-1024x1024@1x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-20x20@1x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-20x20@2x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-29x29@1x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-29x29@2x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-40x40@1x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-40x40@2x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-72x72@1x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-72x72@2x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-76x76@1x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-76x76@2x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-Small-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-Small-50x50@1x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-Small-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/AppIcon.appiconset/Icon-ipad-Small-50x50@2x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/Image_1.imageset/3(1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/Image_1.imageset/3(1).png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/Image_1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "3(1).png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Demo/Assets.xcassets/Image_2.imageset/2(1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/Image_2.imageset/2(1).png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/Image_2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "2(1).png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Demo/Assets.xcassets/Image_3.imageset/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/Image_3.imageset/1.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/Image_3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "1.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Demo/Assets.xcassets/nav_back.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "icon-返回@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "icon-返回@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Demo/Assets.xcassets/nav_back.imageset/icon-返回@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/nav_back.imageset/icon-返回@2x.png -------------------------------------------------------------------------------- /Demo/Assets.xcassets/nav_back.imageset/icon-返回@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRAnimation/CRRefresh/336441963819645dc7427352664f114f5a4cc864/Demo/Assets.xcassets/nav_back.imageset/icon-返回@3x.png -------------------------------------------------------------------------------- /Demo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Demo/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewController.swift 3 | // HotLine 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:https://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class BaseViewController 22 | // @abstract 所有VC的基类 23 | // @discussion 所有VC的基类 24 | // 25 | 26 | import UIKit 27 | 28 | let APP_NAV_BG_COLOR = UIColor(rgb: (140, 141, 178)) 29 | let APP_NAV_LINE_COLOR = UIColor.clear 30 | let APP_BACK_COLOR = UIColor.white 31 | 32 | 33 | class BaseViewController: UIViewController, UIGestureRecognizerDelegate { 34 | 35 | var isFirstDidAppear: Bool = false 36 | var isViewAppear: Bool = false 37 | 38 | override var preferredStatusBarStyle: UIStatusBarStyle { 39 | return .lightContent 40 | } 41 | 42 | var navTitle: String?{ 43 | didSet{ 44 | let titleLabel = UILabel.init(frame: CGRect.zero) 45 | titleLabel.font = UIFont.appMediumFontOfSize(18) 46 | titleLabel.textColor = UIColor.white 47 | titleLabel.text = navTitle 48 | titleLabel.sizeToFit() 49 | self.navigationItem.titleView = titleLabel 50 | } 51 | } 52 | 53 | //MARK: Public Methods 54 | /** 55 | 配置NavBar 56 | */ 57 | func configNavBar() { 58 | setNavImage(bgColor: APP_NAV_BG_COLOR, shadowColor: APP_NAV_LINE_COLOR) 59 | } 60 | 61 | /** 62 | View的相关配置 63 | */ 64 | func configView() { 65 | view.backgroundColor = APP_BACK_COLOR 66 | } 67 | 68 | //左边nav的点击事件 69 | @objc func leftButtonAction(_ button: UIButton) { 70 | _ = navigationController?.popViewController(animated: true) 71 | } 72 | /** 73 | 添加默认返回按钮 74 | */ 75 | @discardableResult 76 | func addNavDefaultBackButton() -> UIButton { 77 | let leftBt = UIButton() 78 | let btImage = UIImage.init(named: "nav_back") 79 | leftBt.setImage(btImage, for: UIControl.State()) 80 | leftBt.frame.size = (btImage?.size)! 81 | leftBt.addTarget(self, action: #selector(leftButtonAction(_:)), for: .touchUpInside) 82 | self.navigationItem.leftBarButtonItem = UIBarButtonItem.init(customView: leftBt) 83 | return leftBt 84 | } 85 | 86 | /** 87 | 设置navBar的颜色 88 | 89 | - parameter bgColor: 背景色 90 | - parameter shadowColor: 阴影色 91 | 92 | - returns: self 93 | */ 94 | func setNavImage(bgColor: UIColor, shadowColor: UIColor) { 95 | let width = UIScreen.main.bounds.width 96 | let scale = UIScreen.main.scale 97 | 98 | UIGraphicsBeginImageContextWithOptions(CGSize.init(width: width, height: 64), false, scale) 99 | var context = UIGraphicsGetCurrentContext()! 100 | CGContext.setFillColor(context)(bgColor.cgColor) 101 | CGContext.addRect(context)(CGRect.init(x: 0, y: 0, width: width, height: 64)) 102 | CGContext.drawPath(context)(using: .fill) 103 | let bgImage = UIGraphicsGetImageFromCurrentImageContext() 104 | 105 | UIGraphicsBeginImageContextWithOptions(CGSize.init(width: width, height: 1), false, scale) 106 | context = UIGraphicsGetCurrentContext()! 107 | CGContext.setLineWidth(context)(1) 108 | CGContext.setStrokeColor(context)(shadowColor.cgColor) 109 | CGContext.move(context)(to: CGPoint.zero) 110 | CGContext.addLine(context)(to: CGPoint.init(x: width, y: 0)) 111 | CGContext.drawPath(context)(using: .stroke) 112 | let shadowImage = UIGraphicsGetImageFromCurrentImageContext() 113 | 114 | self.navigationController?.navigationBar.setBackgroundImage(bgImage, for: .default) 115 | self.navigationController?.navigationBar.shadowImage = shadowImage 116 | } 117 | 118 | //MARK: Override 119 | override func viewDidLoad() { 120 | super.viewDidLoad() 121 | // Do any additional setup after loading the view. 122 | configNavBar() 123 | configView() 124 | } 125 | 126 | override func viewDidAppear(_ animated: Bool) { 127 | super.viewWillAppear(animated) 128 | if self.isFirstDidAppear == false { 129 | self.isFirstDidAppear = true 130 | } 131 | self.isViewAppear = true 132 | } 133 | 134 | override func viewWillAppear(_ animated: Bool) { 135 | super.viewWillAppear(animated) 136 | setNavImage(bgColor: APP_NAV_BG_COLOR, shadowColor: APP_NAV_LINE_COLOR) 137 | navigationController?.navigationBar.isTranslucent = false 138 | } 139 | 140 | override func viewDidDisappear(_ animated: Bool) { 141 | super.viewDidDisappear(animated) 142 | self.isViewAppear = false 143 | } 144 | 145 | override func didReceiveMemoryWarning() { 146 | super.didReceiveMemoryWarning() 147 | // Dispose of any resources that can be recreated. 148 | } 149 | 150 | } 151 | 152 | //MARK: 字体 153 | let appFontLightName = "PingFangSC-Light" 154 | let appFontRegularName = "PingFangSC-Regular" 155 | let appFontMediumName = "PingFangSC-Medium" 156 | extension UIFont { 157 | /** 158 | 更具系统不同返回light字体 159 | */ 160 | class func appLightFontOfSize(_ fontSize:CGFloat) -> UIFont { 161 | if getSystemVersion() >= 9.0 { 162 | return UIFont.init(name: appFontLightName, size: fontSize) ?? UIFont.systemFont(ofSize: fontSize) 163 | }else { 164 | return UIFont.systemFont(ofSize: fontSize) 165 | } 166 | } 167 | /** 168 | 更具系统不同返回Regular字体 169 | */ 170 | class func appRegularFontOfSize(_ fontSize:CGFloat) -> UIFont { 171 | if getSystemVersion() >= 9.0 { 172 | return UIFont.init(name: appFontRegularName, size: fontSize) ?? UIFont.systemFont(ofSize: fontSize) 173 | }else { 174 | return UIFont.systemFont(ofSize: fontSize) 175 | } 176 | } 177 | /** 178 | 更具系统不同返回Medium字体 179 | */ 180 | class func appMediumFontOfSize(_ fontSize:CGFloat) -> UIFont { 181 | if getSystemVersion() >= 9.0 { 182 | return UIFont.init(name: appFontMediumName, size: fontSize) ?? UIFont.systemFont(ofSize: fontSize) 183 | }else { 184 | return UIFont.boldSystemFont(ofSize: fontSize) 185 | } 186 | } 187 | } 188 | 189 | /** 190 | 返回系统版本 191 | */ 192 | func getSystemVersion() -> Double { 193 | let version = UIDevice.current.systemVersion 194 | var systemVersion = "" 195 | var itemIndex = 0 196 | for item in version.components(separatedBy: ".") { 197 | if itemIndex == 0 { 198 | systemVersion = systemVersion + item + "." 199 | }else { 200 | systemVersion = systemVersion + item 201 | } 202 | itemIndex += 1 203 | } 204 | return Double(systemVersion) ?? 0 205 | } 206 | 207 | public extension UIColor { 208 | public convenience init(rgb: (r: CGFloat, g: CGFloat, b: CGFloat)) { 209 | self.init(red: rgb.r/255, green: rgb.g/255, blue: rgb.b/255, alpha: 1.0) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /Demo/Cells/NormalCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NormalCell.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:http://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class NormalCell 22 | // @abstract 首页普通的cell 23 | // @discussion 首页普通的cell 24 | 25 | import UIKit 26 | 27 | class NormalCell: UITableViewCell { 28 | 29 | @IBOutlet weak var iconView: UIImageView! 30 | @IBOutlet weak var titleLabel: UILabel! 31 | @IBOutlet weak var subTitleLabel: UILabel! 32 | 33 | //MARK: Public Methods 34 | func config(_ model: Refresh.Model) { 35 | iconView.image = model.icon 36 | titleLabel.text = model.title 37 | subTitleLabel.text = model.subTitle 38 | } 39 | 40 | //MARK: Override 41 | override func awakeFromNib() { 42 | super.awakeFromNib() 43 | // Initialization code 44 | iconView.layer.cornerRadius = 24.5 45 | } 46 | 47 | override func setSelected(_ selected: Bool, animated: Bool) { 48 | super.setSelected(selected, animated: animated) 49 | 50 | // Configure the view for the selected state 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Demo/Cells/NormalCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /Demo/Cells/RefreshCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RefreshCell.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:http://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class RefreshCell 22 | // @abstract 刷新的cell 23 | // @discussion 刷新的cell 24 | 25 | import UIKit 26 | 27 | class RefreshCell: UITableViewCell { 28 | 29 | @IBOutlet weak var oneRight: NSLayoutConstraint! 30 | @IBOutlet weak var twoRight: NSLayoutConstraint! 31 | 32 | //MARK: Public Methods 33 | override func layoutSubviews() { 34 | super.layoutSubviews() 35 | oneRight.constant = CGFloat(arc4random()%100) + 40 36 | twoRight.constant = CGFloat(arc4random()%100) + 40 37 | } 38 | 39 | //MARK: Override 40 | override func awakeFromNib() { 41 | super.awakeFromNib() 42 | // Initialization code 43 | } 44 | 45 | override func setSelected(_ selected: Bool, animated: Bool) { 46 | super.setSelected(selected, animated: animated) 47 | 48 | // Configure the view for the selected state 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Demo/Cells/RefreshCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /Demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | CRRefresh 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Demo/NavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationViewController.swift 3 | // HotLine 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:https://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class NavigationViewController 22 | // @abstract 所有nav的基类 23 | // @discussion 所有nav的基类 24 | // 25 | 26 | import UIKit 27 | 28 | class NavigationViewController: UINavigationController, 29 | UINavigationControllerDelegate { 30 | 31 | override var preferredStatusBarStyle: UIStatusBarStyle { 32 | return .lightContent 33 | } 34 | 35 | //MARK: Public Methods 36 | 37 | 38 | //MARK: Override 39 | override func viewDidLoad() { 40 | super.viewDidLoad() 41 | // Do any additional setup after loading the view. 42 | navigationBar.isTranslucent = false 43 | delegate = self 44 | } 45 | 46 | override func didReceiveMemoryWarning() { 47 | super.didReceiveMemoryWarning() 48 | // Dispose of any resources that can be recreated. 49 | } 50 | 51 | override func pushViewController(_ viewController: UIViewController, animated: Bool) { 52 | if self.responds(to: #selector(getter: UINavigationController.interactivePopGestureRecognizer)) { 53 | self.interactivePopGestureRecognizer?.isEnabled = false 54 | } 55 | super.pushViewController(viewController, animated: animated) 56 | } 57 | 58 | //MARK: UINavigationControllerDelegate 59 | func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { 60 | if navigationController.responds(to: #selector(getter: UINavigationController.interactivePopGestureRecognizer)) { 61 | navigationController.interactivePopGestureRecognizer?.isEnabled = true 62 | } 63 | if navigationController.viewControllers.count == 1 { 64 | navigationController.interactivePopGestureRecognizer?.isEnabled = false 65 | navigationController.interactivePopGestureRecognizer?.delegate = nil 66 | } 67 | } 68 | 69 | 70 | //MARK: Initial Methods 71 | convenience init(rootViewController: UIViewController, navBarColor: UIColor) { 72 | self.init(rootViewController: rootViewController) 73 | setNavImage(bgColor: navBarColor, shadowColor: UIColor.clear) 74 | } 75 | 76 | //MARK: Setter Getter Methods 77 | 78 | 79 | //MARK: Privater Methods 80 | /** 81 | 设置navBar的颜色 82 | 83 | - parameter bgColor: 背景色 84 | - parameter shadowColor: 阴影色 85 | 86 | - returns: self 87 | */ 88 | func setNavImage(bgColor: UIColor, shadowColor: UIColor) { 89 | let width = UIScreen.main.bounds.width 90 | let scale = UIScreen.main.scale 91 | 92 | UIGraphicsBeginImageContextWithOptions(CGSize.init(width: width, height: 64), false, scale) 93 | var context = UIGraphicsGetCurrentContext()! 94 | CGContext.setFillColor(context)(bgColor.cgColor) 95 | CGContext.addRect(context)(CGRect.init(x: 0, y: 0, width: width, height: 64)) 96 | CGContext.drawPath(context)(using: .fill) 97 | let bgImage = UIGraphicsGetImageFromCurrentImageContext() 98 | 99 | UIGraphicsBeginImageContextWithOptions(CGSize.init(width: width, height: 1), false, scale) 100 | context = UIGraphicsGetCurrentContext()! 101 | CGContext.setLineWidth(context)(1) 102 | CGContext.setStrokeColor(context)(shadowColor.cgColor) 103 | CGContext.move(context)(to: CGPoint.zero) 104 | CGContext.addLine(context)(to: CGPoint.init(x: width, y: 0)) 105 | CGContext.drawPath(context)(using: .stroke) 106 | let shadowImage = UIGraphicsGetImageFromCurrentImageContext() 107 | 108 | self.navigationBar.setBackgroundImage(bgImage, for: .default) 109 | self.navigationBar.shadowImage = shadowImage 110 | } 111 | 112 | //MARK: KVO Methods 113 | 114 | 115 | //MARK: Notification Methods 116 | 117 | 118 | //MARK: Target Methods 119 | 120 | /* 121 | // MARK: - Navigation 122 | 123 | // In a storyboard-based application, you will often want to do a little preparation before navigation 124 | override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { 125 | // Get the new view controller using segue.destinationViewController. 126 | // Pass the selected object to the new view controller. 127 | } 128 | */ 129 | 130 | } 131 | -------------------------------------------------------------------------------- /Demo/RefreshController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RefreshController.swift 3 | // CRRefresh 4 | // 5 | // ************************************************** 6 | // * _____ * 7 | // * __ _ __ ___ \ / * 8 | // * \ \/ \/ / / __\ / / * 9 | // * \ _ / | (__ / / * 10 | // * \/ \/ \___/ / /__ * 11 | // * /_____/ * 12 | // * * 13 | // ************************************************** 14 | // Github :https://github.com/imwcl 15 | // HomePage:http://imwcl.com 16 | // CSDN :http://blog.csdn.net/wang631106979 17 | // 18 | // Created by 王崇磊 on 16/9/14. 19 | // Copyright © 2016年 王崇磊. All rights reserved. 20 | // 21 | // @class RefreshController 22 | // @abstract 刷新VC 23 | // @discussion 刷新VC 24 | // 25 | 26 | import UIKit 27 | import CRRefresh 28 | 29 | class RefreshController: BaseViewController { 30 | 31 | var tableView: UITableView = { 32 | let table = UITableView(frame: .zero, style: .grouped) 33 | return table 34 | }() 35 | 36 | var count: Int = 10 37 | 38 | var refresh: Refresh 39 | 40 | init(refresh: Refresh) { 41 | self.refresh = refresh 42 | super.init(nibName: nil, bundle: nil) 43 | } 44 | 45 | required init?(coder aDecoder: NSCoder) { 46 | fatalError("init(coder:) has not been implemented") 47 | } 48 | 49 | //MARK: Public Methods 50 | override func configNavBar() { 51 | super.configNavBar() 52 | addNavDefaultBackButton() 53 | navTitle = refresh.model.title 54 | } 55 | 56 | override func configView() { 57 | super.configView() 58 | tableView.frame = .init(x: 0, y: 0, width: view.frame.width, height: view.frame.height - 64) 59 | tableView.register(UINib.init(nibName: "RefreshCell", bundle: nil), forCellReuseIdentifier: "RefreshCell") 60 | view.addSubview(tableView) 61 | tableView.delegate = self 62 | tableView.dataSource = self 63 | tableView.separatorStyle = .none 64 | 65 | tableView.cr.addHeadRefresh(animator: refresh.header.commont()) { [weak self] in 66 | print("开始刷新") 67 | DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { 68 | self?.count = 10 69 | self?.tableView.cr.endHeaderRefresh() 70 | self?.tableView.cr.resetNoMore() 71 | self?.tableView.reloadData() 72 | }) 73 | } 74 | 75 | tableView.cr.beginHeaderRefresh() 76 | 77 | tableView.cr.addFootRefresh(animator: refresh.footer.commont()) { [weak self] in 78 | print("开始加载") 79 | DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { 80 | self?.count += 10 81 | self?.tableView.cr.endLoadingMore() 82 | self?.tableView.reloadData() 83 | }) 84 | } 85 | 86 | tableView.isPagingEnabled = true 87 | 88 | } 89 | 90 | //MARK: Override 91 | override func viewDidLoad() { 92 | super.viewDidLoad() 93 | 94 | // Do any additional setup after loading the view. 95 | } 96 | 97 | override func didReceiveMemoryWarning() { 98 | super.didReceiveMemoryWarning() 99 | // Dispose of any resources that can be recreated. 100 | } 101 | 102 | } 103 | 104 | // MARK: - Table view data source 105 | extension RefreshController: UITableViewDelegate, UITableViewDataSource { 106 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 107 | tableView.deselectRow(at: indexPath, animated: true) 108 | } 109 | 110 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 111 | return count 112 | } 113 | 114 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 115 | return CGFloat.leastNormalMagnitude 116 | } 117 | 118 | func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { 119 | return CGFloat.leastNormalMagnitude 120 | } 121 | 122 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 123 | return 130 124 | } 125 | 126 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 127 | let cell = tableView.dequeueReusableCell(withIdentifier: "RefreshCell", for: indexPath) as! RefreshCell 128 | return cell 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Demo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // CRRefresh 4 | // 5 | // Created by 王崇磊 on 2017/2/24. 6 | // Copyright © 2017年 王崇磊. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CRRefresh 11 | 12 | class ViewController: BaseViewController { 13 | 14 | var tableView: UITableView = { 15 | let table = UITableView(frame: .zero, style: .grouped) 16 | return table 17 | }() 18 | 19 | var refreshs: [Refresh] = [ 20 | Refresh(model: .init(icon: #imageLiteral(resourceName: "Image_1"), title: "NormalAnimator", subTitle: "普通刷新控件"), header: .nomalHead, footer: .nomalFoot), 21 | Refresh(model: .init(icon: #imageLiteral(resourceName: "Image_2"), title: "SlackLoadingAnimator", subTitle: "SlackLoading的刷新控件"), header: .slackLoading, footer: .slackLoading), 22 | Refresh(model: .init(icon: #imageLiteral(resourceName: "Image_3"), title: "RamotionAnimator", subTitle: "Ramotion的刷新控件"), header: .ramotion, footer: .nomalFoot), 23 | Refresh(model: .init(icon: #imageLiteral(resourceName: "Image_1"), title: "FastAnimator", subTitle: "FastAnimator的刷新控件"), header: .fast, footer: .nomalFoot) 24 | ] 25 | 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | } 30 | 31 | override func configNavBar() { 32 | super.configNavBar() 33 | navTitle = "CRRefresh" 34 | } 35 | 36 | override func configView() { 37 | super.configView() 38 | tableView.frame = .init(x: 0, y: 0, width: view.frame.width, height: view.frame.height - 64) 39 | tableView.register(UINib.init(nibName: "NormalCell", bundle: nil), forCellReuseIdentifier: "NormalCell") 40 | view.addSubview(tableView) 41 | tableView.delegate = self 42 | tableView.dataSource = self 43 | } 44 | 45 | override func didReceiveMemoryWarning() { 46 | super.didReceiveMemoryWarning() 47 | // Dispose of any resources that can be recreated. 48 | } 49 | } 50 | 51 | 52 | // MARK: - Table view data source 53 | extension ViewController: UITableViewDelegate, UITableViewDataSource { 54 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 55 | tableView.deselectRow(at: indexPath, animated: true) 56 | let refresh = refreshs[indexPath.row] 57 | let vc = RefreshController(refresh: refresh) 58 | navigationController?.pushViewController(vc, animated: true) 59 | } 60 | 61 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 62 | return refreshs.count 63 | } 64 | 65 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 66 | return CGFloat.leastNormalMagnitude 67 | } 68 | 69 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 70 | return 70 71 | } 72 | 73 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 74 | let cell = tableView.dequeueReusableCell(withIdentifier: "NormalCell", for: indexPath) as! NormalCell 75 | let refresh = refreshs[indexPath.row] 76 | cell.config(refresh.model) 77 | return cell 78 | } 79 | } 80 | 81 | struct Refresh { 82 | var model: Model 83 | var header: Style 84 | var footer: Style 85 | struct Model { 86 | var icon: UIImage 87 | var title: String 88 | var subTitle: String 89 | } 90 | 91 | enum Style { 92 | // 普通刷新类 93 | case nomalHead 94 | case nomalFoot 95 | // slackLoading刷新控件 96 | case slackLoading 97 | // ramotion动画 98 | case ramotion 99 | // fast动画 100 | case fast 101 | 102 | func commont() -> CRRefreshProtocol { 103 | switch self { 104 | case .nomalHead: 105 | return NormalHeaderAnimator() 106 | case .nomalFoot: 107 | return NormalFooterAnimator() 108 | case .slackLoading: 109 | return SlackLoadingAnimator() 110 | case .ramotion: 111 | return RamotionAnimator() 112 | case .fast: 113 | return FastAnimator() 114 | } 115 | } 116 | } 117 | } 118 | 119 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 W_C__L 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Version](https://img.shields.io/cocoapods/v/CRRefresh.svg?style=flat)](http://cocoapods.org/pods/CRRefresh) 2 | [![License](https://img.shields.io/cocoapods/l/CRRefresh.svg?style=flat)](http://cocoapods.org/pods/CRRefresh) 3 | [![Platform](https://img.shields.io/cocoapods/p/CRRefresh.svg?style=flat)](http://cocoapods.org/pods/CRRefresh) 4 | [![Support](https://img.shields.io/badge/support-iOS%208%2B%20-blue.svg?style=flat)](https://www.apple.com/nl/ios/)  5 | ![Language](https://img.shields.io/badge/Language-%20swift%20%20-blue.svg) 6 | 7 | ![](CRRefresh.png) 8 | 9 | **CRRefresh** an easy way to use pull-to-refresh, If you want to customize its UI style, you just need conform the specified protocol. We will not regularly updated some nice dynamic effect, at the same time also welcome to write to me ~ 10 | 11 | [中文介绍](README_CN.md) / [博客介绍](http://blog.csdn.net/wang631106979/article/details/62888435) 12 | 13 | ## Screenshots 14 | 15 | | ![](CRRefresh1.gif) | ![](CRRefresh2.gif) | ![](CRRefresh3.gif) | 16 | | :-----------------: | :--------------------: | :-----------------: | 17 | | `NormalAnimator` | `SlackLoadingAnimator` | `RamotionAnimator` | 18 | | ![](CRRefresh4.gif) | | | 19 | | `FastAnimator` | | | 20 | 21 | ## Requirements 22 | 23 | - Xcode 8 or later 24 | - iOS 8.0 or later 25 | - ARC 26 | - Swift 3.0 or later 27 | 28 | ## Features 29 | 30 | - Support `UIScrollView` and its subclasses `UICollectionView` `UITableView` `UITextView` 31 | - Pull-Down to refresh and Pull-Up to load more 32 | - Support customize your own style(s) 33 | 34 | ## Installation 35 | 36 | ### CocoaPods 37 | 38 | CocoaPods is the recommended way to add CRRefresh to your project. 39 | 40 | Add a pod entry for CRRefresh to your Podfile. 41 | 42 | ```ruby 43 | pod 'CRRefresh' 44 | ``` 45 | 46 | Second, install CRRefresh into your project: 47 | 48 | ```ruby 49 | pod install 50 | ``` 51 | ### Carthage 52 | 53 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate `CRRefresh` into your Xcode project using Carthage, specify it in your `Cartfile`: 54 | 55 | ``` 56 | github "CRAnimation/CRRefresh" 57 | ``` 58 | 59 | Run `carthage update` to build the framework and drag the built `CRRefresh.framework` (in Carthage/Build/iOS folder) into your Xcode project (Linked Frameworks and Libraries in `Targets`). 60 | 61 | ### Manually 62 | 63 | 1. Download the latest code version . 64 | 2. Open your project in Xcode,drag the `CRRefresh` folder into your project. Make sure to select Copy items when asked if you extracted the code archive outside of your project. 65 | 3. You need it with `import CRRefresh `. 66 | 67 | ## Usage 68 | 69 | ![](CRRefresh3.gif) 70 | 71 | **Add `CRRefresh` to your project** 72 | 73 | ```swift 74 | import CRRefresh 75 | ``` 76 | 77 | **Add pull-to-refresh** 78 | 79 | ```swift 80 | /// animator: your customize animator, default is NormalHeaderAnimator 81 | tableView.cr.addHeadRefresh(animator: NormalHeaderAnimator()) { [weak self] in 82 | /// start refresh 83 | /// Do anything you want... 84 | DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { 85 | /// Stop refresh when your job finished, it will reset refresh footer if completion is true 86 | self?.tableView.cr.endHeaderRefresh() 87 | }) 88 | } 89 | /// manual refresh 90 | tableView.cr.beginHeaderRefresh() 91 | ``` 92 | 93 | **Add infinite-scrolling** 94 | 95 | ```swift 96 | /// animator: your customize animator, default is NormalFootAnimator 97 | tableView.cr.addFootRefresh(animator: NormalFootAnimator()) { [weak self] in 98 | /// start refresh 99 | /// Do anything you want... 100 | DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { 101 | /// If common end 102 | self?.tableView.cr.endLoadingMore() 103 | /// If no more data 104 | self?.tableView.cr.noticeNoMoreData() 105 | /// Reset no more data 106 | self?.tableView.cr.resetNoMore() 107 | }) 108 | } 109 | ``` 110 | 111 | ## Customize Style 112 | 113 | Customize refresh need conform the **CRRefreshProtocol** protocol. 114 | 115 | ```swift 116 | public protocol CRRefreshProtocol { 117 | /// Customize view 118 | var view: UIView {get} 119 | 120 | /// View insets 121 | var insets: UIEdgeInsets {set get} 122 | 123 | /// The height of the trigger to refresh 124 | var trigger: CGFloat {set get} 125 | 126 | /// The height of the animation is executed 127 | var execute: CGFloat {set get} 128 | 129 | /// Start refresh 130 | mutating func refreshBegin(view: CRRefreshComponent) 131 | 132 | /// End refresh 133 | mutating func refreshEnd(view: CRRefreshComponent, finish: Bool) 134 | 135 | /// Refresh the progress changes 136 | mutating func refresh(view: CRRefreshComponent, progressDidChange progress: CGFloat) 137 | 138 | /// Refresh the state changes 139 | mutating func refresh(view: CRRefreshComponent, stateDidChange state: CRRefreshState) 140 | } 141 | ``` 142 | 143 | ## Remove 144 | 145 | ```swift 146 | tableView.cr.removeFooter() 147 | tableView.cr.removeHeader() 148 | ``` 149 | 150 | ## Contribution 151 | 152 | You are welcome to contribute to the project by forking the repo, modifying the code and opening issues or pull requests. 153 | 154 | ## Contacts 155 | 156 | If you wish to contact me 157 | 158 | - Email: wangchonglei93@icloud.com 159 | - Github: https://github.com/imwcl 160 | - QQ: 631106979 161 | - Dynamic effect study group: 547897182 162 | 163 | ## License 164 | 165 | CRRefresh is released under the [MIT license](LICENSE). See LICENSE for details. 166 | 167 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | [![Version](https://img.shields.io/cocoapods/v/CRRefresh.svg?style=flat)](http://cocoapods.org/pods/CRRefresh) 2 | [![License](https://img.shields.io/cocoapods/l/CRRefresh.svg?style=flat)](http://cocoapods.org/pods/CRRefresh) 3 | [![Platform](https://img.shields.io/cocoapods/p/CRRefresh.svg?style=flat)](http://cocoapods.org/pods/CRRefresh) 4 | [![Support](https://img.shields.io/badge/support-iOS%208%2B%20-blue.svg?style=flat)](https://www.apple.com/nl/ios/)  5 | ![Language](https://img.shields.io/badge/Language-%20swift%20%20-blue.svg) 6 | 7 | ![](CRRefresh.png) 8 | 9 | **CRRefresh** 是一个简单易用的上拉刷新控件,你可以高度自定义UI样式,我们会不定期更新一些好看的动效,同时也欢迎大家投稿~~ 10 | 11 | [英文介绍](README.md) / [博客介绍](http://blog.csdn.net/wang631106979/article/details/62888435) 12 | 13 | ## 效果图 14 | 15 | | ![](CRRefresh1.gif) | ![](CRRefresh2.gif) | ![](CRRefresh3.gif) | 16 | | :-----------------: | :--------------------: | :-----------------: | 17 | | `NormalAnimator` | `SlackLoadingAnimator` | `RamotionAnimator` | 18 | | ![](CRRefresh4.gif) | | | 19 | | `FastAnimator` | | | 20 | 21 | ## 支持环境 22 | 23 | - Xcode 8 or later 24 | - iOS 8.0 or later 25 | - ARC 26 | - Swift 3.0 or later 27 | 28 | ## Features 29 | 30 | - 支持`UIScrollView`及其子类`UICollectionView`、`UITableView`、`UIWebView`等 31 | - 支持下拉刷新和上拉加载更多 32 | - 支持下拉刷新和上拉加载更多 33 | 34 | ## 如何安装 35 | 36 | ### CocoaPods 37 | 38 | ```ruby 39 | pod 'CRRefresh' 40 | ``` 41 | 42 | ### Carthage 43 | 44 | 使用Carthage安装 45 | 46 | 1. 在`Cartfile`中添加`github "CRAnimation/CRRefresh"` 47 | 2. 执行`carthage update` 48 | 3. 导入`CRRefresh.framework` 49 | 50 | ### 手动安装 51 | 52 | 1. 下载最新版本的代码 53 | 2. 将 Source 内的源文件添加(拖放)到你的工程 54 | 3. 导入 `import CRRefresh `. 55 | 56 | ## 使用 57 | 58 | ![](CRRefresh3.gif) 59 | 60 | **添加 `CRRefresh` 到你的工程** 61 | 62 | ```swift 63 | import CRRefresh 64 | ``` 65 | 66 | **添加上拉刷新控件** 67 | 68 | ```swift 69 | /// animator: 你的上拉刷新的Animator, 默认是 NormalHeaderAnimator 70 | tableView.cr.addHeadRefresh(animator: NormalHeaderAnimator()) { [weak self] in 71 | /// 开始刷新了 72 | /// 开始刷新的回调 73 | DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { 74 | /// 停止刷新 75 | self?.tableView.cr.endHeaderRefresh() 76 | }) 77 | } 78 | /// 手动刷新 79 | tableView.cr.beginHeaderRefresh() 80 | ``` 81 | 82 | **添加下拉加载控件** 83 | 84 | ```swift 85 | /// animator: 你的下拉加载的Animator, 默认是NormalFootAnimator 86 | tableView.cr.addFootRefresh(animator: NormalFootAnimator()) { [weak self] in 87 | /// 开始下拉加载 88 | /// 回调 89 | DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { 90 | /// 结束加载 91 | self?.tableView.cr.endLoadingMore() 92 | /// 没有更多数据 93 | self?.tableView.cr.noticeNoMoreData() 94 | /// 复位 95 | self?.tableView.cr.resetNoMore() 96 | }) 97 | } 98 | ``` 99 | 100 | ## 自定义样式 101 | 102 | 你只需要实现`CRRefreshProtocol`就可以自定义样式了 103 | 104 | ```swift 105 | public protocol CRRefreshProtocol { 106 | /// 自定义的view 107 | var view: UIView {get} 108 | 109 | /// view的insets 110 | var insets: UIEdgeInsets {set get} 111 | 112 | /// 触发刷新的高度 113 | var trigger: CGFloat {set get} 114 | 115 | /// 动画执行时的高度 116 | var execute: CGFloat {set get} 117 | 118 | /// 开始刷新 119 | mutating func refreshBegin(view: CRRefreshComponent) 120 | 121 | /// 结束刷新 122 | mutating func refreshEnd(view: CRRefreshComponent, finish: Bool) 123 | 124 | /// 刷新进度的变化 125 | mutating func refresh(view: CRRefreshComponent, progressDidChange progress: CGFloat) 126 | 127 | /// 刷新状态的变化 128 | mutating func refresh(view: CRRefreshComponent, stateDidChange state: CRRefreshState) 129 | } 130 | ``` 131 | 132 | ## 移除控件 133 | 134 | ```swift 135 | tableView.cr.removeFooter() 136 | tableView.cr.removeHeader() 137 | ``` 138 | 139 | ## 贡献你的代码 140 | 141 | 欢迎大家贡献代码,或者投稿好看的动效 142 | 143 | ## 联系 144 | 145 | 如果你想联系我: 146 | 147 | - Email: wangchonglei93@icloud.com 148 | - Github: https://github.com/imwcl 149 | - QQ: 631106979 150 | - 动效学习群:547897182 151 | 152 | ## 许可证 153 | 154 | CRRefresh [MIT license](LICENSE)下发布。有关详细信息,请参阅许可证。 --------------------------------------------------------------------------------