分析MJRefresh框架搔体,并模擬上拉加載更多

先說下下拉刷新動畫效果的實現(xiàn)恨樟,重寫寫一個動畫的類,并且重寫prepare方法疚俱,在這里面添加UI劝术,并在placeSubviews方法中設置她的frame和坐標,因為placeSubviews方法是寫在layoutSubviews方法里面的。

- (void)prepare {
    
    [super prepare];
    [self addSubview:self.gifImageView];
    
}

- (void)placeSubviews {
    
    [super placeSubviews];
    CGFloat stateTextWidth = self.stateLabel.textWidth;
    CGFloat lastTimeTextWidth = self.lastUpdatedTimeLable.textWidth;
    CGFloat finalTextWidth = MAX(stateTextWidth, lastTimeTextWidth);
    _gifImageView.center = CGPointMake((self.eoc_w - finalTextWidth)/4, self.eoc_h/2-20.f);
    _gifImageView.image = [_stateImages[@(EOCRefreshStateIdle)] firstObject];
    _gifImageView.eoc_size = _gifImageView.image.size;
    
}

其次動畫是一幀一幀的养晋,我們根據下拉的比例來決定顯示哪張照片衬吆,因為GIF動畫其實是一組照片依次顯示出來的。

- (void)setPullingPercent:(CGFloat)pullingPercent {
    
    [super setPullingPercent:pullingPercent];
    NSArray *images = self.stateImages[@(EOCRefreshStateIdle)];
    if (self.state != EOCRefreshStateIdle || images.count == 0) return;
    // 停止動畫
    [self.gifImageView stopAnimating];
    // 設置當前需要顯示的圖片
    NSUInteger index =  images.count * pullingPercent;
    if (index >= images.count) index = images.count - 1;
    self.gifImageView.image = images[index];
    
}

最后在刷新和下拉狀態(tài)的時候開始動畫绳泉,在閑置狀態(tài)的時候結束動畫逊抡。

- (void)setState:(EOCRefreshState)state {
    [super setState:state];
    if (state == EOCRefreshStateRefreshing || state == EOCRefreshStatePulling) {
        _gifImageView.animationImages = _stateImages[@(EOCRefreshStateRefreshing)];
        _gifImageView.animationDuration = [_stateAnimationDurations[@(EOCRefreshStateRefreshing)] doubleValue];
        [_gifImageView startAnimating];
    } else if (state == EOCRefreshStateIdle) {
        [_gifImageView stopAnimating];
    }
}

上拉加載更多會涉及到ContentSize和GestureState的變化,所以基類里面增加了

- (void)scrollViewContentSizeDidChange:(NSDictionary *)change;
- (void)scrollViewGestureStateDidChange:(NSDictionary *)change;

這兩個公開的方法零酪,并且通過KVO進行監(jiān)聽秦忿。

- (void)willMoveToSuperview:(UIView *)newSuperview {
    //當self被添加到superView的時候,調用
    if (newSuperview && [newSuperview isKindOfClass:[UIScrollView class]]) {
        
        //非空蛾娶,而且是UIScrollView
        //同一個header被不同的table來添加的時候

       //這里的 self.superView 對應的ATableView
    if (self.superview && [self.superview isKindOfClass:[UIScrollView class]]) {
        
        UIScrollView *lastSuperView = (UIScrollView *)self.superview;
        [lastSuperView removeObserver:self forKeyPath:@"contentOffset"];
        [lastSuperView removeObserver:self forKeyPath:@"contentSize"];
        [lastSuperView.panGestureRecognizer removeObserver:self forKeyPath:@"state"];
    }
        
        self.scrollView = (UIScrollView *)newSuperview;
        self.originalScrollInsets = self.scrollView.contentInset;
        //控件還沒有設置frame
        self.eoc_x = 0.f;
        self.eoc_w = self.scrollView.eoc_w;
        
        //footer和header都繼承灯谣,這兩者的高度是不一樣
    
        [_scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
        
        [_scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
        
        [_scrollView.panGestureRecognizer addObserver:self forKeyPath:@"state" options:NSKeyValueObservingOptionNew context:nil];
    }
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    if ([keyPath isEqualToString:@"contentOffset"]) {
        [self scrollOffsetDidChange:change];
    } else if ([keyPath isEqualToString:@"contentSize"]) {
        [self scrollViewContentSizeDidChange:change];
    } else if ([keyPath isEqualToString:@"state"]) {
        [self scrollViewGestureStateDidChange:change];
    }
}

AutoFooter

一直都會,剛開始就會出現(xiàn)在tableView的底部

內容超過了一屏scrollView的大小的時候

這里的一屏并不一定是屏幕大小蛔琅,而是scrollView的frame大小胎许。這個時候,當yOffset大于內容高度減去scrollView本身高度后罗售,加上footer的高度的和就完全顯示出footer辜窑。


其中紅色為scrollView的frame,藍色為內容的大小寨躁,橙色為footer的大小穆碎,當滑動的距離超過下面紅色那段的時候就完全顯示出footer了。

- (void)scrollOffsetDidChange:(NSDictionary *)change {
    //如果內容超過了一屏scrollView的大小
    if (self.scrollView.eoc_h < self.scrollView.eoc_contentH + self.scrollView.eoc_insetT) {
        if (self.scrollView.contentOffset.y >= self.scrollView.eoc_contentH - self.scrollView.eoc_h + self.eoc_h) {
            //完全顯示出footer
            // 防止手松開時連續(xù)調用
            CGPoint old = [change[@"old"] CGPointValue];
            CGPoint new = [change[@"new"] CGPointValue];
            if (new.y <= old.y) return;  // 新的Y小于舊的Y說明职恳,往上拉動的距離不夠所禀,或者是footer向下離開屏幕的過程,這個時候直接返回放钦,不進行刷新
            self.state = EOCRefreshStateRefreshing;
        }
    }
}
注意1

設置scrollView的contentInset底部為footer的高色徘,即增加可視范圍,完全顯示出AutoFooter

- (void)willMoveToSuperview:(UIView *)newSuperview {
    
    [super willMoveToSuperview:newSuperview];
    
    if (newSuperview) {
        //設置scrollView的contentInset底部為footer的高操禀,即增加了滑動距離即可視范圍剛剛好為footer的高褂策,完全顯示出AutoFooter
        self.scrollView.eoc_insetB = self.eoc_h;
        self.eoc_y = self.scrollView.eoc_contentH;
    } else {  //self被移除掉
        //修改還原scrollView的contentInset
        self.scrollView.eoc_insetB = self.originalScrollInsets.bottom;
    }
}
注意2

footer的Y坐標需要專門在監(jiān)聽contentSize的方法中設置,因為只有有了contentSize的時候颓屑,才能設置在contentSize的底部斤寂,不然當contentSize為零的時候,就加載在tableView的頭部去了

- (void)scrollViewContentSizeDidChange:(NSDictionary *)change {
    //contentSize發(fā)生變化揪惦,一般是tableView發(fā)生變化
    self.eoc_y = self.scrollView.eoc_contentH;
}
內容沒有超過一屏scrollView的大小的時候

這個時候tableView是無法滾動的遍搞,需要來監(jiān)聽手勢,通過公開的手勢監(jiān)聽方法來實現(xiàn)

- (void)scrollViewGestureStateDidChange:(NSDictionary *)change {
    //如果在一屏的時候
    if (self.scrollView.eoc_h > self.scrollView.eoc_contentH + self.scrollView.eoc_insetT) {  // 內容小于一個屏幕時
         CGPoint transitionPoint = [self.scrollView.panGestureRecognizer translationInView:self.scrollView];
        if (transitionPoint.y < 0 && self.scrollView.panGestureRecognizer.state == UIGestureRecognizerStateEnded)
        {
          //往上拉丹擎,手勢不能動
            self.state = EOCRefreshStateRefreshing;
        }
    } else {  //超過一屏的時候
        if (self.scrollView.eoc_offsetY >= self.scrollView.eoc_contentH + self.scrollView.eoc_insetB - self.scrollView.eoc_h ) {
            self.state = EOCRefreshStateRefreshing;
        }
    }
}

BackFooter

需要向上拖動一定的距離才會顯現(xiàn)尾抑,并且刷新的時候停留在底部,當加載更多完了過后蒂培,就消失再愈。

注意1

其中Y坐標的設定分為兩種情況,一種是內容超過了scrollView的frame护戳,Y坐標應該緊跟著內容的后面翎冲,另一種是內容沒有超過scrollView的frame,Y坐標應該緊跟在scrollView的后面媳荒,如果有InsetTop值還要考慮減去它抗悍,因為它的坐標是從InsetTop下面才開始計算的,不然Y坐標會向下移動top的距離钳枕。

- (void)scrollViewContentSizeDidChange:(NSDictionary *)change {
    //內容和contentSize進行比對
    CGFloat contentSizeH = self.scrollView.eoc_contentH;
    //這里必須是_originalEdgeInsets
    CGFloat contentHeight = self.scrollView.eoc_h - self.originalScrollInsets.top - self.originalScrollInsets.bottom;  //減去eoc_insetT缴渊,是希望在上拉scrollView的時候就顯示出來,減去eoc_insetB鱼炒,是把eoc_footer變成內容區(qū)域塊
    self.eoc_y = MAX(contentSizeH, contentHeight);
}

上面紅色為手機屏幕大小衔沼,紫色為scrollView的大小,灰色為內容的大小昔瞧,青色為footer的大小指蚁,灰色和紫色之間為top值。

注意2

找臨界點自晰,即剛剛出現(xiàn)footer頭部時候的值
分為兩種情況:

  • 內容超過scrollView的frame凝化,即contentSize的H大于scrollView的H
    臨界值就等于contentSize的H減去scrollView的H
  • 內容小于scrollView的frame的時候,即為inset的Top值
- (CGFloat)boundaryOffset {
    
    //內容和contentSize進行比對
    CGFloat contentSizeH = self.scrollView.eoc_contentH;
    CGFloat contentHeight = self.scrollView.eoc_h - self.scrollView.eoc_insetT - self.scrollView.eoc_insetB;  //減去eoc_insetT酬荞,是希望在上拉scrollView的時候就顯示出來搓劫,減去eoc_insetB,是把eoc_footer變成內容區(qū)域塊
    CGFloat finalY = MAX(contentSizeH, contentHeight);
    if (finalY == contentSizeH) {
//        return _scrollView.eoc_contentH - _scrollView.eoc_h + _scrollView.eoc_insetB;
        return contentSizeH - self.scrollView.eoc_h;
    } else {
        return -self.scrollView.eoc_insetT;
    }
}
注意3

讓其在刷新的時候混巧,讓footer保持顯示糟把,刷新完成就消失

  • 內容小于scrollView的frame的時候

如上圖,原來的展示范圍只是到灰色框為止牲剃,而現(xiàn)在要是footer展示出來遣疯,所以要將展示的距離增加藍色的高度再加上footer的高度,如果原來還有bottom凿傅,還有加上原來的bottom缠犀,并且將這個新加的和設置為新的Inset的bottom的值,這樣footer就能夠展示了聪舒。

  • 內容大于scrollView的frame的時候
    要使footer完全顯示出來辨液,要將Inset的bottom的值設置為footer的高度,如果有原來的bottom箱残,還要加上原來的bottom值滔迈。
    并且還要使tableView滾動到最底部止吁,這樣才能看到footer,即要將Offset設置為原來算出來內容大于scrollView的frame時候的臨界值燎悍,即剛剛露出footer頭部敬惦,再加上footer的高度和新設置的Inset的bottom的值。

-(void)setState:(EOCRefreshState)state {
    [super setState:state];
    if (state == EOCRefreshStateRefreshing) {
        [UIView animateWithDuration:0.25f animations:^{
            CGFloat bottom = self.eoc_h + self.originalScrollInsets.bottom;
            CGFloat contentSizeH = self.scrollView.eoc_contentH;
            CGFloat contentHeight = self.scrollView.eoc_h - self.originalScrollInsets.top - self.originalScrollInsets.bottom;
            CGFloat deltaH = contentSizeH - contentHeight;
            
            if (deltaH < 0) { // 如果內容高度小于view的高度
                bottom -= deltaH;  // 因為deltaH < 0谈山,所以bottom -= deltaH 相當于加上了一個絕對值為deltaH的正值俄删,即可視范圍增加了deltaH的距離
            }
            self.scrollView.eoc_insetB = bottom;
            
            self.scrollView.eoc_offsetY = [self boundaryOffset] + self.eoc_h + self.scrollView.eoc_insetB;
        } completion:^(BOOL finished) {
             [self beginRefresh];
        }];
    } else if (state == EOCRefreshStateIdle || state == EOCRefreshStateNoMoreData) {
        [UIView animateWithDuration:0.25f animations:^{
            // 刷新完了過后,回到初始值奏路,即隱藏掉footer
            self.scrollView.eoc_insetB = self.originalScrollInsets.bottom;
        } completion:^(BOOL finished) {
        }];
    }
}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末畴椰,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子鸽粉,更是在濱河造成了極大的恐慌斜脂,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件触机,死亡現(xiàn)場離奇詭異秽褒,居然都是意外死亡,警方通過查閱死者的電腦和手機威兜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門销斟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人椒舵,你說我怎么就攤上這事蚂踊。” “怎么了笔宿?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵犁钟,是天一觀的道長。 經常有香客問我泼橘,道長涝动,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任炬灭,我火速辦了婚禮醋粟,結果婚禮上,老公的妹妹穿的比我還像新娘重归。我一直安慰自己米愿,他們只是感情好,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布鼻吮。 她就那樣靜靜地躺著育苟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪椎木。 梳的紋絲不亂的頭發(fā)上违柏,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天博烂,我揣著相機與錄音,去河邊找鬼漱竖。 笑死禽篱,一個胖子當著我的面吹牛,可吹牛的內容都是我干的闲孤。 我是一名探鬼主播谆级,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼烤礁,長吁一口氣:“原來是場噩夢啊……” “哼讼积!你這毒婦竟也來了?” 一聲冷哼從身側響起脚仔,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤勤众,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鲤脏,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體们颜,經...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年猎醇,在試婚紗的時候發(fā)現(xiàn)自己被綠了窥突。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡硫嘶,死狀恐怖阻问,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情沦疾,我是刑警寧澤称近,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站哮塞,受9級特大地震影響刨秆,放射性物質發(fā)生泄漏。R本人自食惡果不足惜忆畅,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一衡未、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧家凯,春花似錦眠屎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至驯镊,卻和暖如春葫督,著一層夾襖步出監(jiān)牢的瞬間竭鞍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工橄镜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留偎快,地道東北人。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓洽胶,卻偏偏與公主長得像晒夹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子姊氓,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內容