閱讀源碼之路終於開啓了, 小白一枚, 大神們要多多照顧啊, 有什麼建議能夠評論或私信, 在此多謝了!!!!css
原文: http://www.jianshu.com/p/05af...html
做爲一個刷新框架, SVPullToRefresh以其簡潔, 通俗易懂爲你們所推崇. 對於剛開始讀源碼的我來講, 再合適不過了, 並且最近正在作一個刷新demo, 用到, 順便整理一下, 學習學習.
SV是個熟悉的前綴, 就算沒聽過SVPullToRefresh, 也聽過SVProgressHUD吧. 除了這些, 做者Sam還有其餘優秀的開源代碼, 你們感興趣能夠看看.git
下拉刷新ScrollViewgithub
@class SVPullToRefreshView; @interface UIScrollView (SVPullToRefresh) typedef NS_ENUM(NSUInteger, SVPullToRefreshPosition) { SVPullToRefreshPositionTop = 0, SVPullToRefreshPositionBottom, }; //默認添加方法, position爲top - (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler; /* 自定義添加下拉刷新的方法, 能夠改變刷新方式; top爲下拉刷新, bottom爲上拉刷新; */ - (void)addPullToRefreshWithActionHandler:(void (^)(void))actionHandler position:(SVPullToRefreshPosition)position; //觸發一次刷新, 會執行handler這個block裏面的方法 - (void)triggerPullToRefresh; //下拉刷新視圖 @property (nonatomic, strong, readonly) SVPullToRefreshView *pullToRefreshView; //是否展現下拉刷新視圖(須在addPullToRefreshWithActionHandler:方法後面) @property (nonatomic, assign) BOOL showsPullToRefresh; @end
下拉刷新Viewobjective-c
//只保留可更改選項 @interface SVPullToRefreshView : UIView //下拉刷新箭頭顏色 @property (nonatomic, strong) UIColor *arrowColor; //文本顏色 @property (nonatomic, strong) UIColor *textColor; //指示器view顏色 @property (nonatomic, strong, readwrite) UIColor *activityIndicatorViewColor NS_AVAILABLE_IOS(5_0); //指示器類型 @property (nonatomic, readwrite) UIActivityIndicatorViewStyle activityIndicatorViewStyle; //根據刷新狀態設置標題 - (void)setTitle:(NSString *)title forState:(SVPullToRefreshState)state; //根據刷新狀態設置副標題 - (void)setSubtitle:(NSString *)subtitle forState:(SVPullToRefreshState)state; //根據刷新狀態設置自定義View - (void)setCustomView:(UIView *)view forState:(SVPullToRefreshState)state; //開始動畫 - (void)startAnimating; //結束動畫 - (void)stopAnimating; //最後更新日期(NSDate) @property (nonatomic, strong) NSDate *lastUpdatedDate DEPRECATED_ATTRIBUTE; //日期格式(NSDateFormatter) @property (nonatomic, strong) NSDateFormatter *dateFormatter DEPRECATED_ATTRIBUTE; @end
?是我本身測試的, 把全部屬性玩了一遍, 親測好用, O(∩_∩)O哈哈~數組
[self.tableView addPullToRefreshWithActionHandler:^{ //下拉刷新數據 }]; self.tableView.pullToRefreshView.backgroundColor = RedColor; self.tableView.pullToRefreshView.arrowColor = [UIColor whiteColor]; self.tableView.pullToRefreshView.textColor = [UIColor whiteColor]; [self.tableView.pullToRefreshView setSubtitle:@"火之玉" forState:SVInfiniteScrollingStateLoading]; [self.tableView.pullToRefreshView setTitle:@"正在加載..waiting.." forState:SVInfiniteScrollingStateLoading]; self.tableView.pullToRefreshView.activityIndicatorViewColor = BlueColor; self.tableView.pullToRefreshView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge; // UIView *pullView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)]; // pullView.backgroundColor = [UIColor cyanColor]; // [self.tableView.pullToRefreshView setCustomView:pullView forState:SVPullToRefreshStateAll];
上拉刷新ScrollViewapp
@class SVInfiniteScrollingView; @interface UIScrollView (SVInfiniteScrolling) //默認添加上拉刷新視圖 - (void)addInfiniteScrollingWithActionHandler:(void (^)(void))actionHandler; ////觸發一次刷新, 會執行handler這個block裏面的方法 - (void)triggerInfiniteScrolling; //上拉刷新視圖 @property (nonatomic, strong, readonly) SVInfiniteScrollingView *infiniteScrollingView; //是否展現上拉刷新視圖 @property (nonatomic, assign) BOOL showsInfiniteScrolling; @end
上拉刷新View框架
@interface SVInfiniteScrollingView : UIView //指示器類型 @property (nonatomic, readwrite) UIActivityIndicatorViewStyle activityIndicatorViewStyle; //刷新狀態 @property (nonatomic, readonly) SVInfiniteScrollingState state; //是否取消上拉加載 @property (nonatomic, readwrite) BOOL enabled; //根據刷新狀態設置自定義View - (void)setCustomView:(UIView *)view forState:(SVInfiniteScrollingState)state; //開始動畫 - (void)startAnimating; //結束動畫 - (void)stopAnimating; @end
附上測試效果:ide
// setup infinite scrolling [self.tableView addInfiniteScrollingWithActionHandler:^{ //上拉刷新數據 }]; self.tableView.infiniteScrollingView.backgroundColor = BlueColor; self.tableView.infiniteScrollingView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhite; // UIImageView *pullImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)]; // pullImageView.image = [UIImage imageNamed:@"avatar"]; // pullImageView.layer.cornerRadius = 25; // pullImageView.layer.masksToBounds = YES; // [self.tableView.infiniteScrollingView setCustomView:pullImageView forState:SVPullToRefreshStateAll]; // // self.tableView.infiniteScrollingView.enabled = NO;
以上是下拉刷新的主要流程圖, 接下來咱們就來扣扣細節;post
-(void)triggerPullToRefresh
觸發了一次刷新:如下是方法的內部實現;
- (void)triggerPullToRefresh { self.pullToRefreshView.state = SVPullToRefreshStateTriggered; [self.pullToRefreshView startAnimating]; }
看了一眼, 當時就懵了; 怎麼就這點兒代碼, 徹底看不出來啊, 彆着急, 接着一個個點進去看. 發現state屬性的setter方法裏面作了處理;
- (void)setState:(SVPullToRefreshState)newState { if(_state == newState) return; SVPullToRefreshState previousState = _state; _state = newState; [self setNeedsLayout]; [self layoutIfNeeded]; switch (newState) { case SVPullToRefreshStateAll: case SVPullToRefreshStateStopped: [self resetScrollViewContentInset]; break; case SVPullToRefreshStateTriggered: break; case SVPullToRefreshStateLoading: [self setScrollViewContentInsetForLoading]; if(previousState == SVPullToRefreshStateTriggered && pullToRefreshActionHandler) pullToRefreshActionHandler(); break; } }
接下來拆分一下:
self.pullToRefreshView.state = SVPullToRefreshStateTriggered;
執行完這步代碼, 執行一次-(void)setState:
, 以後break跳出;
[self.pullToRefreshView startAnimating];
這步pullToRefreshView
執行-(void)startAnimating
, 方法內部實現以下:
- (void)startAnimating{ ... self.state = SVPullToRefreshStateLoading; }
能夠看出以後又執行了一次-(void)setState:
, 這時previousState == SVPullToRefreshStateTriggered
條件知足, 執行infiniteScrollingHandler(), 也就執行了block裏面刷新數據的方法;
代碼以下:
static char UIScrollViewPullToRefreshView; - (void)setPullToRefreshView:(SVPullToRefreshView *)pullToRefreshView { [self willChangeValueForKey:@"SVPullToRefreshView"]; objc_setAssociatedObject(self, &UIScrollViewPullToRefreshView, pullToRefreshView, OBJC_ASSOCIATION_ASSIGN); [self didChangeValueForKey:@"SVPullToRefreshView"]; } - (SVPullToRefreshView *)pullToRefreshView { return objc_getAssociatedObject(self, &UIScrollViewPullToRefreshView); }
關於KVO:
從代碼中能夠看出willChangeValueForKey :
和didChangeValueForKey :
是KVO的一部分, 源文件的代碼爲:
@interface NSObject(NSKeyValueObserverNotification) - (void)willChangeValueForKey:(NSString *)key; - (void)didChangeValueForKey:(NSString *)key; @end
用了這兩個方法也就表明手動觸發了KVO, 這也爲了控制回調的調用時機, 在setPullToRefreshView:
中觸發. 而手動觸發的場景通常是不使用屬性,或重寫了setter,須要手動通知系統.
通常咱們是不須要用的, 好比@property
寫一個屬性, 系統會以某種方式在中間插入 wilChangeValueForKey: 、 didChangeValueForKey: 和 observeValueForKeyPath:ofObject:change:context: 的調用.
想要了解更多, 能夠看一下
KVO Programming Guide - Apple官方文檔
關於runtime:
這裏要知道這兩個方法:
//set objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy) //get objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key) //objc_AssociationPolicy類型說明 //關聯時採用的協議,有assign,retain,copy等協議,通常使用OBJC_ASSOCIATION_RETAIN_NONATOMIC typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */ OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. * The association is not made atomically. */ OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied. * The association is not made atomically. */ OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object. * The association is made atomically. */ OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied. * The association is made atomically. */ };
看完全部API, 其實也瞭解的差很少, 這樣就給scrollView增長了一個SVPullToRefreshView
類型的屬性;
這裏主要涉及scrollView的三個監聽, contentOffset
, contentSize
, frame
;frame
就不用說了, 說一下另外兩個;contentOffset
是scrollview當前顯示區域頂點相對於frame頂點的偏移量。能夠理解爲contentview的頂點相對於scrollerVIew的frame的偏移量;contentSize
是scrollview當前全部內容區域的大小;
順便提下contentInset
, 下面用到, 表示contentView.frame與scrollerView.frame的關係, 能夠類比於css裏的padding.
例如:
testScrollView.contentInset = UIEdgeInsetsMake(10, 10, 10, 10);
則testScrollView的top, left, bottom, right爲10;
好了, 如今到重頭戲了, 監聽如何執行的, 代碼以下:
#pragma mark - Observing - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if([keyPath isEqualToString:@"contentOffset"]) [self scrollViewDidScroll:[[change valueForKey:NSKeyValueChangeNewKey] CGPointValue]]; else if([keyPath isEqualToString:@"contentSize"]) { [self layoutSubviews]; CGFloat yOrigin; ... self.frame = CGRectMake(0, yOrigin, self.bounds.size.width, SVPullToRefreshViewHeight); } else if([keyPath isEqualToString:@"frame"]) [self layoutSubviews]; }
能夠理解爲當監聽contentOffset
改變時, scrollView滾動, 此時執行scrollViewDidScroll:
方法獲得此時的滾動state; 當爲contentSize
和frame
時, scrollView視圖發生變化, 此時執行layoutSubviews
從新加載視圖, 包括根據狀態改變視圖樣式, 都在這裏面執行;scrollViewDidScroll:
方法:
- (void)scrollViewDidScroll:(CGPoint)contentOffset { if(self.state != SVPullToRefreshStateLoading) { CGFloat scrollOffsetThreshold = 0; switch (self.position) { case SVPullToRefreshPositionTop: scrollOffsetThreshold = self.frame.origin.y - self.originalTopInset; break; case SVPullToRefreshPositionBottom: scrollOffsetThreshold = MAX(self.scrollView.contentSize.height - self.scrollView.bounds.size.height, 0.0f) + self.bounds.size.height + self.originalBottomInset; break; } if(!self.scrollView.isDragging && self.state == SVPullToRefreshStateTriggered) self.state = SVPullToRefreshStateLoading; else if(contentOffset.y < scrollOffsetThreshold && self.scrollView.isDragging && self.state == SVPullToRefreshStateStopped && self.position == SVPullToRefreshPositionTop) self.state = SVPullToRefreshStateTriggered; else if(contentOffset.y >= scrollOffsetThreshold && self.state != SVPullToRefreshStateStopped && self.position == SVPullToRefreshPositionTop) self.state = SVPullToRefreshStateStopped; else if(contentOffset.y > scrollOffsetThreshold && self.scrollView.isDragging && self.state == SVPullToRefreshStateStopped && self.position == SVPullToRefreshPositionBottom) self.state = SVPullToRefreshStateTriggered; else if(contentOffset.y <= scrollOffsetThreshold && self.state != SVPullToRefreshStateStopped && self.position == SVPullToRefreshPositionBottom) self.state = SVPullToRefreshStateStopped; } else { CGFloat offset; UIEdgeInsets contentInset; switch (self.position) { case SVPullToRefreshPositionTop: ... self.scrollView.contentInset = UIEdgeInsetsMake(offset, contentInset.left, contentInset.bottom, contentInset.right); break; case SVPullToRefreshPositionBottom: if (self.scrollView.contentSize.height >= self.scrollView.bounds.size.height) { ... self.scrollView.contentInset = UIEdgeInsetsMake(contentInset.top, contentInset.left, offset, contentInset.right); } else if (self.wasTriggeredByUser) { ... self.scrollView.contentInset = UIEdgeInsetsMake(-offset, contentInset.left, contentInset.bottom, contentInset.right); } break; } } }
能夠看出根據postion位置, state是SVPullToRefreshStateLoading狀態的時候, 改變scrollView的contentInset;非該狀態的時候, 根據contentOffset和postion設置state;
下拉刷新後, scrollView的內容高度下移60; 固然只是內容高度, 整個scrollView仍是全屏的, 對比css中padding理解一下;
注:SVPullToRefreshView
向右移動一點兒距離, 方便看視圖層級;
layoutSubviews
方法:
- (void)layoutSubviews { for(id otherView in self.viewForState) { if([otherView isKindOfClass:[UIView class]]) //從父視圖剝離 [otherView removeFromSuperview]; } id customView = [self.viewForState objectAtIndex:self.state]; BOOL hasCustomView = [customView isKindOfClass:[UIView class]]; self.titleLabel.hidden = hasCustomView; self.subtitleLabel.hidden = hasCustomView; self.arrow.hidden = hasCustomView; if(hasCustomView) { //添加customView [self addSubview:customView]; ... [customView setFrame:CGRectMake(origin.x, origin.y, viewBounds.size.width, viewBounds.size.height)]; } else { //根據state旋轉arrowView switch (self.state) { case SVPullToRefreshStateAll: case SVPullToRefreshStateStopped: self.arrow.alpha = 1; [self.activityIndicatorView stopAnimating]; switch (self.position) { case SVPullToRefreshPositionTop: [self rotateArrow:0 hide:NO]; break; case SVPullToRefreshPositionBottom: [self rotateArrow:(float)M_PI hide:NO]; break; } break; ... } CGFloat leftViewWidth = MAX(self.arrow.bounds.size.width,self.activityIndicatorView.bounds.size.width); ... CGFloat labelX = (self.bounds.size.width / 2) - (totalMaxWidth / 2) + leftViewWidth + margin; if(subtitleSize.height > 0){ ... self.titleLabel.frame = CGRectIntegral(CGRectMake(labelX, titleY, titleSize.width, titleSize.height)); self.subtitleLabel.frame = CGRectIntegral(CGRectMake(labelX, titleY + titleSize.height + marginY, subtitleSize.width, subtitleSize.height)); }else{ ... self.titleLabel.frame = CGRectIntegral(CGRectMake(labelX, titleY, titleSize.width, titleSize.height)); self.subtitleLabel.frame = CGRectIntegral(CGRectMake(labelX, titleY + titleSize.height + marginY, subtitleSize.width, subtitleSize.height)); } CGFloat arrowX = (self.bounds.size.width / 2) - (totalMaxWidth / 2) + (leftViewWidth - self.arrow.bounds.size.width) / 2; self.arrow.frame = CGRectMake(arrowX, (self.bounds.size.height / 2) - (self.arrow.bounds.size.height / 2), self.arrow.bounds.size.width, self.arrow.bounds.size.height); self.activityIndicatorView.center = self.arrow.center; } }
self.viewForState
爲一個可變數組, 裏面是並且根據狀態裝入相應state的customView, 首先從經過- (void)setCustomView:forState:
方法添加後removeFromSuperview
從父視圖剝離, 以後根據是否傳入了customView決定是否添加自定義視圖; 若是沒有customView則改變裏面arrowView的角度;最後都得改變titleLabel
, subtitleLabel
, arrow
, activityIndicatorView
的尺寸或位置;
值得注意的是裏面的一個方法暴露了demo的年紀, O(∩_∩)O哈哈~
- (CGSize)sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size lineBreakMode:(NSLineBreakMode)lineBreakMode NS_DEPRECATED_IOS(2_0, 7_0, "Use -boundingRectWithSize:options:attributes:context:") __TVOS_PROHIBITED;
7.0以後已取消, 難怪我感受沒見過這個方法. 哎, 仍是太年輕~
話很少說, 先看東西...(此話出自老羅語錄?)
- (void)setState:(SVInfiniteScrollingState)newState { if(_state == newState) return; SVInfiniteScrollingState previousState = _state; _state = newState; for(id otherView in self.viewForState) { if([otherView isKindOfClass:[UIView class]]) [otherView removeFromSuperview]; } id customView = [self.viewForState objectAtIndex:newState]; BOOL hasCustomView = [customView isKindOfClass:[UIView class]]; if(hasCustomView) { [self addSubview:customView]; ... [customView setFrame:CGRectMake(origin.x, origin.y, viewBounds.size.width, viewBounds.size.height)]; } else { ... [self.activityIndicatorView setFrame:CGRectMake(origin.x, origin.y, viewBounds.size.width, viewBounds.size.height)]; //根據狀態設置activityIndicatorView是否動畫 switch (newState) { case SVInfiniteScrollingStateStopped: [self.activityIndicatorView stopAnimating]; break; ... } } if(previousState == SVInfiniteScrollingStateTriggered && newState == SVInfiniteScrollingStateLoading && self.infiniteScrollingHandler && self.enabled) self.infiniteScrollingHandler(); }
上拉刷新相對於下拉, 少了不少東西, 也就簡單了一些, 重複性的就很少說了.值得注意的是, 對比下拉, 上拉把layoutSubviews
裏面東西放到setState:
裏面. 也是, 畢竟沒多少東西, 對比着下拉刷新來看;
此次閱讀源碼,能夠說收穫滿滿啊。之前以爲源碼閱讀是個比較枯燥的過程,但是當我把一個個問題解決了以後,成就感也慢慢累積,感受就是越讀越來勁兒。並且發現讀一遍是遠遠不夠的,每看了一遍都多少會有些收穫。慢慢的從(這個方法是幹什麼的)-->(爲何寫這個方法)-->(爲何寫在這裏),等等一些思考。在對做者稱讚?的同時也爲本身認識了這種方法而感到高興。
再接再礪!!!
加油?2017!!!
想要了解更多的內容, 能夠關注一下個人我的公衆號。該公衆號每一個工做日會有新聞推送,每週技術分享,博客更新會實時推送。作個有態度的iOS開發者,就從身邊的一件件小事兒作起。滿滿的正能量,有沒有?Do you get it?