MJRefresh原理分析

8378 ワード

国内の多くの開発者はMJRefreshを選んでプルダウンのリフレッシュを実現して、最近私も彼のソースコードを読んで、ここで私が理解した実現の原理を分かち合います
ドロップダウン・リフレッシュの基本原理
  • 一般的なドロップダウン・リフレッシュはcontentInsetで実現され、tableViewがナビゲーション・バーの真下にある場合、彼のcontentInset.topは64であり、contentOffset.yは-64である.引き続きドロップダウンcontentInset.topは変わらず、contentOffset.yは小さくなり、アップcontentOffset.yは大きくなり、左上隅がスクリーンの左上隅に達するまで0になる.
  • デフォルトでは、tableViewがドロップダウンされ、手を離すと元の位置に戻ります.ドロップダウンリフレッシュコントロールは、tableViewの上に自分を置き、初期yを負数に設定するので、普段は表示されず、ドロップダウンの時だけ現れ、放して弾き返します.そしてloadingでは一時的にcontentInsetを大きくし、tableViewを押し下げることに相当します.すると、ドロップダウン・リフレッシュのコントロールが表示され、リフレッシュが完了した後、contentInsetを元の値に戻し、リバウンドの効果
  • を実現します.
    キーコード解析実装
  • インスタンスを作成するコードから
  •     [self.tableView addHeaderWithTarget:self action:@selector(loadNewData)];
     [self.tableView headerEndRefreshing];
    
  • これはtagetActionメソッドで、もう一つのblockメソッドがあり、メソッドには本物のheaderView初期化メソッド
  • と書かれています.
    - (void)addHeaderWithTarget:(id)target action:(SEL)action
    {
        [self addHeaderWithTarget:target action:action dateKey:nil];
    }
    
    - (void)addHeaderWithTarget:(id)target action:(SEL)action dateKey:(NSString*)dateKey
    {
        // 1.    header
        if (!self.header) {
            MJRefreshHeaderView *header = [MJRefreshHeaderView header];
            [self addSubview:header];
            self.header = header;
        }
        
        // 2.         
        self.header.beginRefreshingTaget = target;
        self.header.beginRefreshingAction = action;
        
        // 3.         key
        self.header.dateKey = dateKey;
    }
    
  • MJRefreshHeaderView *header = [MJRefreshHeaderView header]; ,この方法は最初の拡張点であり、具体的なヘッダにどのような属性があり、どのユーザーが設定したスタイルがここで設定されているかは、まだsuperView業者に追加されておらず、tableViewに掛ける行為もなく、次の呼び出しself.header = header;,ここで用いたテクニックは、UIscrollView+MJRefreshのcategoryを利用して、UIscrollViewに属性headerとfooterを追加し、set.getメソッドでは、
  • - (void)setHeader:(MJRefreshHeaderView *)header {
        [self willChangeValueForKey:@"MJRefreshHeaderViewKey"];
        objc_setAssociatedObject(self, &MJRefreshHeaderViewKey,
                                 header,
                                 OBJC_ASSOCIATION_ASSIGN);
        [self didChangeValueForKey:@"MJRefreshHeaderViewKey"];
    }
    
    - (MJRefreshHeaderView *)header {
        return objc_getAssociatedObject(self, &MJRefreshHeaderViewKey);
    }
    
  • ここでは関連オブジェクトのテクニック(AssociatedObject)を使用します.categoryは通常、インスタンス変数を直接追加することはできません.上のコードで、UIscrollViewのsubviewsにheaderを追加し、参照を保持します.しかしこのヘッダのフレームはまだ確定しておらず、ヘッダの位置とリスニング行為
  • を設定する行為もない.
  • [self addSubview:header];このコードを実行すると、次はヘッダのライフサイクルメソッド
    willMoveToSuperview
    に入ります.このメソッドは共通のベースクラスMJRefreshBaseViewで実現されます.これは基礎的な行為なので、共通のベースクラスに書くと、すべてのサブクラスが共有できます.
  • - (void)willMoveToSuperview:(UIView *)newSuperview
    {
        [super willMoveToSuperview:newSuperview];
        
        //      
        [self.superview removeObserver:self forKeyPath:MJRefreshContentOffset context:nil];
        
        if (newSuperview) { //      
            [newSuperview addObserver:self forKeyPath:MJRefreshContentOffset options:NSKeyValueObservingOptionNew context:nil];
            
            //     
            self.mj_width = newSuperview.mj_width;
            //     
            self.mj_x = 0;
            
            //   UIScrollView
            _scrollView = (UIScrollView *)newSuperview;
            //   UIScrollView    contentInset
            _scrollViewOriginalInset = _scrollView.contentInset;
        }
    }
    
  • 次にライフサイクルメソッドに入りますlayoutSubviews:
  • - (void)layoutSubviews
    {
        [super layoutSubviews];
        
        // 1.  
        CGFloat arrowX = self.mj_width * 0.5 - 100;
        self.arrowImage.center = CGPointMake(arrowX, self.mj_height * 0.5);
        
        // 2.   
        self.activityView.center = self.arrowImage.center;
    }
    
  • 上記のコードにより、コントロールの位置、および各subviewの位置が決定される.
  • 次はリスニングUIScrollViewcontentOffsetcontentSizeの変化であり、キーコードは以下の通りである:
  • #pragma mark -   UIScrollView contentOffset  
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    {
        //             
        if (!self.userInteractionEnabled || self.alpha <= 0.01 || self.hidden) return;
    
        //       ,    
        if (self.state == MJRefreshStateRefreshing) return;
    
        if ([MJRefreshContentOffset isEqualToString:keyPath]) {
            [self adjustStateWithContentOffset];
        }
    }
    
    /**
     *      
     */
    - (void)adjustStateWithContentOffset
    {
        //    contentOffset
        CGFloat currentOffsetY = self.scrollView.mj_contentOffsetY;
        //          offsetY
        CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
        
        //                ,    
        if (currentOffsetY >= happenOffsetY) return;
        
        if (self.scrollView.isDragging) {
            //               
            CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_height;
            
            if (self.state == MJRefreshStateNormal && currentOffsetY < normal2pullingOffsetY) {
                //         
                self.state = MJRefreshStatePulling;
            } else if (self.state == MJRefreshStatePulling && currentOffsetY >= normal2pullingOffsetY) {
                //       
                self.state = MJRefreshStateNormal;
            }
        } else if (self.state == MJRefreshStatePulling) {//      &&    
            //     
            self.state = MJRefreshStateRefreshing;
        }
    }
    
  • 次にステータスを設定する方法で、stateステータスの変化に応じて異なる動作を駆動します.setStateメソッドは2番目の拡張ポイントで、ここでMJRefreshCheckStateはマクロであり、親クラスのsetStateメソッドも呼び出されます.ドロップダウン時にcontentInsetを一時的に大きくし、headerを画面に残してcallback blockを呼び出します.終了後にcontentInsetを復元し、
  • - (void)setState:(MJRefreshState)state
    {
        // 0.     contentInset
        if (self.state != MJRefreshStateRefreshing) {
            _scrollViewOriginalInset = self.scrollView.contentInset;
        }
        // 1.        (     )
        if (self.state == state) return;
        
        // 2.   
        MJRefreshState oldState = self.state;
        
        // 3.    
        _state = state;
        
        // 4.           
        switch (state) {
      case MJRefreshStateNormal: //     
            {
                if (oldState == MJRefreshStateRefreshing) {
                    [UIView animateWithDuration:MJRefreshSlowAnimationDuration * 0.6 animations:^{
                        self.activityView.alpha = 0.0;
                    } completion:^(BOOL finished) {
                        //      
                        [self.activityView stopAnimating];
                        
                        //   alpha
                        self.activityView.alpha = 1.0;
                    }];
                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(MJRefreshSlowAnimationDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //      
                        //      normal
    //                    _state = MJRefreshStatePulling;
    //                    self.state = MJRefreshStateNormal;
                        //     
                        self.arrowImage.hidden = NO;
                        
                        //      
                        [self.activityView stopAnimating];
                        
                        //     
                        [self settingLabelText];
                    });
                    //     
                    return;
                } else {
                    //     
                    self.arrowImage.hidden = NO;
                    
                    //      
                    [self.activityView stopAnimating];
                }
       break;
            }
            case MJRefreshStatePulling:
                break;
      case MJRefreshStateRefreshing:
            {
                //      
       [self.activityView startAnimating];
                //     
       self.arrowImage.hidden = YES;
                
                //   
                if ([self.beginRefreshingTaget respondsToSelector:self.beginRefreshingAction]) {
                    msgSend(msgTarget(self.beginRefreshingTaget), self.beginRefreshingAction, self);
                }
                if (self.beginRefreshingCallback) {
                    self.beginRefreshingCallback();
                }
       break;
            }
            default:
                break;
     }
        
        // 5.    
        [self settingLabelText];
    }
    
  • ここまで、MJRefreshの原理分析の差は多くないが、その原理を分析することで、ドロップダウンリフレッシュのプロセスは、ドロップダウンリフレッシュコントロールを初期化する-Frameを設定する-コントロールに傍受を追加する-contentOffsetを監視する-contentOffsetを判断し、対応するコールバックを行う
  • ドロップダウン・リフレッシュ・コントロールを自分で書こうとする皆さんのお役に立てばと思います.