在iOS開發中, 上下拉加載的刷新動畫大多數的APP都會採用基本類似的樣式和動畫, 固然仍是有不少優秀的加載動畫, 不過這些動畫在國內的APP中真的是不多看到使用(感受比較新穎的東西都不多是國人本身首先實現的...), 在使用oc的時候, 相信不少的開發者都會選擇MJRefresh來集成上下拉刷新, 這個優秀的加載框架很方便的實現了常見的加載需求, 同時, 由於其是使用系統的UIImageView來實現gif圖片的播放, 那麼就能夠很方便的直接利用設計給的gif動畫圖片來實現上下拉加載動畫. 由於如今的筆者開發使用swift的時間比較多了, 不少的東西仍是比較但願使用swift實現的. 像刷新控件, 也但願使用個swift的, 因而本身動手也實現了一個, 在使用上儘可能是接近了MJRefresh的, 不過, 若是你去比較的話, 和MJRefresh的效果,靈活度等類似, 可是代碼量相差很大, 筆者這個主要文件一個代碼量不到400行, 若是你要借鑑的話, 非常方便. 而後須要說明的是, 在oc中提倡使用繼承來實現不少東西, 不過swift提倡面向協議編程, 因此此次我也是用協議來實現的.Demo地址(這個是在草原旅行的路上坐車寫的, 草原的風光最近真的不錯)git
其實仔細想一想, 上下拉刷新的原理仍是很簡單的 ------>>> 首先把刷新控件添加到scrollView的頭部或者底部, 而後監控到scrollView的滾動進度(底部刷新控件還須要監控scrollView的內容的改變, 每次改變後再次將控件調整到scrollView的底部), 根據不一樣的進度來設置刷新控件的相應的文字和圖片動畫等...github
zj_refreshHeader
和zj_refreshFooter
用來存取header和footer刷新控件, 這裏有兩種方法能夠實現 1, 使用運行時 private var ZJHeaderKey: UInt8 = 0
private var ZJFooterKey: UInt8 = 0
extension UIScrollView {
private var zj_refreshHeader: RefreshView? {
set {
objc_setAssociatedObject(self, &ZJHeaderKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
return objc_getAssociatedObject(self, &ZJHeaderKey) as? RefreshView
}
}
private var zj_refreshFooter: RefreshView? {
set {
objc_setAssociatedObject(self, &ZJFooterKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
return objc_getAssociatedObject(self, &ZJFooterKey) as? RefreshView
}
}
}複製代碼
2, 使用tag來存取編程
private var ZJHeaderTag = 1994
private var ZJFooterTag = 1995
extension UIScrollView {
private var zj_refreshHeader: RefreshView? {
set {
if let header = newValue {
header.tag = ZJHeaderTag
addSubview(header)
}
}
get {
return viewWithTag(ZJHeaderTag) as? RefreshView
}
}
private var zj_refreshFooter: RefreshView? {
set {
if let footer = newValue {
footer.tag = ZJFooterTag
addSubview(footer)
}
}
get {
return viewWithTag(ZJFooterTag) as? RefreshView
}
}
}複製代碼
<Animator where Animator: UIView, Animator: RefreshViewDelegate>
這個就是約束Animator必須是UIView而且遵照RefreshViewDelegate協議的類型public func zj_addRefreshHeader(headerAnimator: Animator, refreshHandler: RefreshHandler ) { } public func zj_addRefreshFooter 複製代碼(footerAnimator: Animator, refreshHandler: RefreshHandler ) { }
/// 開啓header刷新
public func zj_startHeaderAnimation() {
zj_refreshHeader?.canBegin = true
}
/// 結束header刷新
public func zj_stopHeaderAnimation() {
zj_refreshHeader?.canBegin = false
}
/// 開啓footer刷新
public func zj_startFooterAnimation() {
zj_refreshFooter?.canBegin = true
}
/// 結束footer刷新
public func zj_stopFooterAnimation() {
zj_refreshFooter?.canBegin = false
}複製代碼
public enum RefreshViewState {
/// 正在加載狀態
case loading
/// 正常狀態
case normal
/// 下拉狀態
case pullToRefresh
/// 鬆開手即進入刷新狀態
case releaseToFresh
}複製代碼
/// public func zj_addRefreshHeader(headerAnimator: Animator, refreshHandler: RefreshHandler ) { if let header = zj_refreshHeader { header.removeFromSuperview() } /// let frame = CGRect(x: 0.0, y: -headerAnimator.bounds.height, width: bounds.width, height: headerAnimator.bounds.height) zj_refreshHeader = RefreshView(frame: frame, refreshType: .header, refreshAnimator: headerAnimator, refreshHandler: refreshHandler) addSubview(zj_refreshHeader!) } 複製代碼
private func addObserverOf(scrollView: UIScrollView?) {
scrollView?.addObserver(self, forKeyPath: ConstantValue.ScrollViewContentOffsetPath, options: .Initial, context: &ConstantValue.RefreshViewContext)
}複製代碼
if scrollView.contentOffset.y > -scrollViewOriginalValue.contentInset.top {/**頭部視圖(隱藏)而且還沒到顯示的臨界點*/ return }
// 已經進入拖拽狀態, 進行相關操做
let progress = (-scrollViewOriginalValue.contentInset.top - scrollView.contentOffset.y) / self.bounds.height
if scrollView.tracking {
if progress >= 1.0 {
refreshViewState = .releaseToFresh
} else if progress <= 2="" 0.0="" {="" refreshviewstate=".normal" }="" else="" if="" .releasetofresh="" releasetofreah="" refresh="" canbegin="true//" begin="" release="" progress="" <="0.0" var="" actualprogress="min(1.0," progress)="" actualprogress)="" refreshanimator.refreshdidchangeprogress(self,="" progress:="" actualprogress,="" refreshviewtype:="" refreshviewtype)<="" code="">
複製代碼
開始動畫的時候, 由於刷新控件是添加到scrollView的頭部或者底部的, 在滾動的時候由於scrollView的bounces的緣由, 鬆開手以後, 刷新控件是會回到原來的位置的, 這個時候, 咱們但願加載動畫的時候, 刷新控件停在咱們的實現以內, 因此須要調整scrollView的contentInset(會自動調整contentOffset), 好比下拉刷新須要將contentInset的top加上刷新控件的高度, 上拉刷新的時候須要將contentInset的bottom加上刷新控件的高度swift
private func startAnimation() {
guard let validScrollView = scrollView else { return }
validScrollView.bounces = false
/// may update UI
dispatch_async(dispatch_get_main_queue(), {[weak self] in
guard let validSelf = self else { return }
UIView.animateWithDuration(0.25, animations: {
if validSelf.refreshViewType == .header {
validScrollView.contentInset.top = validSelf.scrollViewOriginalValue.contentInset.top + validSelf.bounds.height
} else {
let offPartHeight = validScrollView.contentSize.height - validSelf.heightOfContentOnScreenOfScrollView(validScrollView)
/// contentSize改變的時候設置的self.y不一樣致使不一樣的結果
/// 全部內容高度>屏幕上顯示的內容高度
let notSureBottom = validSelf.scrollViewOriginalValue.contentInset.bottom + validSelf.bounds.height
validScrollView.contentInset.bottom = offPartHeight>=0 ? notSureBottom : notSureBottom - offPartHeight // 加上
}
}, completion: { (_) in
/// 這個時候才正式刷新
validScrollView.bounces = true
validSelf.refreshViewState = .loading
validSelf.refreshHandler()
})
})
}複製代碼
中止動畫的時候, 須要將scrollView的contentInset復原爲動畫開始以前, 以便於不影響頁面的其餘佈局數組
對於上拉刷新而言, 只是要多一個監控scrollView的contentSize, 在其改變的時候再次將刷新控件調整到scrollView的contentSize的底部網絡
RefreshViewDelegate的定義多線程
public protocol RefreshViewDelegate {
/// 你應該爲每個header或者footer設置一個不一樣的key來保存時間, 不然將公用同一個key使用相同的時間
var lastRefreshTimeKey: String? { get }
/// 是否刷新完成後自動隱藏 默認爲false
var isAutomaticlyHidden: Bool { get }
/// 上次刷新時間, 有默認賦值和返回
var lastRefreshTime: NSDate? { get set }
/// repuired 三個必須實現的代理方法
/// 開始進入刷新(loading)狀態, 這個時候應該開啓自定義的(動畫)刷新
func refreshDidBegin(refreshView: RefreshView, refreshViewType: RefreshViewType)
/// 刷新結束狀態, 這個時候應該關閉自定義的(動畫)刷新
func refreshDidEnd(refreshView: RefreshView, refreshViewType: RefreshViewType)
/// 刷新狀態變爲新的狀態, 這個時候能夠自定義設置各個狀態對應的屬性
func refreshDidChangeState(refreshView: RefreshView, fromState: RefreshViewState, toState: RefreshViewState, refreshViewType: RefreshViewType)
/// optional 兩個可選的實現方法
/// 容許在控件添加到scrollView以前的準備
func refreshViewDidPrepare(refreshView: RefreshView, refreshType: RefreshViewType)
/// 拖拽的進度, 可用於自定義實現拖拽過程當中的動畫
func refreshDidChangeProgress(refreshView: RefreshView, progress: CGFloat, refreshViewType: RefreshViewType)
}複製代碼
public class NormalAnimator: UIView {
/// 設置imageView
@IBOutlet private(set) weak var imageView: UIImageView!
@IBOutlet private(set) weak var indicatorView: UIActivityIndicatorView!
/// 設置state描述
@IBOutlet private(set) weak var descriptionLabel: UILabel!
/// 上次刷新時間label footer 默認爲hidden, 可設置hidden=false開啓
@IBOutlet private(set) weak var lastTimelabel: UILabel!
public typealias SetDescriptionClosure = (refreshState: RefreshViewState, refreshType: RefreshViewType) -> String
public typealias SetLastTimeClosure = (date: NSDate) -> String
/// 是否刷新完成後自動隱藏 默認爲false
/// 這個屬性是協議定義的, 當寫在class裏面能夠供外界修改, 若是寫在extension裏面只能是可讀的
public var isAutomaticlyHidden: Bool = false
private var setupDesctiptionClosure: SetDescriptionClosure?
private var setupLastTimeClosure: SetLastTimeClosure?
/// 耗時
private lazy var formatter: NSDateFormatter = {
let formatter = NSDateFormatter()
formatter.dateStyle = .ShortStyle
return formatter
}()
/// 耗時
private lazy var calendar: NSCalendar = NSCalendar.currentCalendar()
public class func normalAnimator() -> NormalAnimator {
return NSBundle.mainBundle().loadNibNamed(String(NormalAnimator), owner: nil, options: nil).first as! NormalAnimator
}
public func setupDescriptionForState(closure: SetDescriptionClosure) {
setupDesctiptionClosure = closure
}
public func setupLastFreshTime(closure: SetLastTimeClosure) {
setupLastTimeClosure = closure
}
override public func awakeFromNib() {
super.awakeFromNib()
indicatorView.hidden = true
indicatorView.hidesWhenStopped = true
}
// public override func layoutSubviews() {
// super.layoutSubviews()
// print("layout--------------------------------------------")
// }
}
extension NormalAnimator: RefreshViewDelegate {
public func refreshViewDidPrepare(refreshView: RefreshView, refreshType: RefreshViewType) {
if refreshType == .header {
} else {
lastTimelabel.hidden = true
rotateArrowToUpAnimated(false)
}
setupLastTime()
}
public func refreshDidBegin(refreshView: RefreshView, refreshViewType: RefreshViewType) {
indicatorView.hidden = false
indicatorView.startAnimating()
}
public func refreshDidEnd(refreshView: RefreshView, refreshViewType: RefreshViewType) {
indicatorView.stopAnimating()
}
public func refreshDidChangeProgress(refreshView: RefreshView, progress: CGFloat, refreshViewType: RefreshViewType) {
// print(progress)
}
public func refreshDidChangeState(refreshView: RefreshView, fromState: RefreshViewState, toState: RefreshViewState, refreshViewType: RefreshViewType) {
print(toState)
setupDescriptionForState(toState, type: refreshViewType)
switch toState {
case .loading:
imageView.hidden = true
case .normal:
setupLastTime()
imageView.hidden = false
///恢復
if refreshViewType == .header {
rotateArrowToDownAnimated(false)
} else {
rotateArrowToUpAnimated(false)
}
case .pullToRefresh:
if refreshViewType == .header {
if fromState == .releaseToFresh {
rotateArrowToDownAnimated(true)
}
} else {
if fromState == .releaseToFresh {
rotateArrowToUpAnimated(true)
}
}
imageView.hidden = false
case .releaseToFresh:
imageView.hidden = false
if refreshViewType == .header {
rotateArrowToUpAnimated(true)
} else {
rotateArrowToDownAnimated(true)
}
}
}
private func setupDescriptionForState(state: RefreshViewState, type: RefreshViewType) {
if descriptionLabel.hidden {
descriptionLabel.text = ""
} else {
if let closure = setupDesctiptionClosure {
descriptionLabel.text = closure(refreshState: state, refreshType: type)
} else {
switch state {
case .normal:
descriptionLabel.text = "正常狀態"
case .loading:
descriptionLabel.text = "加載數據中..."
case .pullToRefresh:
if type == .header {
descriptionLabel.text = "繼續下拉刷新"
} else {
descriptionLabel.text = "繼續上拉刷新"
}
case .releaseToFresh:
descriptionLabel.text = "鬆開手刷新"
}
}
}
}
}複製代碼
let normal = NormalAnimator.normalAnimator()
/// 指定存儲刷新時間的key, 若是不指定或設置爲nil, 那麼將會和其餘未指定的使用相同的key(記錄的時間相同, MJRefresh是全部的控件使用相同的時間的)
normal.lastRefreshTimeKey = "DemoKey1"
/// 隱藏時間顯示
// normal.lastTimelabel.hidden = true
/// 自定義提示文字
// normal.setupDescriptionForState { (refreshState,refreshType) -> String in
// switch refreshState {
// case .loading:
// return "努力加載中"
// case .normal:
// return "休息中"
// case .pullToRefresh:
// if refreshType == .header {
// return "繼續下下下下"
//
// } else {
// return "繼續上上上上"
// }
// case .releaseToFresh:
// return "放開我"
// };
// }
/// 自定義時間顯示
// normal.setupLastFreshTime { (date) -> String in
// return ...
// }
tableView.zj_addRefreshHeader(normal, refreshHandler: {[weak self] in
/// 多線程中不要使用 [unowned self]
/// 注意這裏的gcd是爲了模擬網絡加載的過程, 在實際的使用中, 不須要這段gcd代碼, 直接在這裏進行網絡請求, 在請求完畢後, 調用分類方法, 結束刷新
dispatch_async(dispatch_get_global_queue(0, 0), {
for i in 0...50000 {
if i <= 10="" {="" self?.data.append(i)="" }="" 延時="" print("加載數據中")="" dispatch_async(dispatch_get_main_queue(),="" self?.tableview.reloaddata()="" 刷新完畢,="" 中止動畫="" self?.tableview.zj_stopheaderanimation()="" })="" })<="" code="">
複製代碼
/// 設置高度
let gifAnimatorHeader = GifAnimator.gifAnimatorWithHeight(100.0)
gifAnimatorHeader.lastRefreshTimeKey = "exampleHeader4"
/// 爲不一樣的state設置不一樣的圖片
/// 閉包須要返回一個元組: 圖片數組和gif動畫每一幀的執行時間
/// 通常須要設置loading狀態的圖片(必須), 做爲加載的gif
/// 和pullToRefresh狀態的圖片數組(可選擇設置), 做爲拖拽時的加載動畫
gifAnimatorHeader.setupImagesForRefreshstate { (refreshState) -> (images: [UIImage], duration: Double)? in
if refreshState == .loading {
var images = [UIImage]()
for index in 1...47 {
let image = UIImage(named: "loading\\(index)")!
images.append(image)
}
return (images, 1.0)
}
else if refreshState == .pullToRefresh {
var images = [UIImage]()
for index in 1...47 {
let image = UIImage(named: "loading\\(index)")!
images.append(image)
}
return (images, 0.25)
}
return nil
}
tableView.zj_addRefreshHeader(gif, refreshHandler: {[weak self] in
/// 多線程中不要使用 [unowned self]
/// 注意這裏的gcd是爲了模擬網絡加載的過程, 在實際的使用中, 不須要這段gcd代碼, 直接在這裏進行網絡請求, 在請求完畢後, 調用分類方法, 結束刷新
dispatch_async(dispatch_get_global_queue(0, 0), {
for i in 0...50000 {
if i <= 10="" {="" self?.data.append(i)="" }="" 延時="" print("加載數據中")="" dispatch_async(dispatch_get_main_queue(),="" self?.tableview.reloaddata()="" 刷新完畢,="" 中止動畫="" self?.tableview.zj_stopheaderanimation()="" })="" })<="" code="">
複製代碼
class TestNormal {
class func normal() -> NormalAnimator {
let normal = NormalAnimator.normalAnimator()
/// 隱藏時間顯示
// normal.lastTimelabel.hidden = true
/// 指定存儲刷新時間的key, 若是不指定或設置爲nil, 那麼將會和其餘未指定的使用相同的key(記錄的時間相同, MJRefresh是全部的控件使用相同的時間的)
normal.lastRefreshTimeKey = "DemoKey1"
normal.setupDescriptionForState({ (refreshState ,refreshType) -> String in
switch refreshState {
case .loading:
return "努力加載中"
case .normal:
return "休息中"
case .pullToRefresh:
if refreshType == .header {
return "繼續下下下下"
} else {
return "繼續上上上上"
}
case .releaseToFresh:
return "放開我"
}
})
return normal
}
}
/// 使用方法
let footer = TestNormal.normal()
tableView.zj_addRefreshFooter(footer) {[weak self] in
dispatch_async(dispatch_get_global_queue(0, 0), {
for i in 0...50000 {
if i <= 10="" {="" self?.data.append(i)="" }="" 延時="" print("加載數據中")="" dispatch_async(dispatch_get_main_queue(),="" self?.tableview.reloaddata()="" self?.tableview.zj_stopfooteranimation()="" })="" }<="" code="">
複製代碼
總的來講, 簡單寫一個刷新控件仍是很簡單的, 可是在實現的過程當中有不少的細節須要調整, 好比刷新的時候要處理sectionHeader的懸停問題... (這裏直接借鑑了MJRefresh中的處理了), Demo地址閉包