一个仿微信朋友圈的下拉刷新
demo链接: https://github.com/Xiexingda/XDRefresh.git
喜欢的话请在github给颗小星星哦😊!
效果:
使用方法
先说一下用法,然后再说实现
使用方法很简单,导入头文件UIView+XDRefresh.h
- (void)viewDidLoad {
[super viewDidLoad];
/**
添加下拉刷新
*/
__weak typeof(self) weakSelf = self;
[self.view XD_refreshWithObject:_yourTableview atPoint:CGPointZero downRefresh:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
//开始刷新
}];
//结束刷新
[self.view XD_endRefresh];
}
//退出界面时释放掉刷新监听
- (void)dealloc {
[self.view XD_freeReFresh];
}
基本思路:
用一个与下拉刷新小圆圈一样大小的scrollview,把其contentSize也置为同样大小,然后把下拉刷新的小圆圈放到scrollview上,这样在下拉刷新过程中只需要根据被观察者的下拉状态去改变这个scrollview的contentoffset.y即可实现小圆圈的上下移动,而不需要去渲染下拉小圆圈的frame
实现过程:
刷新过程主要分为三种状态
typedef NS_ENUM(NSInteger,StatusOfRefresh) {
XDREFRESH_Default = 1, //非刷新状态,该值不能为0
XDREFRESH_BeginRefresh, //刷新状态
XDREFRESH_None //全非状态(即不是刷新 也不是 非刷新状态)
};
@property (nonatomic, assign)CGFloat threshold;//下拉位置的最大范围
主要方法,通过kvo去观察tableview的下拉过程
/**
添加观察者
@param view 观察对象
*/
- (void)addObserverForView:(UIView *)view {
[view addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
}
实现观察者的代理 并在其中实现三种状态(非刷新,刷新,(全非)既不刷新也不非刷新)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
//非状态时屏蔽掉其他的操作
if (self.refreshStatus == XDREFRESH_None) {
return;
}
//屏蔽掉开始进入界面时的系统下拉动作
if (self.refreshStatus == 0) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.refreshStatus = XDREFRESH_Default;
});
return;
}
// 实时监测scrollView.contentInset.top, 系统优化以及手动设置contentInset都会影响contentInset.top。
if (self.marginTop != self.extenScrollView.contentInset.top) {
self.marginTop = self.extenScrollView.contentInset.top;
}
CGFloat offsetY = self.extenScrollView.contentOffset.y;
/**异步调用主线程**/
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
/**非刷新状态**/
if (self.refreshStatus == XDREFRESH_Default) {
[self defaultHandleWithOffSet:offsetY change:change];
/**刷新状态**/
} else if (self.refreshStatus == XDREFRESH_BeginRefresh) {
[self refreshingHandleWithOffSet:offsetY];
}
});
});
}
全非状态时直接return 以屏蔽掉刷新、非刷新状态 (刷新小圆圈在下拉悬停状态时进入全非状态,待刷新完成后自动收回,这个过程应避免人为干预造成卡顿,而刷新、和非刷新状态人为拉动时都会干预到小圆圈的contentoffset所以要屏蔽掉)
//非状态时屏蔽掉其他的操作
if (self.refreshStatus == XDREFRESH_None) {
return;
}
非刷新状态逻辑
/**
非刷新状态时的处理
@param offsetY tableview滚动偏移量
*/
- (void)defaultHandleWithOffSet:(CGFloat)offsetY change:(NSDictionary<NSKeyValueChangeKey,id> *)change {
// 向下滑动时<0,向上滑动时>0;
CGFloat defaultoffsetY = offsetY + self.marginTop;
/**刷新动作区间**/
if (defaultoffsetY > self.threshold && defaultoffsetY < 0) {
[self.refreshView setContentOffset:CGPointMake(0, defaultoffsetY)];
/*
注意:将default动作处理只放到 动作区间 和 超过/等于 临界点 的逻辑块里
目的:实现只有在下拉动作时才会有动作处理,否则没有
*/
[self anmiationHandelwithChange:change
andStatus:XDREFRESH_Default
needAnimation:YES];
}
/**(@"刷新临界点,把刷新icon置为最大区间")**/
if (defaultoffsetY <= self.threshold && self.refreshView.contentOffset.y != self.threshold) {
//添加动作,避免越级过大造成直接跳到最大位置影响体验
[UIView animateWithDuration:0.05 animations:^{
[self.refreshView setContentOffset:CGPointMake(0, self.threshold)];
}];
}
/**超过/等于 临界点后松手开始刷新,不松手则不刷新**/
if (defaultoffsetY <= self.threshold && self.refreshView.contentOffset.y == self.threshold) {
if (self.extenScrollView.isDragging) {
//NSLog(@"不刷新");
//default动作处理
[self anmiationHandelwithChange:change
andStatus:XDREFRESH_Default
needAnimation:YES];
} else {
//NSLog(@"开始刷新");
//刷新状态动作处理
[self anmiationHandelwithChange:change
andStatus:XDREFRESH_BeginRefresh
needAnimation:YES];
// 由非刷新状态 进入 刷新状态
[self beginRefresh];
}
}
/**当tableview回滚到顶端的时候把刷新的iconPosition置零**/
if (defaultoffsetY >= 0 && self.refreshView.contentOffset.y != 0) {
[self.refreshView setContentOffset:CGPointMake(0, 0)];
//当回到原始位置后,转角也回到原始位置
[self trangleToBeOriginal];
}
}
刷新状态逻辑
/**
刷新状态时的处理
@param offsetY tableview滚动偏移量
*/
- (void)refreshingHandleWithOffSet:(CGFloat)offsetY {
//转换坐标(相对费刷新状态)
CGFloat refreshoffsetY = offsetY + self.marginTop + self.threshold;
/**刷新状态时动作区间**/
if (refreshoffsetY > self.threshold && refreshoffsetY < 0) {
[self.refreshView setContentOffset:CGPointMake(0, refreshoffsetY)];
}
/**刷新状态临界点,把刷新icon置为最大区间**/
if (refreshoffsetY <= self.threshold && self.refreshView.contentOffset.y != self.threshold) {
//添加动作,避免越级过大造成直接跳到最大位置影响体验
[UIView animateWithDuration:0.05 animations:^{
[self.refreshView setContentOffset:CGPointMake(0, self.threshold)];
}];
}
/**当tableview相对坐标回滚到顶端的时候把刷新的iconPosition置零**/
if (refreshoffsetY >= 0 && self.refreshView.contentOffset.y != 0) {
[self.refreshView setContentOffset:CGPointMake(0, 0)];
}
}
刷新
/**
开始刷新
*/
- (void)beginRefresh {
//状态取反 保证一次刷新只执行一次回调
if (self.refreshStatus != XDREFRESH_BeginRefresh) {
self.refreshStatus = XDREFRESH_BeginRefresh;
if (self.refreshBlock) {
self.refreshBlock();
}
}
}
动画效果
/**
动作处理
@param change 监听到的offset变化
*/
- (void)anmiationHandelwithChange:(NSDictionary<NSKeyValueChangeKey,id> *)change andStatus:(StatusOfRefresh)status needAnimation:(BOOL)need {
if (!need) {
return;
}
/**
非刷新状态下的动作处理
*/
if (status == XDREFRESH_Default) {
/**把nsPoint结构体转换为cgPoint**/
CGPoint oldPoint;
id oldValue = [change valueForKey:NSKeyValueChangeOldKey];
[(NSValue*)oldValue getValue:&oldPoint];
CGPoint newPoint;
id newValue = [ change valueForKey:NSKeyValueChangeNewKey ];
[(NSValue*)newValue getValue:&newPoint];
dispatch_async(dispatch_get_main_queue(), ^{
if (oldPoint.y < newPoint.y) {
self.refreshView.refreshIcon.transform = CGAffineTransformRotate(self.refreshView.refreshIcon.transform,
-self.offsetCollect/50);
NSLog(@"向上拉动");
} else if (oldPoint.y > newPoint.y) {
self.refreshView.refreshIcon.transform = CGAffineTransformRotate(self.refreshView.refreshIcon.transform,
self.offsetCollect/50);
NSLog(@"向下拉动");
} else {
NSLog(@"没有拉动");
}
});
/**
刷新状态下的动作处理
*/
} else if (status == XDREFRESH_BeginRefresh) {
if (!self.animation) {
self.animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
}
dispatch_async(dispatch_get_main_queue(), ^{
//逆时针效果
self.animation.fromValue = [NSNumber numberWithFloat:0.f];
self.animation.toValue = [NSNumber numberWithFloat: -M_PI *2];
self.animation.duration = CircleTime;
self.animation.autoreverses = NO;
self.animation.fillMode =kCAFillModeForwards;
self.animation.repeatCount = MAXFLOAT; //一直自旋转
[self.refreshView.refreshIcon.layer addAnimation:self.animation forKey:@"refreshing"];
});
}
}
动画结束后回到最初角度
/**
角度还原:用于非刷新时回到顶部 和 刷新状态endRefresh 中
*/
- (void)trangleToBeOriginal {
self.refreshView.refreshIcon.transform = CGAffineTransformIdentity;
}
结束刷新
- (void)endRefresh {
/**
仿微信当下拉一直拖住时,icon不会返回
虽然在repeat的计时器里,但是该方法只会回调一次
原理:nstimer默认是放在defaultrunloop中的,当下拉拖住时runloop改成了tracking模式,同一时间下线程只能处理一种runloop模式,所以滚动时timer只注册不执行,当松开手时拖拽动作执行完毕,runloop回到default模式下,这个时候nstimer被执,block开始回调,在第一次回调后又调用了invalidate方法将计时器释放了
注意** 最后用invalidate把计时器释放掉
*/
if (self.extenScrollView.isDragging) {
//iOS10 以上
if ([UIDevice currentDevice].systemVersion.floatValue >= 10) {
[NSTimer scheduledTimerWithTimeInterval:0.2 repeats:YES block:^(NSTimer * _Nonnull timer) {
[self endRefresh];
[timer invalidate];
}];
//iOS10 以下
} else {
[NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(timerCall:) userInfo:nil repeats:YES];
}
return;
}
//当结束刷新时,把状态置为全非状态,避免在[UIView animateWithDuration:0.2]icon返回动作中的人为干预,造成icon闪顿现象
if (self.refreshStatus != XDREFRESH_None) {
self.refreshStatus = XDREFRESH_None;
[UIView animateWithDuration:IconBackTime animations:^{
[self.refreshView setContentOffset:CGPointMake(0, 0)];
} completion:^(BOOL finished) {
//结束动画
[self.refreshView.refreshIcon.layer removeAnimationForKey:@"refreshing"];
//当回到原始位置后,转角也回到原始位置
[self trangleToBeOriginal];
//结束后将状态重置为非刷新状态 以备下次刷新
self.refreshStatus = XDREFRESH_Default;
}];
}
}
/**
计时器调用方法
@param timer nstimer
*/
- (void)timerCall:(NSTimer *)timer {
[self endRefresh];
[timer invalidate];
}
到此基本刷新逻辑已经完成了 ,还有一些结束刷新时的操作就不在这里赘述了,demo里面有详细的解析,有什么不合理的地方还望大家指出。
demo链接: https://github.com/Xiexingda/XDRefresh.git
使用方法在 该链接的ReadMe里
喜欢的话请在github给颗小星星哦😊!