一個仿微信朋友圈的下拉刷新
demo鏈接: https://github.com/Xiexingda/XDRefresh.git
喜歡的話請在github給顆小星星哦??!
效果:
1544178417750452.gif
使用方法
先說一下用法,然后再說實現
使用方法很簡單,導入頭文件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給顆小星星哦??!