iOS 之 给scrollView 的四个方向加上拉和下拉加载(一)

794 阅读5分钟

一直想给scrollView加上左右的加载更多,也是一直想撸一遍自己常用的第三方.一点点开始吧.今天根据MJRefresh给scrollView加正常的上拉加载和下拉刷新.

废话不多就到这.开始正文.....(自己的理解,不喜勿碰,但可以指正.....)

开始

思路撸起 headerView 和 footerView 都是自定义View 添加到scrollView上的.而且这两个View的大小可以随意确定.可以在里面加广告都行.

  • 1.headerView的添加:添加到哪里?位置如何确定?下拉的三种状态怎么确定? 接下来一一解答. 前两个个问题:添加到那里?位置如何确定? 当然是scrollview的顶部([scrollView addSubview:headerview])并且要预留 inset.top的距离.这样的话headerview的y坐标就确定了就是:-(headerview.height + inset.top),这样才能做到我们下拉的时候这个headerView 才出现,而且不受我们inset的距离的影响.

第三个问题:下拉的三种状态怎么确定? 三种状态:正常,下拉,刷新 包括底部的footerView 也是这三种状态,所以可以考虑写在基类里.

  • 2.footerView的添加:也是和headerView一样加到scrollView的底部.那这样y坐标也确定了.但是问题出现了这个Y值依赖谁呢这里和headerView 不一样的就是这个Y值是动态的,要根据scrollView的contentSize.height 来确定
    • if contentSize.height < scorllView.height 那就是cell的个数不满一屏,这个时候Y = scorllView.height + .bottom
    • if contentSize.height > scorllView.height 那就是 Y = contentSize.height + .bottom 那就是cell个数大于一屏 这个时候,就需要以contentSize.height 来确定啦.

好基本思路已经敲定,接下来咱们就来具体实现以下: 第一步:创建基类 因为无论是上拉还是下拉的状态都是统一的,并且view内部的构造都是 image + label 所以,创建基类. SXRefreshBaseView.h

//监听 scrollView 滑动的key值
#define SXRefreshContentOffset @"contentOffset"

//:设置刷新状态
typedef NS_ENUM(NSInteger,SXRefreshState)
{
   SXRefreshStateNormal = 0,
   SXRefreshStatePulling,
   SXRefreshStateRefreshing
};

NS_ASSUME_NONNULL_BEGIN

@interface SXRefreshBaseView : UIView
/**
* 父视图
*/
@property (nonatomic, weak) UIScrollView *scrollView;

/**
* 手指拉动时的 文字提示
*/
@property (nonatomic, copy) NSString *pullToRefreshText;
/**
* 松手的时候的文字提示
*/
@property (nonatomic, copy) NSString *releaseToRefreshText;

/**
* 刷新的时候的文字提示
*/
@property (nonatomic, copy) NSString *refreshingText;

/**
* 刷新完成之后的回调 或则 你可以回传一个方法 像 MJ 一样  
*/
@property (nonatomic, copy) void(^refreshingCallBack)(void);

/**
* 当前 刷新的状态
*/
@property (nonatomic, assign) SXRefreshState state;

/**
* scrollView的inset
*/
@property (nonatomic, assign) UIEdgeInsets scrollViewOrignalInset;

- (void)beginRefeshing;
- (void)endRefreshing;

SXRefreshBaseView.m文件

#import "SXRefreshBaseView.h"

//:刷新控件的高度
static CGFloat SXRefreshViewHeight = 44.f;


static CGFloat SXRefreshImgH = 30.f;
static CGFloat SXRefreshImgW = 30.f;

static CGFloat SXRefreshLabelW = 100;
static CGFloat SXRefreshLabelH = 20;


@interface SXRefreshBaseView ()

/**
*
*/
@property (nonatomic, strong) UILabel *refreshLabel;
/**
*
*/
@property (nonatomic, strong) UIImageView *refreshImageView;
@end
@implementation SXRefreshBaseView

- (instancetype)initWithFrame:(CGRect)frame
{
   frame.size.height = SXRefreshViewHeight;
   //:这个宽度暂时是写死
   frame.size.width = 414;

   
   self = [super initWithFrame:frame];
   if (self) {
       [self createUI];
   }
   return self;
}
//:布局
- (void)createUI{

   CGFloat imgX = ([UIScreen mainScreen].bounds.size.width - SXRefreshImgW - SXRefreshLabelW) * 0.5;
   CGFloat imgY = (SXRefreshViewHeight - SXRefreshImgH) * 0.5;
   CGFloat labelY = (SXRefreshViewHeight - SXRefreshLabelH)*0.5;
   
   
   [self addSubview:self.refreshImageView];
   [self addSubview:self.refreshLabel];
   self.refreshImageView.frame = CGRectMake(imgX, imgY, SXRefreshImgW, SXRefreshImgW);
   self.refreshLabel.frame = CGRectMake(CGRectGetMaxX(self.refreshImageView.frame), labelY, SXRefreshLabelW, SXRefreshLabelH);
}
#pragma mark ---willmovetosuper 通过这个方法 可以直接获取到俯视图 就不用外部传他们的俯视图了.很好用.
- (void)willMoveToSuperview:(UIView *)newSuperview{
   [super willMoveToSuperview:newSuperview];
   [self.superview removeObserver:self forKeyPath:SXRefreshContentOffset context:nil];
   
   if (newSuperview) {
       [newSuperview addObserver:self forKeyPath:SXRefreshContentOffset options:NSKeyValueObservingOptionNew context:nil];
       //:记录UIScrollview
       _scrollView  = (UIScrollView *)newSuperview;
       _scrollViewOrignalInset = _scrollView.contentInset;
   }
   //:居中显示图片 提示信息
   CGRect temFrame = _refreshImageView.frame;
   temFrame.origin.x = (newSuperview.frame.size.width - SXRefreshImgW - SXRefreshLabelW)*0.5;
   _refreshImageView.frame = temFrame;
   
   CGRect temLFrame = _refreshLabel.frame;
   temLFrame.origin.x = CGRectGetMaxX(_refreshImageView.frame);
   _refreshLabel.frame = temLFrame;
}
//:给提示的label 一个开始默认的值 
- (void)setPullToRefreshText:(NSString *)pullToRefreshText{
   _pullToRefreshText = pullToRefreshText;
   self.refreshLabel.text = pullToRefreshText;
}
//:通过state的状态 来改变 刷新label的文字提示,以及之类可以直接通过修改state的值 就可以改变lable的状态,以及动画效果
- (void)setState:(SXRefreshState) state{
   if (_state == state ) {
       return;
   }
   
   switch (state) {
       case SXRefreshStateNormal:
           //:动画
           //:改变状态
           self.refreshLabel.text = self.pullToRefreshText;
           break;
       case SXRefreshStatePulling:
           //:动画
           //:改变状态
           self.refreshLabel.text = self.releaseToRefreshText;
           break;
       case SXRefreshStateRefreshing:
           //:动画
           //:改变状态
           self.refreshLabel.text = self.refreshingText;
           if (self.refreshingCallBack) {
               self.refreshingCallBack();
           }
           break;

       default:
           break;
   }
   _state = state;
}
//:开始 和结束刷新 也是 通过改变state 的值  所以 通过setter 方法 来修改 还是很方便的
- (void)beginRefeshing {
   self.state = SXRefreshStateRefreshing;
}
- (void)endRefreshing{
   self.state = SXRefreshStateNormal;
}

接下来我们来看headerView的使用

SXRefreshHeaderView.h


#import "SXRefreshBaseView.h"

NS_ASSUME_NONNULL_BEGIN

@interface SXRefreshHeaderView : SXRefreshBaseView
//:声明一个便捷的构造方法
+ (instancetype)header;
@end

NS_ASSUME_NONNULL_END

SXRefreshHeaderView.m

#import "SXRefreshHeaderView.h"

@implementation SXRefreshHeaderView

+ (instancetype)header {
   
   return [[SXRefreshHeaderView alloc]init];
}
- (instancetype)initWithFrame:(CGRect)frame
{
   self = [super initWithFrame:frame];
   if (self) {
       self.pullToRefreshText = @"下拉即可刷新";
       self.releaseToRefreshText = @"释放即可刷新";
       self.refreshingText = @"刷新中...";
   }
   return self;
}
//:这个方法 可以再次修改 布局 而且 不用动 父类的布局文件
- (void)willMoveToSuperview:(UIView *)newSuperview{
   [super willMoveToSuperview:newSuperview];
   
   //:在这个方法里面 设置自己的尺寸
   CGRect frame = self.frame;
   frame.origin.y = - self.frame.size.height - self.scrollView.contentInset.top;
   self.frame = frame;
   
}

重点来了

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    //:不能根用户交互或正在刷新的就直接返回
    
    if (!self.userInteractionEnabled || self.alpha <= 0.01 || self.hidden || self.state == SXRefreshStateRefreshing) {
        return;
    }
    //:根据偏移量设置响应的状态
    if ([keyPath isEqualToString:SXRefreshContentOffset]) {
        [self setStateWithcontentOffset];
    }
}
- (void)setStateWithcontentOffset{
    //:当前偏移量 使用绝对值 好理解一些
    CGFloat currentOffsetY = fabs(self.scrollView.contentOffset.y);
    
    
    //:头部控件底部!!!!底部!!!!底部底部刚好出现的 offsetY
    CGFloat happendOffsetY = - self.scrollViewOrignalInset.top;
    
    //:头部控件的底部刚好出现 的位置 等于当前的 scrollView的 inset.top的 转换为坐标就是负的值
    //:然后刚好出现到 完全出现 移动的距离就等于  headerView 的 height 负的
   //:没有超过这个点就是 普通下拉状态 超过了这个点 就是 可以刷新的 点
    
    if (currentOffsetY <= happendOffsetY) {
        return;
    }
    //:滑动时  因为 上拉下拉都在监听  所以 要判断一下 滑动方向
    if (self.scrollView.isDragging && self.scrollView.contentOffset.y < 0) {
        //:普通状态和即将刷新的临界点  
        CGFloat normalToPullingOffsetY = fabs(happendOffsetY - self.frame.size.height);
        //:转为即将刷新状态
        //:这里其实用 绝对值要好理解一些  向下拉 contentOffset的值 是负的  如果当前 offset.y 大于 .top + headerView.height  那就代表 可以刷新了  并且 当前的状态是 normal;
        //:反之就不进行刷新 直接返回正常状态
        if (self.state == SXRefreshStateNormal && currentOffsetY > normalToPullingOffsetY) {
            self.state = SXRefreshStatePulling;
        }else if(self.state == SXRefreshStatePulling && currentOffsetY <= normalToPullingOffsetY){
            self.state = SXRefreshStateNormal;
        }
        //:松手时 如果是松开可以进行刷新的状态 则进行刷新
    }else if (self.state == SXRefreshStatePulling){
        self.state = SXRefreshStateRefreshing;
    }
}
//:这里重写父类方法 进行扩展一些动画实现
- (void)setState:(SXRefreshState)state{
    if (self.state == state ) {
        return;
    }
    //:保存旧的状态
    SXRefreshState  oldState = self.state;
    //:调用父类方法
    [super setState:state];
    
    switch (state) {
        case SXRefreshStateNormal:
            if (oldState == SXRefreshStateRefreshing) {
                [UIView animateWithDuration:0.25f animations:^{
                    UIEdgeInsets inset = self.scrollView.contentInset;
                    inset.top -= self.frame.size.height;
                    self.scrollView.contentInset = inset;
                }];
            }
            break;
            
        case SXRefreshStatePulling:
            break;
        case SXRefreshStateRefreshing:{
            //执行动画
            [UIView animateWithDuration:0.25f animations:^{
                CGFloat top = self.scrollViewOrignalInset.top + self.frame.size.height;
                
                //增加滚动区域
                UIEdgeInsets inset = self.scrollView.contentInset;
                inset.top = top;
                self.scrollView.contentInset = inset;
                
                //设置滚动位置
                CGPoint offset = self.scrollView.contentOffset;
                offset.y = - top;
                self.scrollView.contentOffset = offset;
            }];
            break;
        }

        default:
            break;
    }
    self.state = state;
}

到这里headerView我们就完成了. 接下来我们将headerView加到我们的滑动视图上去. 创建一个scrollView的分类

  • UIScrollView+SXRefresh.h
@interface UIScrollView (SXRefresh)
//:添加下拉刷新回调
- (void)addHeaderRefreshWithCallBack:(void(^)(void))callBack;

//:让下拉刷新控件停止刷新
- (void)headerEndRefreshing;



@end
  • UIScrollView+SXRefresh.m
#import "UIScrollView+SXRefresh.h"
#import "SXRefreshHeaderView.h"
#import "SXRefreshFooterView.h"
#import <objc/runtime.h>

@interface UIScrollView ()
/**
 *
 */
@property (nonatomic,strong) SXRefreshHeaderView *headerView;

@property (nonatomic,strong) SXRefreshFooterView *footerView;

@end
@implementation UIScrollView (SXRefresh)

static char SXRefreshHeaderKey;
static char SXRefreshFooterKey;
//关联对象
- (void)setHeaderView:(SXRefreshHeaderView *)headerView{
    [self willChangeValueForKey:@"SXRefreshHeaderKey"];
    
    objc_setAssociatedObject(self,&SXRefreshHeaderKey, headerView, OBJC_ASSOCIATION_ASSIGN);
    
    [self didChangeValueForKey:@"SXRefreshHeaderKey"];
}
- (SXRefreshHeaderView *)headerView{
    return objc_getAssociatedObject(self, &SXRefreshHeaderKey);
}
- (void)addHeaderRefreshWithCallBack:(void (^)(void))callBack{
    if (!self.headerView) {
        SXRefreshHeaderView *headerView = [SXRefreshHeaderView header];
        [self addSubview:headerView];
        self.headerView = headerView;
    }
    self.headerView.refreshingCallBack = callBack;
}
- (void)headerEndRefreshing{
    [self.headerView endRefreshing];
}

这里headerView 就可以正常使用啦.footerView的添加方式和headerView的实现一样. 接下来我们来看下footerView的实现

#import "SXRefreshBaseView.h"

NS_ASSUME_NONNULL_BEGIN

@interface SXRefreshFooterView : SXRefreshBaseView
+ (instancetype)footerView;

@end
#import "SXRefreshFooterView.h"
#define SXRefreshContentSize @"contentSize"

@implementation SXRefreshFooterView
+ (instancetype)footerView {
    
    return [[SXRefreshFooterView alloc]init];
}

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor redColor];
        self.pullToRefreshText = @"上拉即可刷新";
        self.releaseToRefreshText = @"释放即可刷新";
        self.refreshingText = @"刷新中...";
        
    }
    return self;
}
- (void)willMoveToSuperview:(UIView *)newSuperview{
    [super willMoveToSuperview:newSuperview];
    [self.superview removeObserver:self forKeyPath:SXRefreshContentSize context:nil];
    if (newSuperview) {
        [newSuperview addObserver:self forKeyPath:SXRefreshContentSize options:NSKeyValueObservingOptionNew context:nil];
        [self changeFooterViewFrame];
    }
    
}
- (void)changeFooterViewFrame{

    CGFloat contentSizeH = self.scrollView.contentSize.height;
    CGFloat scrollViewBoundsH = self.scrollView.bounds.size.height;
    NSLog(@"inset :%f ++++ %f",scrollViewBoundsH,self.scrollView.contentInset.bottom);
    CGFloat footerViewY = MAX(contentSizeH, scrollViewBoundsH) + self.scrollView.contentInset.bottom;

    CGRect rect = self.frame;
    rect.origin.y = footerViewY;
    self.frame = rect;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if (!self.userInteractionEnabled || self.alpha <= 0.01 || self.hidden) {
        return;
    }
    if ([keyPath isEqualToString:SXRefreshContentSize]) {
        //:改变footerView 的位置
        [self changeFooterViewFrame];
    }else if ([keyPath isEqualToString:SXRefreshContentOffset]){
        //:如果正在刷新直接返回

        [self setStateWithcontentOffset];
        
    }
    
}
- (void)setStateWithcontentOffset{
    //:获取当前的contentoffset.y
    CGFloat contentOffsetY = self.scrollView.contentOffset.y;
    
    //:滑到底部刚好看到刷新控件的的Y
    //:若果当前 contentSize 大于 scrollview 的frame
    CGFloat contentSizeH = self.scrollView.contentSize.height;
    CGFloat scrollViewBoundsH = self.scrollView.bounds.size.height;
    CGFloat footerViewY = MAX(contentSizeH, scrollViewBoundsH) + self.scrollView.contentInset.bottom;
    
    //:底部刷新视图完全出来 的距离 是
    CGFloat  footerViewFullApperance = contentOffsetY + scrollViewBoundsH;
    BOOL isCanRefreshing = footerViewFullApperance - footerViewY - self.bounds.size.height > 0 ? YES:NO;
    
    if (self.state == SXRefreshStateRefreshing) {
        return;
    }
    if (self.scrollView.isDragging) {
        if (self.state == SXRefreshStateNormal && isCanRefreshing ) {
            self.state = SXRefreshStatePulling;
        }else{
            //:还原
            
        }
    }else if(self.state == SXRefreshStatePulling) {
        self.state = SXRefreshStateRefreshing;
    }
}

上下实现实现到这里基本可以啦,剩下就是根据自己的项目需求,做一些自己的特有的事情. 后面的左右添加正在实现......