亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 學院 > 開發設計 > 正文

MJRefresh 源碼詳細解析

2019-11-09 15:16:53
字體:
來源:轉載
供稿:網友

MJRefresh是李明杰老師的作品,到現在已經有9800多顆star了,是一個簡單實用,功能強大的iOS下拉刷新(也支持上拉加載更多)控件。它的可定制性很高,幾乎可以滿足大部分下拉刷新的設計需求,值得學習。

該框架的結構設計得很清晰,使用一個基類MJRefreshComponent來做一些基本的設定,然后通過繼承的方式,讓MJRefreshHeaderMJRefreshFooter分別具備下拉刷新和上拉加載的功能。從繼承機構來看可以分為三層,具體可以從下面的圖里看出來:

框架組織結構圖

首先來看一下該控件的基類:MJRefreshComponent:

MJRefreshComponent

這個類作為該控件幾類,涵蓋了基類所具備的成份:狀態,回調block等,大致分成下面這5種職能:

有哪些職能?

聲明控件的所有狀態。聲明控件的回調函數。添加監聽。提供刷新,停止刷新接口。提供子類需要實現的方法。

職能如何實現?

1. 聲明控件的所有狀態

12345678910111213
/** 刷新控件的狀態 */typedef NS_ENUM(NSInteger, MJRefreshState) {    /** 普通閑置狀態 */    MJRefreshStateIdle = 1,    /** 松開就可以進行刷新的狀態 */    MJRefreshStatePulling,    /** 正在刷新中的狀態 */    MJRefreshStateRefreshing,    /** 即將刷新的狀態 */    MJRefreshStateWillRefresh,    /** 所有數據加載完畢,沒有更多的數據了 */    MJRefreshStateNoMoreData};

2. 聲明控件的回調函數

123456
/** 進入刷新狀態的回調 */typedef void (^MJRefreshComponentRefreshingBlock)();/** 開始刷新后的回調(進入刷新狀態后的回調) */typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)();/** 結束刷新后的回調 */typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)();

3. 添加監聽

監聽的聲明:

12345678
- (void)addObservers{    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil];//contentOffset屬性    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentSize options:options context:nil];//contentSize屬性    self.pan = self.scrollView.panGestureRecognizer;    [self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil];//UipanGestureRecognizer 的state屬性}

對于監聽的處理:

123456789101112131415161718
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{    // 遇到這些情況就直接返回    if (!self.userInteractionEnabled) return;        // 這個就算看不見也需要處理    if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) {        [self scrollViewContentSizeDidChange:change];    }    // 看不見    if (self.hidden) return;        if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) {        [self scrollViewContentOffsetDidChange:change];    } else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) {        [self scrollViewPanStateDidChange:change];    }}

4. 提供刷新,停止刷新接口

12345678910111213141516171819202122232425262728293031323334353637383940414243
#PRagma mark 進入刷新狀態- (void)beginRefreshingWithCompletionBlock:(void (^)())completionBlock{    self.beginRefreshingCompletionBlock = completionBlock;        [self beginRefreshing];}- (void)beginRefreshing{    [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{        self.alpha = 1.0;    }];    self.pullingPercent = 1.0;    // 只要正在刷新,就完全顯示    if (self.window) {        //將狀態切換為正在刷新        self.state = MJRefreshStateRefreshing;    } else {        // 預防正在刷新中時,調用本方法使得header inset回置失敗        if (self.state != MJRefreshStateRefreshing) {            //將狀態切換為即將刷新            self.state = MJRefreshStateWillRefresh;            // 刷新(預防從另一個控制器回到這個控制器的情況,回來要重新刷新一下)            [self setNeedsDisplay];        }    }}#pragma mark 結束刷新狀態- (void)endRefreshing{    self.state = MJRefreshStateIdle;}- (void)endRefreshingWithCompletionBlock:(void (^)())completionBlock{    self.endRefreshingCompletionBlock = completionBlock;        [self endRefreshing];}#pragma mark 是否正在刷新- (BOOL)isRefreshing{    return self.state == MJRefreshStateRefreshing || self.state == MJRefreshStateWillRefresh;}

交給子類實現的方法:

123456789
- (void)prepare NS_REQUIRES_SUPER;/** 擺放子控件frame */- (void)placeSubviews NS_REQUIRES_SUPER;/** 當scrollView的contentOffset發生改變的時候調用 */- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;/** 當scrollView的contentSize發生改變的時候調用 */- (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;/** 當scrollView的拖拽狀態發生改變的時候調用 */- (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;

5. 提供子類需要實現的方法

1234567891011
#pragma mark - 交給子類們去實現/** 初始化 */- (void)prepare NS_REQUIRES_SUPER;/** 擺放子控件frame */- (void)placeSubviews NS_REQUIRES_SUPER;/** 當scrollView的contentOffset發生改變的時候調用 */- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;/** 當scrollView的contentSize發生改變的時候調用 */- (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;/** 當scrollView的拖拽狀態發生改變的時候調用 */- (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;

從上面等結構圖可以看出,緊接著這個基類,下面分為MJRefreshHeaderMJRefreshFooter,這里順著MJRefreshHeader這個分支向下展開:

MJRefreshHeader

MJRefreshHeader繼承于MJRefreshComponent,它做了這幾件事:

有哪些職能?

初始化。設置header高度。重新調整y值。根據contentOffset的變化,來切換狀態(默認狀態,可以刷新的狀態,正在刷新的狀態),實現方法是:scrollViewContentOffsetDidChange:。在切換狀態時,執行相應的操作。實現方法是:setState:

職能如何實現?

####1. 初始化

初始化有兩種方法:

1234567891011121314
+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock{    MJRefreshHeader *cmp = [[self alloc] init];    //傳入block    cmp.refreshingBlock = refreshingBlock;    return cmp;}+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action{    MJRefreshHeader *cmp = [[self alloc] init];    //設置self.refreshingTarget 和 self.refreshingAction    [cmp setRefreshingTarget:target refreshingAction:action];    return cmp;}

2. 設置header高度

通過重寫prepare方法來設置header的高度:

12345678910
- (void)prepare{    [super prepare];        // 設置用于在NSUserDefaults里存儲時間的key    self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;        // 設置header的高度    self.mj_h = MJRefreshHeaderHeight;}

3. 重新調整y值

通過重寫placeSubviews方法來重新調整y值:

12345678
- (void)placeSubviews{    [super placeSubviews];        // 設置y值(當自己的高度發生改變了,肯定要重新調整Y值,所以放到placeSubviews方法中設置y值)    self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;    //self.ignoredScrollViewContentInsetTop 如果是10,那么就向上移動10}

4. 狀態切換的代碼:

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{    [super scrollViewContentOffsetDidChange:change];    // 正在刷新的狀態    if (self.state == MJRefreshStateRefreshing) {                if (self.window == nil) return;                //- self.scrollView.mj_offsetY:-(-54-64)= 118 : 刷新的時候,偏移量是不動的。偏移量 = 狀態欄 + 導航欄 + header的高度        //_scrollViewOriginalInset.top:64 (狀態欄 + 導航欄)        //insetT 取二者之間大的那一個        CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;               //118        insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT;                //設置contentInset        self.scrollView.mj_insetT = insetT;                // 記錄刷新的時候的偏移量 -54 = 64 - 118        self.insetTDelta = _scrollViewOriginalInset.top - insetT;        return;    }    // 跳轉到下一個控制器時,contentInset可能會變     _scrollViewOriginalInset = self.scrollView.contentInset;        // 記錄當前的contentOffset    CGFloat offsetY = self.scrollView.mj_offsetY;    // 頭部控件剛好全部出現的offsetY,默認是-64(20 + 44)    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;        // 向上滾動,直接返回    if (offsetY > happenOffsetY) return;        // 從普通 到 即將刷新 的臨界距離    CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;// -64 - 54 = -118        //下拉的百分比:下拉的距離與header高度的比值    CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;        if (self.scrollView.isDragging) {                //記錄當前下拉的百分比        self.pullingPercent = pullingPercent;                if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {            // 如果當前為默認狀態 && 下拉的距離大于臨界距離(將tableview下拉得很低),則將狀態切換為可以刷新            self.state = MJRefreshStatePulling;                    } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {            // 如果當前狀態為可以刷新 && 下拉的距離小于臨界距離,則將狀態切換為默認            self.state = MJRefreshStateIdle;        }    } else if (self.state == MJRefreshStatePulling) {// 即將刷新 && 手松開        // 手松開 && 狀態為可以刷新(MJRefreshStatePulling)時 開始刷新        [self beginRefreshing];            } else if (pullingPercent < 1) {        //手松開后,默認狀態時,恢復self.pullingPercent        self.pullingPercent = pullingPercent;            }}

需要注意三點:

這里的狀態有三種:默認狀態(MJRefreshStateIdle),可以刷新的狀態(MJRefreshStatePulling)以及正在刷新的狀態(MJRefreshStateRefreshing)。狀態切換的因素有兩個:一個是下拉的距離是否超過臨界值,另一個是 手指是否離開屏幕。注意:可以刷新的狀態和正在刷新的狀態是不同的。因為在手指還貼在屏幕的時候是不能進行刷新的。所以即使在下拉的距離超過了臨界距離(狀態欄 + 導航欄 + header高度),如果手指沒有離開屏幕,那么也不能馬上進行刷新,而是將狀態切換為:可以刷新。一旦手指離開了屏幕,馬上將狀態切換為正在刷新。 

這里提供一張圖來體現三個狀態的不同:三個狀態

5. 狀態切換時的相應操作:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
- (void)setState:(MJRefreshState)state{    MJRefreshCheckState       if (state == MJRefreshStateIdle) {               //============== 設置狀態為默認狀態 =============//                //如果當前不是正在刷新就返回,因為這個方法主要針對從正在刷新狀態(oldstate)到默認狀態        if (oldState != MJRefreshStateRefreshing) return;                //刷新完成后,保存刷新完成的時間        [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];        [[NSUserDefaults standardUserDefaults] synchronize];                // 恢復inset和offset        [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{                        //118 -> 64(剪去了header的高度)            self.scrollView.mj_insetT += self.insetTDelta;                        // 自動調整透明度            if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;                    } completion:^(BOOL finished) {                        self.pullingPercent = 0.0;                        if (self.endRefreshingCompletionBlock) {                //調用刷新完成的block                self.endRefreshingCompletionBlock();            }        }];            } else if (state == MJRefreshStateRefreshing) {                 //============== 設置狀態為正在刷新狀態 =============//         dispatch_async(dispatch_get_main_queue(), ^{                         [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{                               CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;//64 + 54 (都是默認的高度)                // 重新設置contentInset,top = 118                self.scrollView.mj_insetT = top;                // 設置滾動位置                [self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];                             } completion:^(BOOL finished) {                //調用進行刷新的block                [self executeRefreshingCallback];            }];         });    }}

這里需要注意兩點:

這里狀態的切換,主要圍繞著兩種:默認狀態和正在刷新狀態。也就是針對開始刷新和結束刷新這兩個切換點。從正在刷新狀態狀態切換為默認狀態時(結束刷新),需要記錄刷新結束的時間。因為header里面有一個默認的label是用來顯示上次刷新的時間的。

MJRefreshStateHeader

這個類是MJRefreshHeader類的子類,它做了兩件事:

有哪些職能?

簡單布局了stateLabellastUpdatedTimeLabel。根據控件狀態的切換(默認狀態,正在刷新狀態),實現了這兩個label顯示的文字的切換。

給一張圖,讓大家直觀感受一下這兩個控件:

兩個Label

職能如何實現?

這個類通過覆蓋父類三個方法來實現上述兩個實現:

方法1:prepare方法

123456789101112
- (void)prepare{    [super prepare];        // 初始化間距    self.labelLeftInset = MJRefreshLabelLeftInset;        // 初始化文字    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderIdleText] forState:MJRefreshStateIdle];    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderPullingText] forState:MJRefreshStatePulling];    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderRefreshingText] forState:MJRefreshStateRefreshing];}

在這里,將每一個狀態對應的提示文字放入一個字典里面,key是狀態的NSNumber形式

123456
- (void)setTitle:(NSString *)title forState:(MJRefreshState)state{    if (title == nil) return;    self.stateTitles[@(state)] = title;    self.stateLabel.text = self.stateTitles[@(self.state)];}

方法2:placeSubviews方法

12345678910111213141516171819202122232425262728293031323334
- (void)placeSubviews{    [super placeSubviews];        if (self.stateLabel.hidden) return;        BOOL noConstrainsOnStatusLabel = self.stateLabel.constraints.count == 0;        if (self.lastUpdatedTimeLabel.hidden) {                //如果更新時間label是隱藏的,則讓狀態label撐滿整個header        if (noConstrainsOnStatusLabel) self.stateLabel.frame = self.bounds;            } else {                //如果更新時間label不是隱藏的,根據約束設置更新時間label和狀態label(高度各占一半)        CGFloat stateLabelH = self.mj_h * 0.5;                if (noConstrainsOnStatusLabel) {            self.stateLabel.mj_x = 0;            self.stateLabel.mj_y = 0;            self.stateLabel.mj_w = self.mj_w;            self.stateLabel.mj_h = stateLabelH;        }                // 更新時間label        if (self.lastUpdatedTimeLabel.constraints.count == 0) {            self.lastUpdatedTimeLabel.mj_x = 0;            self.lastUpdatedTimeLabel.mj_y = stateLabelH;            self.lastUpdatedTimeLabel.mj_w = self.mj_w;            self.lastUpdatedTimeLabel.mj_h = self.mj_h - self.lastUpdatedTimeLabel.mj_y;        }    }}

這里主要是對lastUpdatedTimeLabelstateLabel進行布局。要注意lastUpdatedTimeLabel隱藏的情況。

方法3: setState:方法

12345678910
- (void)setState:(MJRefreshState)state{    MJRefreshCheckState        // 設置狀態文字    self.stateLabel.text = self.stateTitles[@(state)];        // 重新設置key(重新顯示時間)    self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;}

在這里,根據傳入的state的不同,在stateLabellastUpdatedTimeLabel里切換相應的文字。

stateLabel里的文字直接從stateTitles字典里取出即可。lastUpdatedTimeLabel里的文字需要通過一個方法來取出即可:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
- (void)setLastUpdatedTimeKey:(NSString *)lastUpdatedTimeKey{    [super setLastUpdatedTimeKey:lastUpdatedTimeKey];        // 如果label隱藏了,就不用再處理    if (self.lastUpdatedTimeLabel.hidden) return;        //根據key,從NSUserDefaults獲取對應的NSData型時間    NSDate *lastUpdatedTime = [[NSUserDefaults standardUserDefaults] objectForKey:lastUpdatedTimeKey];        // 如果有block,從block里拿來時間,這應該是用戶自定義顯示時間格式的渠道    if (self.lastUpdatedTimeText) {        self.lastUpdatedTimeLabel.text = self.lastUpdatedTimeText(lastUpdatedTime);        return;    }        //如果沒有block,就按照下面的默認方法顯示時間格式    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]];    }}

在這里注意兩點:

作者通過使用block來讓用戶自己定義日期現實的格式,如果用戶沒有自定義,就使用作者提供的默認格式。在默認格式的設置里,判斷了是否是今日,是否是今年的情況。在以后設計顯示時間的labe的時候可以借鑒一下。

MJRefreshNormalHeader

有哪些職能?

MJRefreshNormalHeader 繼承于 MJRefreshStateHeader,它主要做了兩件事:

它在MJRefreshStateHeader上添加了_arrowViewloadingView。布局了這兩個view并在Refresh控件的狀態切換的時候改變這兩個view的樣式。

還是給一張圖來直觀感受一下這兩個view:

兩個view

職能如何實現?

同MJRefreshStateHeader一樣,也是重寫了父類的三個方法:

方法1:prepare

123456
- (void)prepare{    [super prepare];        self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;}

方法2:placeSubviews

123456789101112131415161718192021222324252627282930313233343536373839404142
- (void)placeSubviews{    [super placeSubviews];        // 首先將箭頭的中心點x設為header寬度的一半    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;        }                //在stateLabel里的文字寬度和更新時間里的文字寬度里取較寬的        CGFloat textWidth = MAX(stateWidth, timeWidth);        //根據self.labelLeftInset和textWidth向左移動中心點x        arrowCenterX -= textWidth / 2 + self.labelLeftInset;    }        //中心點y永遠設置為header的高度的一半    CGFloat arrowCenterY = self.mj_h * 0.5;        //獲得了最終的center,而這個center同時適用于arrowView和loadingView,因為二者是不共存的。    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;    }        //arrowView的色調與stateLabel的字體顏色一致    self.arrowView.tintColor = self.stateLabel.textColor;}

在這里注意一點:因為stateLabellastUpdatedTimeLabel是上下并排分布的,而arrowViewloadingView是在這二者的左邊,所以為了避免這兩組重合,在計算arrowViewloadingView的center的時候,需要獲取stateLabellastUpdatedTimeLabel兩個控件的寬度并比較大小,將較大的一個作為兩個label的‘最寬距離’,再計算center,這樣一來就不會重合了。而對于如何計算寬度,作者給出了一個方案,大家可以在以后的實踐中使用:

12345678910111213141516171819
- (CGFloat)mj_textWith {    CGFloat stringWidth = 0;    CGSize size = CGSizeMake(MAXFLOAT, MAXFLOAT);    if (self.text.length > 0) {#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000        stringWidth =[self.text                      boundingRectWithSize:size                      options:NSStringDrawingUsesLineFragmentOrigin                      attributes:@{NSFontAttributeName:self.font}                      context:nil].size.width;#else                stringWidth = [self.text sizeWithFont:self.font                             constrainedToSize:size                                 lineBreakMode:NSLineBreakByCharWrapping].width;#endif    }    return stringWidth;}

方法3: setState:

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
- (void)setState:(MJRefreshState)state{    MJRefreshCheckState        // 根據狀態更新arrowView和loadingView的顯示    if (state == MJRefreshStateIdle) {               //1. 設置為默認狀態        if (oldState == MJRefreshStateRefreshing) {                        //1.1 從正在刷新狀態中切換過來            self.arrowView.transform = CGAffineTransformIdentity;                        [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{                //隱藏菊花                self.loadingView.alpha = 0.0;                            } completion:^(BOOL finished) {                                // 如果執行完動畫發現不是idle狀態,就直接返回,進入其他狀態                if (self.state != MJRefreshStateIdle) return;                //菊花停止旋轉                self.loadingView.alpha = 1.0;                [self.loadingView stopAnimating];                //顯示箭頭                self.arrowView.hidden = NO;            }];                    } else {            //1.2 從其他狀態中切換過來            [self.loadingView stopAnimating];            //顯示箭頭并設置為初始狀態            self.arrowView.hidden = NO;            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{                self.arrowView.transform = CGAffineTransformIdentity;            }];        }            } else if (state == MJRefreshStatePulling) {                //2. 設置為可以刷新狀態        [self.loadingView stopAnimating];        self.arrowView.hidden = NO;        [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{            //箭頭倒立            self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);        }];            } else if (state == MJRefreshStateRefreshing) {                //3. 設置為正在刷新狀態        self.loadingView.alpha = 1.0; // 防止refreshing -> idle的動畫完畢動作沒有被執行        //菊花旋轉        [self.loadingView startAnimating];        //隱藏arrowView        self.arrowView.hidden = YES;    }}

到此為止,我們已經從MJRefreshComponentMJRefreshNormalHeader的實現過程看了一遍??梢钥闯?,作者將prepare,placeSubviews以及setState:方法作為基類的方法,讓下面的子類去一層一層實現。

而每一層的子類,根據自身的職責,分別按照自己的方式來實現這三個方法:

MJRefreshHeader: 負責header的高度和調整header自身在外部的位置。MJRefreshStateHeader:負責header內部的stateLabellastUpdatedTimeLabel的布局和不同狀態下內部文字的顯示。MJRefreshNormalHeader:負責header內部的loadingView以及arrowView的布局和不同狀態下的顯示。

這樣做的好處是,如果想要增加某種類型的header,只要在某一層上做文章即可。例如該框架里的MJRefreshGifHeader,它和MJRefreshNormalHeader屬于同一級,都是繼承于MJRefreshStateHeader。因為二者都具有相同形式的stateLabellastUpdatedTimeLabel,唯一不同的就是左側的部分:

MJRefreshNormalHeader的左側是箭頭。MJRefreshGifHeader的左側則是一個gif動畫。

還是提供一張圖來直觀感受一下:normalHeader 與 gifHeader

下面我們來看一下的實現:

MJRefreshGifHeader

它提供了兩個接口,是用來設置不同狀態下使用的圖片數組的:

1234567891011121314151617181920
- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state {     if (images == nil) return;         //設置不同狀態下的圖片組和持續時間    self.stateImages[@(state)] = images;     self.stateDurations[@(state)] = @(duration);         /* 根據圖片設置控件的高度 */     UIImage *image = [images firstObject];     if (image.size.height > self.mj_h) {         self.mj_h = image.size.height;     } }- (void)setImages:(NSArray *)images forState:(MJRefreshState)state {    //如果沒有傳入duration,則根據圖片的多少來計算    [self setImages:images duration:images.count * 0.1 forState:state]; }

有哪些職能?

然后,和MJRefreshNormalHeader一樣,它也重寫了基類提供的三個方法來實現顯示gif圖片的職能。

職能如何實現?

####1. 初始化和label的間距

1234567
- (void)prepare{    [super prepare];        // 初始化間距    self.labelLeftInset = 20;}

####2. 根據label的寬度和存在與否設置gif的位置

12345678910111213141516171819202122232425262728
- (void)placeSubviews{    [super placeSubviews];        //如果約束存在,就立即返回    if (self.gifView.constraints.count) return;        self.gifView.frame = self.bounds;        if (self.stateLabel.hidden && self.lastUpdatedTimeLabel.hidden) {                //如果stateLabel和lastUpdatedTimeLabel都在隱藏狀態,將gif劇中顯示        self.gifView.contentMode = UIViewContentModeCenter;            } else {                //如果stateLabel和lastUpdatedTimeLabel中至少一個存在,則根據label的寬度設置gif的位置        self.gifView.contentMode = UIViewContentModeRight;                CGFloat stateWidth = self.stateLabel.mj_textWith;        CGFloat timeWidth = 0.0;        if (!self.lastUpdatedTimeLabel.hidden) {            timeWidth = self.lastUpdatedTimeLabel.mj_textWith;        }        CGFloat textWidth = MAX(stateWidth, timeWidth);        self.gifView.mj_w = self.mj_w * 0.5 - textWidth * 0.5 - self.labelLeftInset;    }}

3. 根據傳入狀態的不同來設置動畫

1234567891011121314151617181920212223242526
- (void)setState:(MJRefreshState)state{    MJRefreshCheckState        if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) {                //1. 如果傳進來的狀態是可以刷新和正在刷新        NSArray *images = self.stateImages[@(state)];        if (images.count == 0) return;                [self.gifView stopAnimating];                if (images.count == 1) {            //1.1 單張圖片            self.gifView.image = [images lastObject];        } else {            //1.2 多張圖片            self.gifView.animationImages = images;            self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue];            [self.gifView startAnimating];        }    } else if (state == MJRefreshStateIdle) {        //2.如果傳進來的狀態是默認狀態        [self.gifView stopAnimating];    }}

Footer類是用來處理上拉加載的,實現原理和下拉刷新很類似,在這里先不介紹了~

總的來說,該框架設計得非常工整:通過一個基類來定義一些狀態和一些需要子類實現的接口。通過一層一層地繼承,讓每一層的子類各司其職,只完成真正屬于自己的任務,提高了框架的可定制性,而且對于功能的擴展和bug的追蹤也很有幫助,非常值得我們參考與借鑒。


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
欧美另类极品videosbestfree| 亚洲区中文字幕| 欧美不卡视频一区发布| 在线国产精品视频| 欧美成人精品一区二区| 久久精品国产v日韩v亚洲| 亚洲精品国产成人| 国产精品网站入口| 日韩在线观看你懂的| 久久国产视频网站| 国产免费一区二区三区在线观看| 亚洲级视频在线观看免费1级| 久久国产精品电影| 国产在线观看不卡| 亚洲999一在线观看www| 国产精品日韩欧美| 国产精品jvid在线观看蜜臀| 日韩美女激情视频| 国产一区二区三区在线播放免费观看| 精品国产拍在线观看| 国产精品免费视频久久久| 日韩免费在线免费观看| 国产成人福利夜色影视| 欧美在线国产精品| 国产亚洲精品成人av久久ww| 国产精品高潮视频| 亚洲а∨天堂久久精品喷水| 中日韩午夜理伦电影免费| 亚洲欧美日韩高清| 国产精品永久免费在线| 欧美丝袜一区二区| 亚洲视频网站在线观看| 久久99亚洲精品| 日韩视频中文字幕| 亚洲精品乱码久久久久久按摩观| 色综合色综合久久综合频道88| 欧美亚洲激情在线| 国产精品日韩欧美综合| 国产精品网红直播| 亚洲精品自在久久| 成人午夜激情网| 色综合五月天导航| 一区二区欧美日韩视频| 国产成人精品在线| 欧美日韩另类在线| 国产精品99蜜臀久久不卡二区| 亚洲日本中文字幕免费在线不卡| 成人激情视频小说免费下载| 午夜精品久久久久久99热| 国产欧美精品xxxx另类| 国产精品福利网| 日韩美女主播视频| 国产日韩精品在线| 欧美视频在线看| 亚洲第一页中文字幕| 亚洲18私人小影院| 日韩在线中文字幕| 日韩av电影在线网| 欧美日韩一区二区免费在线观看| 日韩美女视频中文字幕| 精品免费在线视频| 成人在线视频网| 色综合伊人色综合网| 久久久久国色av免费观看性色| 中文字幕在线亚洲| 国产97色在线| 精品国产一区二区三区久久狼5月| 日韩av免费观影| 精品久久久久久久大神国产| 精品视频久久久久久久| 亚洲欧美另类中文字幕| 久久99视频精品| 久久九九国产精品怡红院| 日韩av影视综合网| 久久91精品国产91久久跳| 欧美黑人一级爽快片淫片高清| 97精品伊人久久久大香线蕉| 日韩电影第一页| 国模私拍一区二区三区| 国产精品一区二区三| 国产精品一区久久| 亚洲成人久久久久| 91网站在线免费观看| 欧美裸体xxxx极品少妇软件| 日韩av免费看网站| 久久久国产一区二区三区| 欧美性黄网官网| 亚洲大胆美女视频| 在线成人免费网站| 亚洲综合一区二区不卡| 欧美日韩激情小视频| 精品无人区太爽高潮在线播放| 日韩视频永久免费观看| 国产视频久久久久| 国产日韩av在线| 91a在线视频| 国产乱人伦真实精品视频| 久久全国免费视频| 亚洲欧美成人精品| 91午夜在线播放| 久久影院模特热| 日韩精品免费综合视频在线播放| 亚洲国产91精品在线观看| 国产精品成久久久久三级| 亚洲三级黄色在线观看| 日韩免费在线免费观看| 国产精品久久精品| 国产精品成人va在线观看| 日韩风俗一区 二区| 国产香蕉精品视频一区二区三区| 国产精品成人观看视频国产奇米| 国模私拍一区二区三区| 欧美亚洲国产成人精品| 97超碰国产精品女人人人爽| 国产偷国产偷亚洲清高网站| 亚洲成av人片在线观看香蕉| 亚洲成人在线视频播放| 亚洲999一在线观看www| 国产精品自产拍在线观| 97在线免费视频| 欧美与欧洲交xxxx免费观看| 欧美精品在线第一页| 日韩a**中文字幕| 欧美大片大片在线播放| 欧洲成人性视频| 亚洲欧洲在线播放| 久久视频国产精品免费视频在线| 欧美福利小视频| 欧美乱妇40p| 日韩在线观看免费高清完整版| 在线日韩日本国产亚洲| 91欧美视频网站| 亚洲综合视频1区| 2023亚洲男人天堂| 日韩av在线影视| 成人天堂噜噜噜| 美日韩精品免费视频| 亚洲永久在线观看| 欧美黄色三级网站| 97人人做人人爱| 日本一欧美一欧美一亚洲视频| 欧美性69xxxx肥| 日本老师69xxx| 日韩av综合网| 91精品国产乱码久久久久久久久| 国产美女精品免费电影| 一区二区三区日韩在线| 欧美情侣性视频| 一区二区福利视频| 97人人爽人人喊人人模波多| 黄色精品在线看| 欧美日韩成人免费| 精品视频在线播放免| 亚洲无av在线中文字幕| 亚洲第一国产精品| 国产精品一区二区三区久久久| 欧美亚洲伦理www| 色偷偷91综合久久噜噜| 国产精品久久综合av爱欲tv| 久久成人一区二区| 亚洲一区二区三区乱码aⅴ| 欧美一区二区三区艳史| 中文字幕亚洲综合久久| 91精品国产九九九久久久亚洲|