背景
寫這篇文章主要有兩個目的:
- 雖然refresh的源碼已經有很多小伙伴分析過了。但是其應用不能說不夠廣闊。所以,抽空重新梳理了一下該源碼辐啄,然后記錄一下自己的心得采章。方便之后重新閱讀,然后對比更新壶辜。
- 另一個問題是發(fā)現(xiàn)一個閱讀源碼的現(xiàn)象:比如源碼有100個文件悯舟,當閱讀了10個文件,就說我閱讀過某某某框架的源碼砸民,然后看到源碼中某某方法和另一方法實現(xiàn)一致图谷,然后就直接調用同一個方法。但是此時就很容易出現(xiàn)問題阱洪。有時和你理論的時候便贵,依舊意識不到問題所在,繼續(xù)翻開源碼說實現(xiàn)是一致的冗荸〕辛В可源碼整體看一下,就能發(fā)現(xiàn)遇到的問題蚌本。最后一種情況將會分析這遇到的問題盔粹。
本文從以下幾個方面進行記錄:
- 繼承層次
- 整體分析
- 各類的職責
- 下拉刷新
- 主動調用代碼
- UI進行交互
- 上拉刷新
- 常見的錯誤?
一、基本類繼承層次關系
// 為了方便表示程癌,使用~代替
~ = MJRefresh
~Component: UIView
~Header
~StateHeader
~NormalHeader
~GifHeader
~Footer
~AutoFooter
~AutoStateFooter
~AutoNormalFooter
~AutoGifFooter
~BackFooter
~BackStateFooter
~BackNormalFooter
~BackGifFooter
二舷嗡、整體分析:
- MJRefreshComponet是繼承自UIView。其他所有的類都是其子類嵌莉。
- mj_header和mj_footer是添加在ScrollView上分類的屬性进萄,其類型是Header/Footer類型,并且其添加到了scrollview的view容器內锐峭。因此在賦值后中鼠,才會出現(xiàn)在頭部/尾部。
通篇控制的方式沿癞,是通過子類重寫其基類的MJRefreshState類型的state屬性援雇,重寫其set方法進行更改當前刷新的狀態(tài)。從而控制頭部和尾部的展示內容(是否隱藏椎扬,內邊距惫搏,偏移量等)。最終匯聚成看到的展示樣貌蚕涤。
三筐赔、各類的職責
要看清各個部件是如何進行邏輯的組織及代碼的編寫,得分清各個類的職責是什么钻趋,負責哪一模塊川陆,然后才能更好地理通整個源碼的思想。
- ~State模塊主要處理狀態(tài):時間和不同狀態(tài)的title
- ~Normal模塊主要處理:箭頭的方向,是否顯示及l(fā)oadingView的是否顯示较沪。
- ~Component基模塊:主要暴露相應的接口鳞绕,block供給子類進行組裝及監(jiān)聽等的處理。
- ~Header 尸曼、~Footer模塊:初始化的操作们何、設置frame的高度、scrollview的布局操作(inset, offset等)控轿。
四冤竹、Header
下拉刷新刷新的方式有兩種:一種是首次進入頁面的時候,就主動觸發(fā)代碼刷新的操作茬射,使其開始下拉刷新鹦蠕。另一種是手動下拉scrollview,進行的一系列UI操作及其給出的反饋在抛。
主動調用代碼進行的刷新操作
調用流程:
-
-[MJRefreshHeader beginRefreshing]
- setState:(refreshing)
- 展示刷新樣式(增加滾動區(qū)域钟病,更新offest值為header的高度)
- 執(zhí)行block內部的刷新請求(executeRefreshingCallback)。
- 當3中的請求回調操作執(zhí)行完之后刚梭,當外部主動調用- endRefreshing時進行還原操作肠阱。
-
-[MJRefreshHeader endRefreshing]
- setState:(idle)
- 恢復刷新樣式,頭部消失(更改inset為初始值)
- 如果endrefreshing中有回調時就執(zhí)行,沒有就結束整個流程朴读。
與UI進行交互出現(xiàn)的刷新操作
主要有通過下拉或者上滑出現(xiàn)的UI變動及刷新的邏輯等屹徘。后半部分是一致的。主要分析前半部分與UI進行交互衅金,然后進行更新頭部的動畫的過程噪伊。
以下拉刷新為例:(其中h為header的高度)
當下拉scrollview時,觸發(fā)其delegate方法:scrollViewContentOffsetDidChange:方法典挑。
-
當drag == true 時酥宴,state狀態(tài)一直在變化啦吧;
下拉到臨界值:
(state == idle && offsetY < -h)時 --> state = pulling您觉。此時狀態(tài)文字要發(fā)生變化,箭頭也要進行翻轉授滓。根據(jù)各類的職責去相應的類中更改即可琳水。上拉到臨界值:
state == pulling && offsetY >= -h時,恢復state = idle般堆。同時恢復箭頭在孝,文字等的狀態(tài)。
當drag == false && state == pulling淮摔,即在拉過臨界值私沮,并且松手了,那么此之后執(zhí)行的邏輯和橙,就是下拉刷新過程仔燕。
五造垛、Footer
比Header包含的狀態(tài)稍微多一點,但是和header的邏輯是類似的晰搀。
Footer分為Auto和Back兩種狀態(tài)
聯(lián)系:與header的比較五辽,之前處理
~Header
的邏輯現(xiàn)在遷移到了AutoFooter和BackFooter。區(qū)別:
~BackFooter
的刷新是通過手動拖拽外恕,并且在tableview的尾部最后進行展示的杆逗。而~AutoFooter的刷新操作,當上滑到最后一個cell時鳞疲,將會自動進行刷新的操作罪郊。
對于footer通常不會去主動調用- beginRefresing該方法。他在設置好相應的初始位置后尚洽,就開始進行手動與UI
進行交互的上拉刷新操作:
~BackFooter:
- footer的位置在屏幕的下方(無論是contentsize.height是否大于screensize.height).
- 之后的操作就不再贅述排龄,和下拉刷新的流程真的一模一樣。(可以通過源碼進行對照查看)
~AutoFooter:
- footer.y = contentsize.height + △ (其中△是一些inset信息).
- 之后的刷新也和下拉是一致的內容.
六翎朱、常見的錯誤?:
問題出現(xiàn)點:在上拉刷新的時候橄维,根據(jù)有沒有更多數(shù)據(jù),來更改state拴曲。
代碼:
if pullup {
self.endFooterRefreshing(noMoreData: noMoreData)
}
else {
self.tableView.mj_header.endRefreshing()
}
private func endFooterRefreshing(noMoreData: Bool) {
if noMoreData {
self.tableView.mj_footer.endRefreshingWithNoMoreData()
} else {
self.tableView.mj_footer.resetNoMoreData()
}
}
導致結果:當所有數(shù)據(jù)都加載完畢后争舞,重新下拉刷新,然后上拉加載時澈灼,由于footer處于nomoredata狀態(tài)竞川,而無法加載其他頁的數(shù)據(jù)。
分析:
從pullup代碼來看叁熔,當上拉加載完所有數(shù)據(jù)后委乌,將狀態(tài)更新為了nomoredata狀態(tài)。
當重新在該頁面進行下拉刷新時荣回,數(shù)據(jù)只加載page == 1 的數(shù)據(jù)遭贸,但是此時footer.state == nomoredata。
當要進行上拉加載page == 2 的數(shù)據(jù)時心软,由于state == nomoredata壕吹,在源碼中就直接結束了整個刷新的操作.-
源碼:
scrollViewContentOffsetDidChange:~BackFooter:
// 如果已全部加載,僅設置pullingPercent删铃,然后返回 if (self.state == MJRefreshStateNoMoreData) { self.pullingPercent = pullingPercent; return; }
~AutoFooter:
if (self.state != MJRefreshStateIdle || !self.automaticallyRefresh || self.mj_y == 0) return;
-
解決方案:
if pullup { self.tableView.endFooterRefreshing(noMoreData: noMoreData) } else { self.tableView.endHeaderRefreshing(noMoreData: noMoreData, count: newcount) } // 給scrollview類進行擴展 extension UIScrollView { /// 尾部停止刷新 /// /// - Parameter noMoreData: 下次刷新是否有新的數(shù)據(jù) func endFooterRefreshing(noMoreData: Bool) { guard mj_footer != nil else { return } if noMoreData { mj_footer.endRefreshingWithNoMoreData() } else { footerEndRefreshing(action: nil) } } /// 頭部停止刷新 /// /// - Parameter noMoreData: 下次刷新是否有新的數(shù)據(jù) /// - count: 返回的結果個數(shù) func endHeaderRefreshing(noMoreData: Bool, count: Int) { guard mj_header != nil else { return } mj_header.endRefreshing() updateFooterState(noMoreData: noMoreData, count: count) } /// 根據(jù)首次刷新數(shù)據(jù), 更新footer狀態(tài) /// /// - Parameter noMoreData: 下次刷新是否有新的數(shù)據(jù) /// - count: 返回的結果個數(shù) private func updateFooterState(noMoreData: Bool, count: Int) { guard mj_footer != nil else { return } if noMoreData { mj_footer.endRefreshingWithNoMoreData() if count == 0 { mj_footer.isHidden = true } } else { mj_footer.isHidden = false mj_footer.resetNoMoreData() } } }