一直想给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;
}
}
上下实现实现到这里基本可以啦,剩下就是根据自己的项目需求,做一些自己的特有的事情. 后面的左右添加正在实现......