MJRefresh is almost a must-have tripartite library for our development efforts, providing a very simple and practical solution for drag-and-drop callback events. Below is the official framework.The most commonly used default view classes are:

Tensile loading controls drop-down refresh control: MJRefreshNormalHeader: MJRefreshAutoNormalFooter, MJRefreshBackNormalFooter left slip load control: MJRefreshNormalTrailerCopy the code

These classes will be analyzed from the top down.

Public base class control

MJRefreshComponent

As you can see from the framework diagram, all views come from the same base class, MJRefreshComponent, which provides common attributes and events for subclasses, including:

  • Callback object and callback method
  • Drag state definition and control
  • Add listeners to events (control offset, content size, gesture state) via KVO (callback responses to subclasses)
  • Other:
    • Drag percentage
    • Automatically toggle transparency according to drag ratio

The MJRefreshComponent also provides the basic logical framework for subclasses:

View creation

1. Initialize - (instancetype)initWithFrame:(CGRect)frame{; } // 2. } // 3. The view is about to be added by the superview - (void)willMoveToSuperview:(UIView *)newSuperview{// scroll the record of the initial value of the view // update some values // listen for updates of events} // 4. (void)layoutSubviews{[self placeSubviews]; }Copy the code

Scroll view state callback

When the offset value is changed / / - (void) scrollViewContentOffsetDidChange: (NSDictionary *) change {} / / when content changes - size Change (void) scrollViewContentSizeDidChange: (NSDictionary *) {} / / - when click the gesture state change (void)scrollViewPanStateDidChange:(NSDictionary *)change{}Copy the code

state

- (void)setState:(MJRefreshState)state{; }Copy the code

Commonly used method

// Enter refresh state - (void)beginRefreshing{; } // End refresh state - (void)endRefreshing{; }Copy the code

other

SetAutoChangeAlpha :(BOOL)autoChangeAlpha{; } - (BOOL)isAutoChangeAlpha{; } - (void)setAutomaticallyChangeAlpha:(BOOL)automaticallyChangeAlpha{; } // set the transparency according to the drag progress - (void)setPullingPercent:(CGFloat)pullingPercent{; }Copy the code

Drop down refresh control (Header)

The drop-down refresh control contains four classes:

  • MJRefreshHeader
    • MJRefreshStateHeader
      • MJRefreshNormalHeader
    • MJRefreshGifHeader

MJRefreshHeader

The MJRefreshHeader class is a blank view that contains the full drop-down refresh logic, and the subclasses MJRefreshStateHeader and MJRefreshGifHeader just need to add some additional images and text to improve the experience and keep the code simple and readable.

The implementation process

1. Initialization

Create the view and set the height and position.

- (void)prepare { [super prepare]; } - (void)placeSubviews {[super placeSubviews]; // Set the Header position (y coordinate)}Copy the code
2. Offset change:- scrollViewContentSizeDidChange

When the user drag and drop rolling control, is the offset value is changed, will the callback – scrollViewContentOffsetDidChange (NSDictionary *) change method, perform the corresponding logic in different condition. If the scroll view has already rolled the Header out of the screen, subsequent logic is not processed.

– scrollViewContentOffsetDidChange: (NSDictionary *) change method, there are some key variable values, respectively is:

  • Current scroll offset: offsetY
  • Offset value happenOffsetY for the header control
  • Critical point to refresh: normal2pullingOffsetY

By comparing these variable values, you can calculate what state the drag action should be set to.

  • Control is being dragged
    • When the offset during drag is greater than the critical value and the original state is idle, set the state to about to refresh
    • When the drag offset is less than the critical value and the original state is about to refresh, resetting the state will be idle
  • Control is not dragged and the current state is free to refresh
    • Executes the method that starts the refresh
  • Control is not dragged and the critical point for performing a refresh callback is reached
If (self. The scrollView. IsDragging) {/ / if you are dragging the self. PullingPercent = pullingPercent; // When the offset is greater than the critical value and the original state is idle, If (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {// Change the state to be refreshed self.state = MJRefreshStatePulling; } // When the offset is less than the critical value and the original state is about to refresh, Else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {// Go to normal state self.state = MJRefreshStateIdle; Else if (self.state == MJRefreshStatePulling) {// Start refreshing [self beginRefreshing]; } else if (pullingPercent < 1) {self.pullingPercent = pullingPercent; }Copy the code

The -resetinset method is executed when the Header is in the MJRefreshStateRefreshing state and the control is still scrolling. The purpose of this method is to record the top margin value insetTDelta that needs to be adjusted after the refresh. At the same time, avoid the abnormal Layout rendering problem caused by the automatic scaling Cell refresh of CollectionView according to Autolayout and content.

3. Set the status
- (void)setState:(MJRefreshState)state{ _state = state; / / to join the home side column is the purpose of the setState: after completion of method calls, set the text to layout child controls MJRefreshDispatchAsyncOnMainQueue ([self setNeedsLayout]) }Copy the code

View refreshes are added to the main thread of the asynchronous queue in order to wait as long as possible for the space properties to be set before refreshing the layout.

4. Start refreshing

Run the -beginrefreshing method and set the status to MJRefreshStateRefreshing. The method call flow is as follows:

1. Start refreshing method call - (void)beginRefreshing{//... self.state = MJRefreshStateRefreshing; / /... } if (state == MJRefreshStateIdle) {if (state == MJRefreshStateIdle) {if (state == MJRefreshStateIdle) { / /... } else if (state == MJRefreshStateRefreshing) { [self headerRefreshingAction]; }} 3. Refresh action - (void) headerRefreshingAction {/ / main code [UIView animateWithDuration: MJRefreshFastAnimationDuration animations:^{ if (self.scrollView.panGestureRecognizer.state != UIGestureRecognizerStateCancelled) { CGFloat top = Self. ScrollViewOriginalInset. Top + self. Mj_h; / / increase the scroll area top self. The scrollView. Mj_insetT = top; / / set the scroll position CGPoint offset = self.scrollView.contentOffset; offset.y = -top; [self.scrollView setContentOffset:offset animated:NO]; } } completion:^(BOOL finished) { [self executeRefreshingCallback]; }]; }Copy the code

– The headerRefreshingAction method sets a new inset and offset for the scrollview so that the Header stays at the top of the scrollview and is used to display refreshing text animations and the like.

5. Finish refreshing

End Refresh Requires users to invoke the -endreFreshing method after time-consuming operations are completed. The method call flow is as follows:

1. The end of the refresh method calls - (void) endRefreshing {MJRefreshDispatchAsyncOnMainQueue (self. State = MJRefreshStateIdle;) } 2. SetState to idle - (void)setState:(MJRefreshState)state{MJRefreshCheckState if (state == MJRefreshStateIdle) {if (state == MJRefreshStateIdle) {if  (oldState ! = MJRefreshStateRefreshing) return; [self headerEndingAction]; } else if (state == MJRefreshStateRefreshing) { // ... }} 3. Execute end action - (void)headerEndingAction {; }Copy the code

– The headerEndingAction method resets the scrolling view’s inset to the value before the refresh state, hiding the header again

Pull-up load control (Footer)

The drop-down refresh control contains seven classes:

  • MJRefreshFooter
    • MJRefreshBackFooter
      • MJRefreshBackNormalFooter
      • MJRefreshBackGifFooter
    • MJRefreshAutoFooter
      • MJRefreshAutoNormalFooter
      • MJRefreshAutoGifFooter

MJRefreshFooter

The MJRefreshFooter class cannot be used directly, as it only defines a few basic properties and methods, such as constructors, initial controller height, and handling when no data is loaded.

The pull-up controls that can be used directly are two subclasses of MJRefreshBackFooter and MJRefreshAutoFooter. The differences between these two controls are as follows:

  • MJRefreshBackFooter: hidden outside the bottom boundary of the scroll view, the load operation is performed when you drag to the refresh threshold of the Footer and release your hand.
  • MJRefreshAutoFooterIf the contentSize is smaller than the size of the scroll view, the user can see the Footer control without scrolling. It refreshes when the user is in drag and reaches the Footer refresh threshold.

MJRefreshBackFooter

The implementation process

1. Initialization

When MJRefreshBackFooter will soon be joined the parent view, go – willMoveToSuperview: method, and in the method call – scrollViewContentSizeDidChange: method. This method takes the height of the superview and the height of the content of the superview, and uses the greater of the two as the ordinate value of the Footer, ensuring that the Footer is hidden right at the bottom of the view or content.

- (void)scrollViewContentSizeDidChange:(NSDictionary *)change { [super scrollViewContentSizeDidChange:change]; / / content height CGFloat contentHeight = self. The scrollView. Mj_contentH + self. IgnoredScrollViewContentInsetBottom; / / form highly CGFloat scrollHeight = self. The scrollView. Mj_h - self. ScrollViewOriginalInset. Top - self.scrollViewOriginalInset.bottom + self.ignoredScrollViewContentInsetBottom; // Set position and size self.mj_y = MAX(contentHeight, scrollHeight); }Copy the code
2. Offset change:- scrollViewContentSizeDidChange

When the user sliders to offset changes, will trigger the monitoring event — – scrollViewContentOffsetDidChange MJRefreshKeyPathContentOffset. MJRefreshBackFooter code logic and MJRefreshHeader in – scrollViewContentOffsetDidChange method is almost the same, there is no more. It is important to note that the threshold calculation of the MJRefreshBackFooter view takes into account the height difference between the content height and the scrolling view.

# pragma mark get scrollView content beyond the height of the view - (CGFloat) heightForContentBreakView {CGFloat h = self.scrollView.frame.size.height - self.scrollViewOriginalInset.bottom - self.scrollViewOriginalInset.top; return self.scrollView.contentSize.height - h; } # pragma mark just pull up refresh control contentOffset. See y - (CGFloat) happenOffsetY {CGFloat deltaH = [self heightForContentBreakView];  / / content and view the height difference of the if (deltaH > 0) {/ / content height > view high return deltaH - self. ScrollViewOriginalInset. The top; <} else {/ / content height view high return - self. ScrollViewOriginalInset. Top; }}Copy the code
3. Set the status

The main job of the -setState method of the MJRefreshBackFooter class is to update the corresponding offset and inset values for the scrolling view at the start and end of the refresh.

MJRefreshAutoFooter

The implementation process

1. Initialization

When MJRefreshAutoFooter is about to be added to the superview, the -WillMoveToSuperView: method is called. This method takes the content of the superview as the y value of the Footer, ensuring that the Footer is positioned right at the bottom of the content.

- (void)willMoveToSuperview:(UIView *)newSuperview { [super willMoveToSuperview:newSuperview]; If (newSuperview) {// New parent control if (self.hidden == NO) {self.scrollView.mj_insetb += self.mj_h; } // Set the position self.mj_y = _scrollView.mj_contenth; If (self.hidden == NO) {self.scrollView.mj_insetb -= self.mj_h; }}}Copy the code
2. Refresh the logic

MJRefreshAutoFooter controls are positioned close to the content, so there are two situations: 1. When the content height < control height, you can see the Footer directly at the bottom of the content. In this case, the load time is called after the user has released his grip.

- (void)scrollViewPanStateDidChange:(NSDictionary *)change { [super scrollViewPanStateDidChange:change]; if (self.state ! = MJRefreshStateIdle) return; UIGestureRecognizerState panState = _scrollView.panGestureRecognizer.state; Switch (panState) {/ / hand case UIGestureRecognizerStateEnded: {if (_scrollView.mj_insett + _scrollView.mj_contenth <= _scrollView.mj_h) {// Content < control height if (_scrollView.mj_offsety >= -_scrollView.mj_insett) {// Drag self.triggerByDrag = YES; [self beginRefreshing]; }} else {// Contents > Control height if (_scrollView.mj_offsety >= _scrollView.mj_contenth + _scrollView.mj_insetb - _scrollView.mj_h) { self.triggerByDrag = YES; [self beginRefreshing]; } } } break; case UIGestureRecognizerStateBegan: { [self resetTriggerTimes]; } break; default: break; }}Copy the code

2. When the content height is greater than or equal to the control height, you need to drag the view to the Footer load threshold, but you do not need to let go, as long as the scroll view offset exceeds the threshold, the loading method will be triggered.

- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change { [super scrollViewContentOffsetDidChange:change]; if (self.state ! = MJRefreshStateIdle || ! self.automaticallyRefresh || self.mj_y == 0) return; // When autoTriggerTimes is set to -1 (infinite loading while scrolling) // This method ensures that the view is still scrolling after dragging to let go, If (_scrollView.mj_insett + _scrollView.mj_contenth > _scrollView.mj_h) {// Content height - Control height + control bottom margin + Footer Height * Percentage - Footer Height // Content height - Control height + control bottom margin if (_scrollView.mj_offsety >= _scrollView.mj_contenth - _scrollView.mj_h + self.mj_h * self.triggerAutomaticallyRefreshPercent + _scrollView.mj_insetB - self.mj_h) { // Old = [change[@"old"] CGPointValue]; CGPoint new = [change[@"new"] CGPointValue]; if (new.y <= old.y) return; if (_scrollView.isDragging) { self.triggerByDrag = YES; } // Refresh [self beginRefreshing] only when the bottom refresh control is completely present; }}}Copy the code

As can be seen from the code, the conditions to meet the refresh are: drag offset ≥ content height – control height + control bottom margin + footer height * Refresh control exposure percentage – footer height When the refresh control exposure percentage is the default value 1. , the inequality can be simplified as: drag offset ≥ content height – control height + control bottom margin

3. Unlimited trigger

Another feature of MJRefreshAutoFooter is that it can trigger indefinitely. Developers can set the number of times the properties are automatically refreshed.

*/ @property (nonatomic) NSInteger autoTriggerTimes; */ @property (nonatomic) NSInteger autoTriggerTimes;Copy the code

When scrolling the view in the continuous rolling (content height control or higher level), will constantly call – scrollViewContentOffsetDidChange: method, thereby continuously while loading condition call – beginRefreshing method.

If (self.triggerByDrag && self.leftTriggerTimes <= 0 &&! self.unlimitedTrigger) { return; } [super beginRefreshing]; }Copy the code

If autoTriggerTimes == -1 is currently supported, the load task will be triggered when the view reaches its load threshold before the scrolling view stops scrolling.

4. Set the status

-beginRefreshing when triggered, state is set to MJRefreshStateRefreshing and a callback is performed to load data. Usually, we will be at the end of the load data of callback methods to call – endRefreshing or – endRefreshingWithNoMoreData method, State is set to either MJRefreshStateIdle or MJRefreshStateNoMoreData, which corresponds to the code in the -setState: method, and we can see that the infinite number of times triggered is controlled here.

- (void)setState:(MJRefreshState)state{MJRefreshCheckState if (state == MJRefreshStateRefreshing) {// perform the load data callback [self executeRefreshingCallback]; } else if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) { if (self.triggerByDrag) { if (! self.unlimitedTrigger) { self.leftTriggerTimes -= 1; } self.triggerByDrag = NO; } /** End refresh */ if (MJRefreshStateRefreshing == oldState) {// When paging is enabled, Set the animation and callback the if (self. The scrollView. PagingEnabled) {CGPoint offset = self. The scrollView. ContentOffset; offset.y -= self.scrollView.mj_insetB; [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{ self.scrollView.contentOffset = offset; if (self.endRefreshingAnimationBeginAction) { self.endRefreshingAnimationBeginAction(); } } completion:^(BOOL finished) { if (self.endRefreshingCompletionBlock) { self.endRefreshingCompletionBlock(); } }]; return; } / / end refresh callback if (self. EndRefreshingCompletionBlock) {self. EndRefreshingCompletionBlock (); }}}}Copy the code

Slide left loading controls (Trailer)

MJRefreshTrailer is implemented logically exactly the same as MJRefreshBackFooter, except that some parameters have been changed from vertical to horizontal.

State and Normal control

The main feature of the state-type control is the addition of different State prompt text and refresh time display. Normal controls add arrow ICONS and refresh animations to State controls.

Gif subclass controls

Gif-type controls can enhance the user experience by displaying beautiful animations when dragging and refreshing. The two main methods are:

- (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 { [self setImages:images duration:images.count * 0.1 forState: state]; }Copy the code

The refresh control will adjust its height according to the height of the picture, and it will automatically set the time of the animation to play the entire animation according to the number of frames in the case of no custom animation length.

Drag and drop the animation

The developer can use drag percentages to set the animation the user is dragging, by calculating the current drag percentage in the overall animation corresponding to a frame of the picture to get the approximate subscript.

// set the corresponding animation frames between Idle and Pulling states by dragging percentages - (void)setPullingPercent:(CGFloat)pullingPercent {[super setPullingPercent:pullingPercent]; NSArray *images = self.stateImages[@(MJRefreshStateIdle)]; if (self.state ! = MJRefreshStateIdle || images.count == 0) return; // Stop animation [self.gifView stopAnimating]; // Set the current image to be displayed. NSUInteger index = images.count * pullingPercent; if (index >= images.count) index = images.count - 1; self.gifView.image = images[index]; }Copy the code

Refresh the animation

The refresh animation is performed when the view state is “about to refresh” and “Refresh”, and is played frame by frame using UIImageView’s startAnimating for preconfigured groups of images, in an infinite loop by default.

- (void) setState (MJRefreshState) state {MJRefreshCheckState / / do things according to the state if state = = MJRefreshStatePulling | | state = = MJRefreshStateRefreshing) {// Animation of the state to be refreshed and refreshed NSArray *images = self.stateImages[@(state)]; if (images.count == 0) return; [self.gifView stopAnimating]; If (images.count == 1) {self.gifview.image = [images lastObject]; } else {/ / picture self. GifView. AnimationImages = images; self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue]; [self.gifView startAnimating]; }} else if (state == MJRefreshStateIdle) {// restrict state stopAnimating [self.gifview stopAnimating]; }}Copy the code

conclusion

The clean, uncluttering architecture of MJRefresh provides developers with a tremendous amount of extensibility, and the default controls are usually sufficient when there is no need for customization.