李明杰老師的代表作之一MJRefresh可以說(shuō)是棒棒的,很多小伙伴都會(huì)在沒(méi)有什么特殊要求的情況下使用這個(gè)框架滔韵,簡(jiǎn)單好用,功能還豐富,我也是用了很久械巡,最近項(xiàng)目不是很忙,決定研究一下老師的設(shè)計(jì)思路库菲。
這個(gè)框架第一遍看的時(shí)候我是懵逼的琐鲁,一句代碼要跳轉(zhuǎn)很多類,看的暈頭轉(zhuǎn)向惭每,造成這個(gè)原因主要是因?yàn)槔罾蠋煹目蚣茉O(shè)計(jì)采用繼承的方式骨饿,有一個(gè)基類MJRefreshComponent,然后所以類都是繼承于它台腥,每個(gè)子類實(shí)現(xiàn)不同的功能宏赘,如此一層層繼承下去,這樣的做的好處方便擴(kuò)展黎侈,如果你想要自定義某些東西的時(shí)候察署,可以選擇一個(gè)子類繼承,然后在原有的基礎(chǔ)上加?xùn)|西峻汉,很方便贴汪。什么意思呢,比方說(shuō)A類實(shí)現(xiàn)的a功能休吠,B類繼承于A類扳埂,它實(shí)現(xiàn)了b功能,C類繼承于B類瘤礁,實(shí)現(xiàn)了c功能阳懂,那么現(xiàn)在創(chuàng)建一個(gè)D類,繼承于C類蔚携,那么這個(gè)D類同時(shí)擁有的a,b,c三個(gè)功能希太,如有你不想擁有c功能,這個(gè)時(shí)候你可以讓D類繼承于B類酝蜒,那么D類就只擁有a和b兩個(gè)功能誊辉,在此基礎(chǔ)上你可以自己再添加e,f,g等等功能,這樣做方便擴(kuò)展亡脑。我是這么理解的堕澄,如果有不對(duì)的地方邀跃,歡迎大家指正,共同學(xué)習(xí)蛙紫。那么下面進(jìn)入正題了拍屑。
基類MJRefreshComponent繼承與UIView,它主要做了哪么事呢唁毒?
1.定義了刷新的所有狀態(tài)蒜茴,一共6種
typedef NS_ENUM(NSInteger, MJRefreshState) {
??? /** 普通閑置狀態(tài) */
??? MJRefreshStateIdle = 1,
??? /** 松開就可以進(jìn)行刷新的狀態(tài) */
??? MJRefreshStatePulling,
??? /** 正在刷新中的狀態(tài) */
??? MJRefreshStateRefreshing,
??? /** 即將刷新的狀態(tài) */
??? MJRefreshStateWillRefresh,
??? /** 所有數(shù)據(jù)加載完畢,沒(méi)有更多的數(shù)據(jù)了 */
??? MJRefreshStateNoMoreData
}
2.定義了一些回調(diào)
/** 進(jìn)入刷新狀態(tài)的回調(diào) */
typedef void (^MJRefreshComponentRefreshingBlock)(void);
/** 開始刷新后的回調(diào)(進(jìn)入刷新狀態(tài)后的回調(diào)) */
typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)(void);
/** 結(jié)束刷新后的回調(diào) */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)(void);
3.初始化方法
- (instancetype)initWithFrame:(CGRect)frame
{
? ? if (self = [super initWithFrame:frame]) {
? ? ? ? // 準(zhǔn)備工作
? ? ? ? [self prepare];
???????? // 默認(rèn)是普通狀態(tài)
? ? ? ? self.state = MJRefreshStateIdle;
? ? }
? ? return self;
}
4.添加了對(duì)scrollView屬性的監(jiān)聽浆西,刷新功能主要是利用scrollView的三個(gè)屬性來(lái)實(shí)現(xiàn)的粉私, contentOffset:scrollView的偏移量,contentSize:內(nèi)容大小近零,contentInset:內(nèi)容距離上下左右的距離诺核,正在刷新的時(shí)候刷新控件會(huì)在頂部停留,刷新之后刷新控件會(huì)上移消失久信,這個(gè)效果就是通過(guò)設(shè)置contentInset的top來(lái)實(shí)現(xiàn)的窖杀,后面會(huì)具體說(shuō)道,如果對(duì)這三個(gè)屬性的用法不太了解入篮,最好先去寫個(gè)scorllView研究一下這三個(gè)屬性具體干嘛的陈瘦,不然后面的介紹可能會(huì)比較不好理解。
- (void)addObservers
{
? ? NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
? ? [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil];
? ? [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentSize options:options context:nil];
? ? self.pan = self.scrollView.panGestureRecognizer;
? ? [self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil];
}
5.留給子類實(shí)現(xiàn)的接口
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{}
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change{}
- (void)scrollViewPanStateDidChange:(NSDictionary *)change{}
6.所有子類都實(shí)現(xiàn)的一個(gè)方法潮售,所有子類根據(jù)不同的狀態(tài)做出不同的響應(yīng)都是通過(guò)- (void)setState:(MJRefreshState)state這個(gè)方法來(lái)實(shí)現(xiàn)的
- (void)setState:(MJRefreshState)state
{ _state = state;
// 加入主隊(duì)列的目的是等setState:方法調(diào)用完畢、設(shè)置完文字后再去布局子控件
dispatch_async(dispatch_get_main_queue(), ^{
? ? ? ? [self setNeedsLayout];
? ? });
}我們看一下這個(gè)方法里面就幾行代碼锅风,第一句_state = state;就是簡(jiǎn)單的賦值酥诽,然后是一個(gè)GCD知識(shí),開啟一個(gè)異步任務(wù)皱埠,將block添加到主隊(duì)列里肮帐,異步任務(wù)的特點(diǎn)就是不用等到它執(zhí)行完就可以執(zhí)行下面的任務(wù),所有代碼會(huì)繼續(xù)往下走边器,之后才執(zhí)行 [self setNeedsLayout];這句代碼训枢,這樣的做的目的是等setState:方法調(diào)用完畢、設(shè)置完文字后再去布局子控件忘巧。
7.提供了開始很暫停刷新的接口
- (void)beginRefreshing;
- (void)endRefreshing;
MJRefreshComponent這個(gè)類下面分出來(lái)兩條線MJRefreshHeader和MJRefreshFooter恒界,顧名思義一個(gè)是下來(lái)刷新,一個(gè)是上拉加載對(duì)應(yīng)的head和foot砚嘴。我們看一下MJRefreshHeader這個(gè)類都干嘛了十酣,MJRefreshFooter這個(gè)類對(duì)比著學(xué)習(xí)就行了涩拙。
MJRefreshHeade主要做了那些事?
1.設(shè)置了初始化方法
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
??? MJRefreshHeader *cmp = [[self alloc] init];
??? //cmp.backgroundColor = [UIColor blueColor];
??? cmp.refreshingBlock = refreshingBlock;
??? return cmp;
}
+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action
{
??? MJRefreshHeader *cmp = [[self alloc] init];
??? [cmp setRefreshingTarget:target refreshingAction:action];
??? return cmp;
}
2.設(shè)置了head的高度
- (void)placeSubviews
{[super placeSubviews];// 設(shè)置y值(當(dāng)自己的高度發(fā)生改變了耸采,肯定要重新調(diào)整Y值兴泥,所以放到placeSubviews方法中設(shè)置y值)
? ? self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
}
3.滑動(dòng)的回調(diào)方法- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change,只要偏移量發(fā)生變化就會(huì)掉這個(gè)方法虾宇,這個(gè)方法的綁定是在父類的kvo里面搓彻。具體代碼邏輯
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
? ? [super scrollViewContentOffsetDidChange:change];
// 在刷新的refreshing狀態(tài)
? ? if (self.state == MJRefreshStateRefreshing) {
? ? ? ? // 暫時(shí)保留
? ? ? ? if (self.window == nil) return;
???? // sectionheader停留解決
? ? ? ? CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;
? ? ? ? insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT;
? ? ? ? self.scrollView.mj_insetT = insetT;
??????? self.insetTDelta = _scrollViewOriginalInset.top - insetT;
? ? ? ? return;
? ? }
// 跳轉(zhuǎn)到下一個(gè)控制器時(shí),contentInset可能會(huì)變
? ? _scrollViewOriginalInset = self.scrollView.mj_inset;
?// 當(dāng)前的contentOffset
? ? CGFloat offsetY = self.scrollView.mj_offsetY;
? ? // 頭部控件剛好出現(xiàn)的offsetY
? ? CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
?? // 如果是向上滾動(dòng)到看不見(jiàn)頭部控件嘱朽,直接返回
? ? // >= -> >
? ? if (offsetY > happenOffsetY) return;
? ? // 普通 和 即將刷新 的臨界點(diǎn)
? ? CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
? ? CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
? ? if (self.scrollView.isDragging) { // 如果正在拖拽
? ? ? ? self.pullingPercent = pullingPercent;
? ? ? ? if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
? ? ? ? ? ? // 轉(zhuǎn)為即將刷新狀態(tài)
? ? ? ? ? ? self.state = MJRefreshStatePulling;
? ? ? ? } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
? ? ? ? ? ? // 轉(zhuǎn)為普通狀態(tài)
? ? ? ? ? ? self.state = MJRefreshStateIdle;
? ? ? ? }
? ? } else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開
? ? ? ? // 開始刷新
? ? ? ? [self beginRefreshing];
? ? } else if (pullingPercent < 1) {
? ? ? ? self.pullingPercent = pullingPercent;
? ? }
}
4.根據(jù)不同的狀態(tài)設(shè)置不同的效果還是調(diào)用- (void)setState:(MJRefreshState)state方法
- (void)setState:(MJRefreshState)state
{
? ? MJRefreshCheckState
? ? // 根據(jù)狀態(tài)做事情
? ? if (state == MJRefreshStateIdle) {
? ? ? ? if (oldState != MJRefreshStateRefreshing) return;
? ? ? ? // 保存刷新時(shí)間
? ? ? ? [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
? ? ? ? [[NSUserDefaults standardUserDefaults] synchronize];
? ? ? ? // 恢復(fù)inset和offset
? ? ? ? [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
? ? ? ? ? ? self.scrollView.mj_insetT += self.insetTDelta;// 自動(dòng)調(diào)整透明度
? ? ? ? ? ? if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
? ? ? ? } completion:^(BOOL finished) {
? ? ? ? ? ? self.pullingPercent = 0.0;
? ? ? ? ? ? if (self.endRefreshingCompletionBlock) {
? ? ? ? ? ? ? ? self.endRefreshingCompletionBlock();
? ? ? ? ? ? }
? ? ? ? }];
? ? } else if (state == MJRefreshStateRefreshing) {
? ? ? ? dispatch_async(dispatch_get_main_queue(), ^{
? ? ? ? ? ? [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
? ? ? ? ? ? ? ? CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
? ? ? ? ? ? ? ? // 增加滾動(dòng)區(qū)域top
? ? ? ? ? ? ? ? self.scrollView.mj_insetT = top;
? ? ? ? ? ? ? ? // 設(shè)置滾動(dòng)位置
? ? ? ? ? ? ? ? CGPoint offset = self.scrollView.contentOffset;
? ? ? ? ? ? ? ? offset.y = -top;
? ? ? ? ? ? ? ? [self.scrollView setContentOffset:offset animated:NO];
? ? ? ? ? ? } completion:^(BOOL finished) {
? ? ? ? ? ? ? ? [self executeRefreshingCallback];
? ? ? ? ? ? }];
? ? ? ? });
? ? }
}
接下來(lái)就到了MJRefreshStateHeader這個(gè)類
MJRefreshStateHeader繼承于MJRefreshHeader旭贬,它的作用是給刷新控件添加了兩個(gè)label,一個(gè)顯示刷新狀態(tài)燥翅,一個(gè)顯示上次刷新時(shí)間骑篙,具體看一下里面的代碼。
1.首先將所有狀態(tài)對(duì)應(yīng)的文字添加到字典stateTitles中森书,這樣做的目的是根據(jù)不同的狀態(tài)取出對(duì)應(yīng)的文字內(nèi)容靶端。
? ? // 初始化文字
? ? [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderIdleText] forState:MJRefreshStateIdle];
? ? [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderPullingText] forState:MJRefreshStatePulling];
? ? [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderRefreshingText] forState:MJRefreshStateRefreshing];
- (void)setTitle:(NSString *)title forState:(MJRefreshState)state
{
? ? if (title == nil) return;
? ? self.stateTitles[@(state)] = title;
? ? self.stateLabel.text = self.stateTitles[@(self.state)];
}
2.關(guān)于更新時(shí)間的格式處理
- (void)setLastUpdatedTimeKey:(NSString *)lastUpdatedTimeKey
{
? ? [super setLastUpdatedTimeKey:lastUpdatedTimeKey];
? ? // 如果label隱藏了,就不用再處理
? ? if (self.lastUpdatedTimeLabel.hidden) return;
? ? NSDate *lastUpdatedTime = [[NSUserDefaults standardUserDefaults] objectForKey:lastUpdatedTimeKey];
? ? // 如果有block
? ? if (self.lastUpdatedTimeText) {
? ? ? ? self.lastUpdatedTimeLabel.text = self.lastUpdatedTimeText(lastUpdatedTime);
? ? ? ? return;
? ? }
? ? if (lastUpdatedTime) {
? ? ? ? // 1.獲得年月日
? ? ? ? NSCalendar *calendar = [self currentCalendar];
? ? ? ? NSUInteger unitFlags = NSCalendarUnitYear| NSCalendarUnitMonth | NSCalendarUnitDay |NSCalendarUnitHour |NSCalendarUnitMinute;
? ? ? ? NSDateComponents *cmp1 = [calendar components:unitFlags fromDate:lastUpdatedTime];
? ? ? ? NSDateComponents *cmp2 = [calendar components:unitFlags fromDate:[NSDate date]];
? ? ? ? // 2.格式化日期
? ? ? ? NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
? ? ? ? BOOL isToday = NO;
? ? ? ? if ([cmp1 day] == [cmp2 day]) { // 今天
? ? ? ? ? ? formatter.dateFormat = @" HH:mm";
? ? ? ? ? ? isToday = YES;
? ? ? ? } else if ([cmp1 year] == [cmp2 year]) { // 今年
? ? ? ? ? ? formatter.dateFormat = @"MM-dd HH:mm";
? ? ? ? } else {
? ? ? ? ? ? formatter.dateFormat = @"yyyy-MM-dd HH:mm";
? ? ? ? }
? ? ? ? NSString *time = [formatter stringFromDate:lastUpdatedTime];
? ? ? ? // 3.顯示日期
? ? ? ? self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@%@",
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? isToday ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderDateTodayText] : @"",
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? time];
? ? } else {
? ? ? ? self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@",
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderNoneLastDateText]];
? ? }
}這里面有一段代碼if (self.lastUpdatedTimeText) {
? ? ? ? self.lastUpdatedTimeLabel.text = self.lastUpdatedTimeText(lastUpdatedTime);
? ? ? ? return;
? ? }凛膏,這段代碼的作用是讓用戶可以自定義事件格式杨名,如果你不想用它的。
最后就是MJRefreshNormalHeader這個(gè)類了
MJRefreshNormalHeader繼承于MJRefreshStateHeader猖毫,它主要是給刷新視圖添加了箭頭指示和菊花台谍。
1.布局箭頭和菊花
- (void)placeSubviews
{
? ? [super placeSubviews];
? ? // 箭頭的中心點(diǎn)
? ? CGFloat arrowCenterX = self.mj_w * 0.5;
? ? if (!self.stateLabel.hidden) {
? ? ? ? CGFloat stateWidth = self.stateLabel.mj_textWith;
? ? ? ? CGFloat timeWidth = 0.0;
? ? ? ? if (!self.lastUpdatedTimeLabel.hidden) {
? ? ? ? ? ? timeWidth = self.lastUpdatedTimeLabel.mj_textWith;
? ? ? ? }
? ? ? ? CGFloat textWidth = MAX(stateWidth, timeWidth);
? ? ? ? arrowCenterX -= textWidth / 2 + self.labelLeftInset;
? ? }
? ? CGFloat arrowCenterY = self.mj_h * 0.5;
? ? CGPoint arrowCenter = CGPointMake(arrowCenterX, arrowCenterY);
? ? // 箭頭
? ? if (self.arrowView.constraints.count == 0) {
? ? ? ? self.arrowView.mj_size = self.arrowView.image.size;
? ? ? ? self.arrowView.center = arrowCenter;
? ? }
? ? // 圈圈
? ? if (self.loadingView.constraints.count == 0) {
? ? ? ? self.loadingView.center = arrowCenter;
? ? }
? self.arrowView.tintColor = self.stateLabel.textColor;
?}
2.根據(jù)不同的狀態(tài)顯示不同的效果
- (void)setState:(MJRefreshState)state
{
? ? MJRefreshCheckState
? ? // 根據(jù)狀態(tài)做事情
? ? if (state == MJRefreshStateIdle) {
? ? ? ? if (oldState == MJRefreshStateRefreshing) {
? ? ? ? ? ? self.arrowView.transform = CGAffineTransformIdentity;
? ? ? ? ? ? [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
? ? ? ? ? ? ? ? self.loadingView.alpha = 0.0;
? ? ? ? ? ? } completion:^(BOOL finished) {
? ? ? ? ? ? ? ? // 如果執(zhí)行完動(dòng)畫發(fā)現(xiàn)不是idle狀態(tài),就直接返回吁断,進(jìn)入其他狀態(tài)
? ? ? ? ? ? ? ? if (self.state != MJRefreshStateIdle) return;
? ? ? ? ? ? ? ? self.loadingView.alpha = 1.0;
? ? ? ? ? ? ? ? [self.loadingView stopAnimating];
? ? ? ? ? ? ? ? self.arrowView.hidden = NO;
? ? ? ? ? ? }];
? ? ? ? } else {
? ? ? ? ? ? [self.loadingView stopAnimating];
? ? ? ? ? ? self.arrowView.hidden = NO;
? ? ? ? ? ? [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
? ? ? ? ? ? ? ? self.arrowView.transform = CGAffineTransformIdentity;
? ? ? ? ? ? }];
? ? ? ? }
? ? } else if (state == MJRefreshStatePulling) {
? ? ? ? [self.loadingView stopAnimating];
? ? ? ? self.arrowView.hidden = NO;
? ? ? ? [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
? ? ? ? ? ? self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
? ? ? ? }];
? ? } else if (state == MJRefreshStateRefreshing) {
? ? ? ? self.loadingView.alpha = 1.0; // 防止refreshing -> idle的動(dòng)畫完畢動(dòng)作沒(méi)有被執(zhí)行
? ? ? ? [self.loadingView startAnimating];
? ? ? ? self.arrowView.hidden = YES;
? ? }
}
這就是整個(gè)下拉刷新大致的流程
MJRefreshComponent------>>>>MJRefreshHeader----->>>>>MJRefreshStateHeader----->>>>MJRefreshNormalHeader
通過(guò)繼承趁蕊,每個(gè)子類實(shí)現(xiàn)各自的功能,從而完成一個(gè)完整地刷新操作仔役。