MJRefresh Principle Analysis, mjrefresh Principle

Source: Internet
Author: User

MJRefresh Principle Analysis, mjrefresh Principle

MJRefresh is a popular pull-down refresh control. To fix a BUG some time ago, I read its source code. This article summarizes the implementation principles.

Basic Principles of pull-down refresh

Most of the pull-down refresh controls are implemented using contentInset. By default, if the upper left corner of a UIScrollView is at the bottom of the navigation bar, its contentInset is 64 and contentOffset is-64. If you continue the drop-down, the contentOffset will become smaller and smaller. If you slide, the contentOffset will increase until the upper left corner reaches the upper left corner of the screen, and the contentOffset will be exactly 0.

By default, if you drop down a UIScrollView, the initial position (below the navigation bar) is displayed after you release it ). Most of the pull-down refresh controls place themselves above UIScrollView and start y is set to a negative number, so it is not displayed at ordinary times. It only appears when the drop-down is made, and it will pop up again. Then, when loading is performed, increase contentInset temporarily, which is equivalent to squeezing UIScrollView down. the pull-down and refresh controls are displayed. After refreshing, change contentInset back to the original value, rebound effect

Basically, this is also true for MJRefresh.

Create a drop-down refresh control instance

Start with the code for creating an instance:

MJRefreshNormalHeader *header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{    [myController loadCollectionDataNeedReset:YES withBlock:^{        [self.header endRefreshing];        [self reloadData];    }];}];

A factory method headerWithRefreshingBlock is called. This method is defined in the base class MJRefreshHeader of various header controls:

+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock{    MJRefreshHeader *cmp = [[self alloc] init];    cmp.refreshingBlock = refreshingBlock;    return cmp;}

The init method will be called. Because the init method is not defined in MJRefreshHeader, and its base class is defined in MJRefreshComponent, it will enter the initialization method of the base class:

-(Instancetype) initWithFrame :( CGRect) frame {if (self = [super initWithFrame: frame]) {// preparation [self prepare]; // The default value is normal self. state = MJRefreshStateIdle;} return self ;}

The key here is the prepare method, which isFirst extension pointThe specific attributes and styles of the header (including the native header provided by the Library and the custom header) are implemented in this method. The prepare method of each subclass calls the prepare method of the parent class. Therefore, during expansion, common attributes are written in the prepare method of the parent class, and specific attributes are written in the prepare method of the subclass. For example, let's take a look at the MJRefreshStateHeader:

-(Void) prepare {[super prepare]; // initialize the text [self setTitle: fail forState: MJRefreshStateIdle]; [self setTitle: fail forState: MJRefreshStatePulling]; [self setTitle: MJRefreshHeaderRefreshingText forState: MJRefreshStateRefreshing];}

In short, after calling the headerWithRefreshingBlock method, a UIView instance is obtained, that is, the pull-down refresh control. But now it has not been mounted to any superview, and there is no behavior

Refresh the control from the drop-down list and mount it to UIScrollView.

Next call:

self.header = header;

This is a category in UIScrollView + MJRefresh, and adds the attribute header and footer for UIScrollView. AssociatedObject is used here, because category generally cannot directly add instance variables.

-(Void) setHeader :( MJRefreshHeader *) header {if (header! = Self. header) {// Delete the old one and add a new [self. header removeFromSuperview]; [self addSubview: header]; // store the new [self willChangeValueForKey: @ "header"]; // KVO objc_setAssociatedObject (self, & MJRefreshHeaderKey, header, OBJC_ASSOCIATION_ASSIGN); [self didChangeValueForKey: @ "header"]; // KVO }}

The above Code adds the header to the subviews of UIScrollView and retains a reference. However, the frame of this header has not been determined and there is no behavior.

Set the header location and listening behavior

Because addSubview is executed above, the header lifecycle method willMoveToSuperview will be entered. This method is implemented in the common base class MJRefreshComponent. Because this is a basic behavior, all subclasses can be shared in the common base class:

-(Void) willMoveToSuperview :( UIView *) newSuperview {[super willMoveToSuperview: newSuperview]; // if it is not UIScrollView, do not do anything if (newSuperview &&! [NewSuperview isKindOfClass: [UIScrollView class]) return; // The old parent control removes the listener [self removeObservers]; if (newSuperview) {// new parent control // set the width self. mj_w = newSuperview. mj_w; // set the position self. mj_x = 0; // record UIScrollView _ scrollView = (UIScrollView *) newSuperview; // The setting always supports Vertical Spring Effect _ scrollView. alwaysBounceVertical = YES; // record the first contentInset _ scrollViewOriginalInset = self in UIScrollView. scrollView. contentInset; // Add a listener [self addObservers];}

The key here is to set alwaysBounceVertical to ensure that UIScrollView can be pulled down. Otherwise, you need to process the contentSize to pull it, which is a lot of trouble. In addition, the header is also referenced by UIScrollView, and various attributes can be obtained later.

Next, add the listener method addObservers. Here we mainly use the KVO technique:

- (void)addObservers{    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil];    [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentSize options:options context:nil];    self.pan = self.scrollView.panGestureRecognizer;    [self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil];}
-(Void) observeValueForKeyPath :( NSString *) keyPath ofObject :( id) object change :( NSDictionary *) change context :( void *) context {// if (! Self. userInteractionEnabled | self. hidden) return; if ([keyPath isinclutostring: Custom]) {[self defined: change];} else if ([keyPath isinclutostring: MJRefreshKeyPathContentSize]) {[self scrollViewContentSizeDidChange: change];} else if ([keyPath isinclutostring: Custom]) {[self scrollViewContentInsetDidChange: change];} else if ([keyPath isinclutostring: Custom]) {[self scrollViewPanStateDidChange: change] ;}}

The changes of the three keys are monitored, including the contentOffset and contentSize of UIScrollView, and the status of the sliding gesture. Then, call several didChange methods when each value changes. These didChange methods are hook, yesSecond extension pointIn fact, they are all implemented by subclasses.

The following describes the lifecycle method layoutSubviews:

- (void)layoutSubviews{    [super layoutSubviews];    [self placeSubviews];}

Here placeSubviews is how to set the header, yesThird extension pointSet header origin. y to a negative value, which is implemented in the MJRefreshHeader method:

-(Void) placeSubviews {[super placeSubviews]; // set the y value (when your height changes, you must re-adjust Y value, so put it in the placeSubviews method to set the y value) self. mj_y =-self. mj_h ;}

The placeSubviews method of each subclass should call this method of the parent class first.

The code above determines the position of the pull-down refresh control and the position of each subview. It also listens on the changes in contentOffset and contentSize of UIScrollView.

Actual action when pulling down

The drop-down will cause the contentOffset to change. Because KVO listening has been added before, the scrollViewContentOffsetDidChange method will be executed:

-(Void) scrollViewContentOffsetDidChange :( NSDictionary *) change {[super scrollViewContentOffsetDidChange: change]; // if (self. state = MJRefreshStateRefreshing) {// sectionheader to resolve return;} // when redirected to the next controller, contentInset may change to _ scrollViewOriginalInset = self. scrollView. contentInset; // The current contentOffset CGFloat offsetY = self. scrollView. mj_offsetY; // offsetY CGFloat happenOffsetY =-self. scrollViewOriginalInset. top; // if you scroll up to the invisible header control, return the if (offsetY> = happenOffsetY) return; // normal and the critical point to be refreshed CGFloat normal2pullingOffsetY = happenOffsetY-self. mj_h; CGFloat pullingPercent = (happenOffsetY-offsetY)/self. mj_h; if (self. scrollView. isDragging) {// if you are dragging self. pullingPercent = pullingPercent; if (self. state = MJRefreshStateIdle & offsetY <normal2pullingOffsetY) {// The status is about to be refreshed. state = MJRefreshStatePulling;} else if (self. state = MJRefreshStatePulling & offsetY> = normal2pullingOffsetY) {// convert to normal state self. state = MJRefreshStateIdle;} else if (self. state = MJRefreshStatePulling) {// will be refreshed soon & release hands // start refreshing [self beginRefreshing];} else if (pullingPercent <1) {self. pullingPercent = pullingPercent ;}}

This code is long, mainly to determine whether the offset changes have reached the critical value, as well as the current gesture, switch the state of the header, and then drive different behaviors according to the state changes:

-(Void) setState :( MJRefreshState) state {MJRefreshCheckState // do things based on the state if (state = MJRefreshStateIdle) {if (oldState! = MJRefreshStateRefreshing) return; // Save the refresh time [[NSUserDefaults standardUserDefaults] setObject: [NSDate date] forKey: self. lastUpdatedTimeKey]; [[NSUserDefaults standardUserDefaults] synchronize]; // restore inset and offset [UIView animateWithDuration: MJRefreshSlowAnimationDuration animations: ^ {self. scrollView. mj_insetT-= self. mj_h; // automatically adjusts the transparency if (self. isAutoChangeAlpha) self. alpha = 0.0;} completion: ^ (BOOL finished) {self. pullingPercent = 0.0;}];} else if (state = MJRefreshStateRefreshing) {[UIView animateWithDuration: Refreshing animations: ^ {// Add a rolling area CGFloat top = self. scrollViewOriginalInset. top + self. mj_h; self. scrollView. mj_insetT = top; // sets the scroll position self. scrollView. mj_offsetY =-top;} completion: ^ (BOOL finished) {[self executeRefreshingCallback] ;}] ;}}

The setState method isFourth extension pointThe MJRefreshCheckState here is a macro and the setState method of the parent class is also called. Add contentInset temporarily during the drop-down, keep the header on the screen, and then call callback block. After the drop-down, restore contentInset.

Copyright Disclaimer: This article is an original article by the blogger and cannot be reproduced without the permission of the blogger.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.