MJRefresh is the work of Li Mingjie teacher, up to now has more than 11,000 stars, is a simple and practical, powerful iOS drop-down refresh (also support pull up to load more) control. It is highly customizable and can almost meet most of the design requirements of the drop-down refresh, so it is worth learning.

The framework structure is very clear, using a base class called MJRefreshComponent to do some basic Settings, and then inheriting MJRefreshHeader and MJRefreshFooter with pull-down refresh and pull-up loading capabilities, respectively. From the perspective of inheritance structure, it can be divided into three layers, as can be seen from the figure below:

First look at the control’s base class: MJRefreshComponent:

MJRefreshComponent

As the base class of the control, this class covers some of the basic classes: state, callback block, etc., roughly divided into the following five functions:

What are the functions?

  1. Declares all states of the control.
  2. Declares a callback function for the control.
  3. Add a listener.
  4. Provides refresh, stop refresh interface.
  5. Provides methods that subclasses need to implement.

How are functions implemented?

1. Declare all states of the control

/** Refresh the state of the control */
typedef NS_ENUM(NSInteger, MJRefreshState) {
    /** Normal idle state */
    MJRefreshStateIdle = 1./** State that can be refreshed when released */
    MJRefreshStatePulling,
    /** The state being refreshed */
    MJRefreshStateRefreshing,
    /** The state to be refreshed */
    MJRefreshStateWillRefresh,
    /** All data loaded, no more data */
    MJRefreshStateNoMoreData
};
Copy the code

2. Declare the control’s callback function

/** Enter the refresh state callback */
typedef void (^MJRefreshComponentRefreshingBlock)();
/** Callback after the start of the refresh (callback after entering the refresh state) */
typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)();
/** End the refresh callback */
typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)();
Copy the code

3. Add a listener

Listening statement:

- (void)addObservers
{
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil];/ / contentOffset property
    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentSize options:options context:nil];/ / contentSize property
    self.pan = self.scrollView.panGestureRecognizer;
    [self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil];// The state property of UIPanGestureRecognizer
}
Copy the code

Handling of listening:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    // Return directly in these cases
    if (!self.userInteractionEnabled) return;
    
    // This needs to be handled even if it is invisible
    if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) {
        [self scrollViewContentSizeDidChange:change];
    }

    / / see nothing
    if (self.hidden) return;
    
    if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) {
        [self scrollViewContentOffsetDidChange:change];
    } else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) {
        [selfscrollViewPanStateDidChange:change]; }}Copy the code

4. Refresh and stop the refresh interface

#pragma Mark enters the refresh state

- (void)beginRefreshingWithCompletionBlock:(void (^)())completionBlock
{
    self.beginRefreshingCompletionBlock = completionBlock;
    
    [self beginRefreshing];
}

- (void)beginRefreshing
{
    [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
        self.alpha = 1.0;
    }];
    self.pullingPercent = 1.0;
    // Display fully as long as you are refreshing
    if (self.window) {
        // Change the state to refresh
        self.state = MJRefreshStateRefreshing;
    } else {
        // Call this method in case the header inset fails to be backloaded while refreshing
        if (self.state ! = MJRefreshStateRefreshing) {// Change the state to refresh soon
            self.state = MJRefreshStateWillRefresh;
            // Refresh (in case you go back to this controller from another controller, refresh it again)
            [selfsetNeedsDisplay]; }}}#pragma mark ends the refresh state
- (void)endRefreshing
{
    self.state = MJRefreshStateIdle;
}

- (void)endRefreshingWithCompletionBlock:(void (^)())completionBlock
{
    self.endRefreshingCompletionBlock = completionBlock;
    
    [self endRefreshing];
}

Whether the #pragma mark is refreshing
- (BOOL)isRefreshing
{
    return self.state == MJRefreshStateRefreshing || self.state == MJRefreshStateWillRefresh;
}

Copy the code

Methods handed to subclasses to implement:

- (void)prepare NS_REQUIRES_SUPER;
/** Lays the child control frame */
- (void)placeSubviews NS_REQUIRES_SUPER;
/** Called when the contentOffset of scrollView has changed
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
*/ is called when the contentSize of the scrollView changes
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** * is called when the scrollView's drag state changes
- (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
Copy the code

5. Provide methods that subclasses need to implement

#pragma mark - Leave it to subclasses to implement
/** Initialize */
- (void)prepare NS_REQUIRES_SUPER;
/** Lays the child control frame */
- (void)placeSubviews NS_REQUIRES_SUPER;
/** Called when the contentOffset of scrollView has changed
- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
*/ is called when the contentSize of the scrollView changes
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
/** * is called when the scrollView's drag state changes
- (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER;
Copy the code

MJRefreshHeader footer and MJRefreshHeader footer footer

MJRefreshHeader

MJRefreshHeader inherits from MJRefreshComponent, which does these things:

What are the functions?

  1. Initialization.
  2. Set the header height.
  3. Rearrange the y value.
  4. According to thecontentOffsetTo switch the state (default state, can refresh state, refresh state), the implementation method is:scrollViewContentOffsetDidChange:.
  5. When switching the state, perform the corresponding operation. The implementation method is:setState:.

How are functions implemented?

# # # # 1. Initialization

There are two methods of initialization:

+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
    MJRefreshHeader *cmp = [[self alloc] init];
    / / the incoming block
    cmp.refreshingBlock = refreshingBlock;
    return cmp;
}
+ (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action
{
    MJRefreshHeader *cmp = [[self alloc] init];
    // Set self.refreshingTarget and self.refreshingAction
    [cmp setRefreshingTarget:target refreshingAction:action];
    return cmp;
}

Copy the code

2. Set the header height

Set the height of the header by overriding the prepare method:

- (void)prepare
{
    [super prepare];
    
    // Sets the key used to store times in NSUserDefaults
    self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
    
    // Set the header height
    self.mj_h = MJRefreshHeaderHeight;
}
Copy the code

3. Adjust the Y value

Resize y by overriding the placeSubviews method:

- (void)placeSubviews
{
    [super placeSubviews];
    
    // Set the y value (when your height changes, you must adjust the y value, so in the placeSubviews method to set the y value)
    self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
    / / self ignoredScrollViewContentInsetTop if is 10, then move up 10
}
Copy the code

4. Code for state switch:

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
{
    [super scrollViewContentOffsetDidChange:change];
    
    // The state is being refreshed
    if (self.state == MJRefreshStateRefreshing) {
        
        if (self.window == nil) return;
        
        // -self.scrollView.mj_offsety: - (-54-64) = 118: The offset is fixed when refreshed. Offset = status bar + navigation bar + Header height
        / / _scrollViewOriginalInset. Top: 64 (status bar + navigation bar)
        //insetT takes the one between the two
        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;
        
        / / set contentInset
        self.scrollView.mj_insetT = insetT;
        
        // Record the offset when refreshing -54 = 64-118
        self.insetTDelta = _scrollViewOriginalInset.top - insetT;
        
        return;
    }
    
    // contentInset may change when jumping to the next controller
     _scrollViewOriginalInset = self.scrollView.contentInset;
    
    // Record current contentOffset
    CGFloat offsetY = self.scrollView.mj_offsetY;

    // The header controls just all appear offsetY, default is -64 (20 + 44)
    CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
    
    // Scroll up to return directly
    if (offsetY > happenOffsetY) return;
    
    // Critical distance from normal to about to refresh
    CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;// -64 -54 = -118
    
    // Percentage of pull-down: Ratio of the pull-down distance to the header height
    CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
    
    if (self.scrollView.isDragging) {
        
        // Records the current drop down percentage
        self.pullingPercent = pullingPercent;
        
        if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
            // If the current default pull-down distance is greater than the critical distance (drop the tableView too low), then change the state to refresh
            self.state = MJRefreshStatePulling;
            
        } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
            // If the current state is refreshable and the pull-down distance is less than the critical distance, then the state is switched to default
            self.state = MJRefreshStateIdle; }}else if (self.state == MJRefreshStatePulling) {// About to refresh && release
        // Release && start refreshing when the state is refreshable (MJRefreshStatePulling)
        [self beginRefreshing];
        
    } else if (pullingPercent < 1) {
        // After release, restore self.pullingPercent by default
        self.pullingPercent = pullingPercent; }}Copy the code

Three points to note:

  1. There are three states: the default state (MJRefreshStateIdle), the state that can be refreshed (MJRefreshStatePulling) and the state that is being refreshed (MJRefreshStateRefreshing).
  2. There are two factors for state switching: one is whether the pull-down distance exceeds a critical value, and the other is whether the finger leaves the screen.
  3. Note: The state that can be refreshed is different from the state that is being refreshed. Because you can’t refresh while your finger is still attached to the screen. So even if the pull-down distance exceeds the critical distance (status bar + navigation bar + Header height), if the finger does not leave the screen, it cannot be refreshed immediately. Instead, the state changes to refresh. As soon as the finger leaves the screen, change the state to Refresh.

Here is a diagram to illustrate the differences between the three states:

5. Corresponding operations during state switchover:

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
   
    if (state == MJRefreshStateIdle) {
       
        //============== Set the status to the default =============//
        
        // Return if it is not currently flushing, since this method is aimed at flushing from oldstate to default
        if(oldState ! = MJRefreshStateRefreshing)return;
        
        // After the refresh is complete, save the time when the refresh is complete
        [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
        [[NSUserDefaults standardUserDefaults] synchronize];
        
        // Restore inset and offset
        [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
            
            //118 -> 64
            self.scrollView.mj_insetT += self.insetTDelta;
            
            // Automatically adjust transparency
            if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
            
        } completion:^(BOOL finished) {
            
            self.pullingPercent = 0.0;
            
            if (self.endRefreshingCompletionBlock) {
                // Call the refreshed block
                self.endRefreshingCompletionBlock(); }}]; }else if (state == MJRefreshStateRefreshing) {
        
         //============== Set the status to being refreshed =============//
         dispatch_async(dispatch_get_main_queue(), ^{
            
             [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
               
                CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;//64 + 54 (both default heights)
                // Reset contentInset, top = 118
                self.scrollView.mj_insetT = top;
                // Set the scroll position
                [self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO];
                 
            } completion:^(BOOL finished) {
                // Invoke the refreshed block
                [selfexecuteRefreshingCallback]; }]; }); }}Copy the code

Two points to note here:

  1. There are two types of state switching: default state and refreshing state. That is, for the start and end refresh of the two switch points.
  2. When you switch from the refreshing state to the default state (ending the refreshing), record the time when the refreshing ends. Because the header has a default label that shows when the last refresh was done.

MJRefreshStateHeader

This class, which is a subclass of the MJRefreshHeader class, does two things:

What are the functions?

  1. Simple layoutstateLabelandlastUpdatedTimeLabel.
  2. The text displayed by the two labels is toggled based on the control state (default state, refreshing state).

Here’s a diagram to give you an intuitive feel for these two controls:

How are functions implemented?

This class implements the above two implementations by overriding three methods of the parent class:

Method 1: Prepare method

- (void)prepare
{
    [super prepare];
    
    // Initialize the spacing
    self.labelLeftInset = MJRefreshLabelLeftInset;
    
    // Initialize the text
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderIdleText] forState:MJRefreshStateIdle];
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderPullingText] forState:MJRefreshStatePulling];
    [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderRefreshingText] forState:MJRefreshStateRefreshing];
}
Copy the code

In this case, the prompt text corresponding to each state is put into a dictionary. Key is the NSNumber form of the state

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

Method 2: placeSubviews method

- (void)placeSubviews
{
    [super placeSubviews];
    
    if (self.stateLabel.hidden) return;
    
    BOOL noConstrainsOnStatusLabel = self.stateLabel.constraints.count == 0;
    
    if (self.lastUpdatedTimeLabel.hidden) {
        
        // If the update time label is hidden, make the status label hold the entire header
        if (noConstrainsOnStatusLabel) self.stateLabel.frame = self.bounds;
        
    } else {
        
        // If the update time label is not hidden, set the update time label and status label according to the constraint (half height).
        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;
        }
        
        // Update time 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; }}}Copy the code

Here the layout is mainly for lastUpdatedTimeLabel and stateLabel. Note that lastUpdatedTimeLabel is hidden.

Method 3: setState: method

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // Set the status text
    self.stateLabel.text = self.stateTitles[@(state)];
    
    // Reset key (redisplay time)
    self.lastUpdatedTimeKey = self.lastUpdatedTimeKey;
}

Copy the code

Here, depending on the state passed in, switch the corresponding text between stateLabel and lastUpdatedTimeLabel.

  • stateLabelThe text is directly fromstateTitlesJust take it out of the dictionary.
  • lastUpdatedTimeLabelThere is a method to retrieve the text in:
- (void)setLastUpdatedTimeKey:(NSString *)lastUpdatedTimeKey
{
    [super setLastUpdatedTimeKey:lastUpdatedTimeKey];
    
    // If the label is hidden, no further processing is required
    if (self.lastUpdatedTimeLabel.hidden) return;
    
    // Get the corresponding NSData type times from NSUserDefaults based on the key
    NSDate *lastUpdatedTime = [[NSUserDefaults standardUserDefaults] objectForKey:lastUpdatedTimeKey];
    
    // If there is a block, take the time from the block, this should be the user custom display time format channel
    if (self.lastUpdatedTimeText) {
        self.lastUpdatedTimeLabel.text = self.lastUpdatedTimeText(lastUpdatedTime);
        return;
    }
    
    // If there is no block, the time format is displayed as the default below
    if (lastUpdatedTime) {
        
        // Get the last update
        // 1. Get the year, month and day
        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. Format the date
        NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
        BOOL isToday = NO;
        if ([cmp1 day] == [cmp2 day]) {
            // Today, leave out the year, month and day
            formatter.dateFormat = @" HH:mm";
            isToday = YES;
            
        } else if ([cmp1 year] == [cmp2 year]) { / / this year
            // This year, province last year, shows month and day
            formatter.dateFormat = @"MM-dd HH:mm";
        } else {
            // Other, year month day display
            formatter.dateFormat = @"yyyy-MM-dd HH:mm";
        }
        NSString *time = [formatter stringFromDate:lastUpdatedTime];
        
        // 3. Display the date
        self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@ % @ % @ % @ ""[NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
                                          isToday ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderDateTodayText] : @ "",
                                          time];
    } else {
        // Did not get the last update date (should be the first update or multiple updates, previous updates failed)
        self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@ % @ % @ ""[NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText],
                                          [NSBundlemj_localizedStringForKey:MJRefreshHeaderNoneLastDateText]]; }}Copy the code

Two points to note here:

  1. The author uses blocks to let the user define a realistic format for the date, or if the user does not customize it, use the default format provided by the author.
  2. In the default Settings, check whether it is today or this year. It can be used for reference when designing the labE that displays the time in the future.

MJRefreshNormalHeader

What are the functions?

MJRefreshNormalHeader inherits from MJRefreshStateHeader, which does two main things:

  1. It is added on the MJRefreshStateHeader_arrowViewandloadingView.
  2. The two views are laid out and styled when the state of the Refresh control switches.

Here’s a diagram to get a feel for the two views:

How are functions implemented?

As with MJRefreshStateHeader, the three methods of the parent class are overridden:

Method 1: Prepare

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

Method 2: placeSubviews

- (void)placeSubviews
{
    [super placeSubviews];
    
    // First set the center point x of the arrow to be half the width of the 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;
        }
        
        // Select the wider text width in stateLabel and the wider text width in update time
        CGFloat textWidth = MAX(stateWidth, timeWidth);
        // Move center point x to the left according to self.labelLeftInset and textWidth
        arrowCenterX -= textWidth / 2 + self.labelLeftInset;
    }
    
    // Center y is always set to half the height of the header
    CGFloat arrowCenterY = self.mj_h * 0.5;
    
    // Get the final Center, which works with both arrowView and loadingView, because they don't coexist.
    CGPoint arrowCenter = CGPointMake(arrowCenterX, arrowCenterY);
    
    / / arrow
    if (self.arrowView.constraints.count == 0) {
        // Control size equals image size
        self.arrowView.mj_size = self.arrowView.image.size;
        self.arrowView.center = arrowCenter;
    }
        
    / / the chrysanthemum
    if (self.loadingView.constraints.count == 0) {
        self.loadingView.center = arrowCenter;
    }
    
    //arrowView's color is the same as stateLabel's font color
    self.arrowView.tintColor = self.stateLabel.textColor;
}
Copy the code

A note here: Since stateLabel and lastUpdatedTimeLabel are side by side, and arrowView or loadingView are to the left of them, in order to avoid overlap between these two groups, when calculating the center of arrowView or loadingView, You need to get the width of stateLabel and lastUpdatedTimeLabel controls and compare the size. Use the larger one as the ‘widest distance’ between the two labels and calculate the center so that they do not overlap. As for how to calculate the width, the author gives a scheme that you can use in the future practice:

- (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;
}
Copy the code

Method 3: setState:

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    // Update the display of arrowView and loadingView based on the status
    if (state == MJRefreshStateIdle) {
       
        //1. Set it to the default state
        if (oldState == MJRefreshStateRefreshing) {
            
            //1.1 Switch from the Flushing state
            self.arrowView.transform = CGAffineTransformIdentity;
            
            [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{
                // Hide chrysanthemums
                self.loadingView.alpha = 0.0;
                
            } completion:^(BOOL finished) {
                
                // If the animation is not in idle state, go back to another state
                if (self.state ! = MJRefreshStateIdle)return;
                // Chrysanthemum stops spinning
                self.loadingView.alpha = 1.0;
                [self.loadingView stopAnimating];
                // Display arrows
                self.arrowView.hidden = NO;
            }];
            
        } else {
            //1.2 Switch from other states
            [self.loadingView stopAnimating];
            // Display the arrow and set it to its initial state
            self.arrowView.hidden = NO;
            [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
                self.arrowView.transform = CGAffineTransformIdentity; }]; }}else if (state == MJRefreshStatePulling) {
        
        //2. Set it to refresh
        [self.loadingView stopAnimating];
        self.arrowView.hidden = NO;
        [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
            // Arrow upside down
            self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI);
        }];
        
    } else if (state == MJRefreshStateRefreshing) {
        
        //3. Set the status to being refreshed
        self.loadingView.alpha = 1.0; // Prevent refreshing -> idle not executed after animation
        // Chrysanthemum rotation
        [self.loadingView startAnimating];
        / / hide arrowView
        self.arrowView.hidden = YES; }}Copy the code

So far, we have looked at the implementation of the MJRefreshComponent to MJRefreshNormalHeader. As you can see, the authors treat the prepare,placeSubviews, and setState: methods as base class methods, leaving the following subclasses to implement them layer by layer.

Subclasses of each layer implement these three methods in their own way according to their own responsibilities:

  • MJRefreshHeader: Is responsible for the height of the header and adjusting the external position of the header itself.
  • MJRefreshStateHeader: Is in charge of the headerstateLabelandlastUpdatedTimeLabelLayout and display of internal text in different states.
  • MJRefreshNormalHeader: Is in charge of the headerloadingViewAs well asarrowViewLayout and display in different states.

The nice thing about this is that if you want to add a header of a certain type, you just have to work on one layer. For example, the MJRefreshGifHeader in this framework belongs to the same level as MJRefreshNormalHeader and is derived from MJRefreshStateHeader. Since both have the same form of stateLabel and lastUpdatedTimeLabel, the only difference is the left part:

  • MJRefreshNormalHeaderTo the left is the arrow.
  • MJRefreshGifHeaderTo the left is a GIF animation.

Here’s a picture to get a feel for it:

Let’s take a look at the implementation:

MJRefreshGifHeader

It provides two interfaces for setting the array of images to be used in different states:

- (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state 
{ 
    if (images == nil) return; 
    
    // Set image groups and duration in different states
    self.stateImages[@(state)] = images; 
    self.stateDurations[@(state)] = @(duration); 
    
    /* Set the height of the control according to the picture */ 
    UIImage *image = [images firstObject]; 
    if (image.size.height > self.mj_h) { 
        self.mj_h = image.size.height; }} - (void)setImages:(NSArray *)images forState:(MJRefreshState)state 
{ 
   If duration is not passed, it is calculated based on the number of images
    [self setImages:images duration:images.count * 0.1 forState:state]; 
}
Copy the code

What are the functions?

Then, like MJRefreshNormalHeader, it overrides the three methods provided by the base class to display the GIF image.

How are functions implemented?

####1. Initialize and label spacing

- (void)prepare
{
    [super prepare];
    
    // Initialize the spacing
    self.labelLeftInset = 20;
}
Copy the code

####2. Set the GIF location based on the width and existence of the label

- (void)placeSubviews
{
    [super placeSubviews];
    
    // If the constraint exists, return immediately
    if (self.gifView.constraints.count) return;
    
    self.gifView.frame = self.bounds;
    
    if (self.stateLabel.hidden && self.lastUpdatedTimeLabel.hidden) {
        
        // If stateLabel and lastUpdatedTimeLabel are both hidden, GIF will be displayed
        self.gifView.contentMode = UIViewContentModeCenter;
        
    } else {
        
        // If at least one of stateLabel and lastUpdatedTimeLabel exists, the GIF position is set according to the width of the label
        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; }}Copy the code

3. Set the animation according to the incoming state

- (void)setState:(MJRefreshState)state
{
    MJRefreshCheckState
    
    if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) {
        
        //1. If the state passed in is refreshed and is being refreshed
        NSArray *images = self.stateImages[@(state)];
        if (images.count == 0) return;
        
        [self.gifView stopAnimating];
        
        if (images.count == 1) {
            //1
            self.gifView.image = [images lastObject];
        } else {
            // More than 1.2 images
            self.gifView.animationImages = images;
            self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue];
            [self.gifView startAnimating]; }}else if (state == MJRefreshStateIdle) {
        //2. If the state passed in is the default state
        [self.gifView stopAnimating]; }}Copy the code

The Footer class is used to handle pull-up loading. The implementation principle is very similar to that of pull-down refresh

Overall, the framework is very neat: a base class defines states and interfaces that need to be subclassed. Through layer by layer inheritance, let each layer of subclasses do their own jobs, only to complete their own tasks, improve the framework’s customizability, and for function expansion and bug tracking is also very helpful, it is worth our reference and reference.

This article has been synchronized to my blog: J_Knight MJRefresh source code analysis

— — — — — — — — — — — — — — — — — — — — — — — — — — — — on July 17, 2018 update — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Pay attention!!

The author recently opened a personal public account, mainly to share programming, reading notes, thinking articles.

  • Programming articles: including selected technical articles published by the author before, and subsequent technical articles (mainly original), and gradually away from iOS content, will shift the focus to improve the direction of programming ability.
  • Reading notes: Share reading notes on programming, thinking, psychology, and career books.
  • Thinking article: to share the author’s thinking on technology and life.

Because the number of messages released by the official account has a limit, so far not all the selected articles in the past have been published on the official account, and will be released gradually.

And because of the various restrictions of the major blog platform, the back will also be released on the public number of some short and concise, to see the big dry goods article oh ~

Scan the qr code of the official account below and click follow, looking forward to growing with you