MJRefreshHeader.m 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. // 代码地址: https://github.com/CoderMJLee/MJRefresh
  2. // MJRefreshHeader.m
  3. // MJRefresh
  4. //
  5. // Created by MJ Lee on 15/3/4.
  6. // Copyright (c) 2015年 小码哥. All rights reserved.
  7. //
  8. #import "MJRefreshHeader.h"
  9. #import "UIView+MJExtension.h"
  10. #import "UIScrollView+MJExtension.h"
  11. #import "UIScrollView+MJRefresh.h"
  12. NSString * const MJRefreshHeaderRefreshing2IdleBoundsKey = @"MJRefreshHeaderRefreshing2IdleBounds";
  13. NSString * const MJRefreshHeaderRefreshingBoundsKey = @"MJRefreshHeaderRefreshingBounds";
  14. @interface MJRefreshHeader() <CAAnimationDelegate>
  15. @property (assign, nonatomic) CGFloat insetTDelta;
  16. @end
  17. @implementation MJRefreshHeader
  18. #pragma mark - 构造方法
  19. + (instancetype)headerWithRefreshingBlock:(MJRefreshComponentAction)refreshingBlock
  20. {
  21. MJRefreshHeader *cmp = [[self alloc] init];
  22. cmp.refreshingBlock = refreshingBlock;
  23. return cmp;
  24. }
  25. + (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action
  26. {
  27. MJRefreshHeader *cmp = [[self alloc] init];
  28. [cmp setRefreshingTarget:target refreshingAction:action];
  29. return cmp;
  30. }
  31. #pragma mark - 覆盖父类的方法
  32. - (void)prepare
  33. {
  34. [super prepare];
  35. // 设置key
  36. self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey;
  37. // 设置高度
  38. self.mj_h = MJRefreshHeaderHeight;
  39. }
  40. - (void)placeSubviews
  41. {
  42. [super placeSubviews];
  43. // 设置y值(当自己的高度发生改变了,肯定要重新调整Y值,所以放到placeSubviews方法中设置y值)
  44. self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop;
  45. }
  46. - (void)resetInset {
  47. if (@available(iOS 11.0, *)) {
  48. } else {
  49. // 如果 iOS 10 及以下系统在刷新时, push 新的 VC, 等待刷新完成后回来, 会导致顶部 Insets.top 异常, 不能 resetInset, 检查一下这种特殊情况
  50. if (!self.window) { return; }
  51. }
  52. // sectionheader停留解决
  53. CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top;
  54. insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT;
  55. self.insetTDelta = _scrollViewOriginalInset.top - insetT;
  56. // 避免 CollectionView 在使用根据 Autolayout 和 内容自动伸缩 Cell, 刷新时导致的 Layout 异常渲染问题
  57. if (self.scrollView.mj_insetT != insetT) {
  58. self.scrollView.mj_insetT = insetT;
  59. }
  60. }
  61. - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change
  62. {
  63. [super scrollViewContentOffsetDidChange:change];
  64. // 在刷新的refreshing状态
  65. if (self.state == MJRefreshStateRefreshing) {
  66. [self resetInset];
  67. return;
  68. }
  69. // 跳转到下一个控制器时,contentInset可能会变
  70. _scrollViewOriginalInset = self.scrollView.mj_inset;
  71. // 当前的contentOffset
  72. CGFloat offsetY = self.scrollView.mj_offsetY;
  73. // 头部控件刚好出现的offsetY
  74. CGFloat happenOffsetY = - self.scrollViewOriginalInset.top;
  75. // 如果是向上滚动到看不见头部控件,直接返回
  76. // >= -> >
  77. if (offsetY > happenOffsetY) return;
  78. // 普通 和 即将刷新 的临界点
  79. CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h;
  80. CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h;
  81. if (self.scrollView.isDragging) { // 如果正在拖拽
  82. self.pullingPercent = pullingPercent;
  83. if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) {
  84. // 转为即将刷新状态
  85. self.state = MJRefreshStatePulling;
  86. } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) {
  87. // 转为普通状态
  88. self.state = MJRefreshStateIdle;
  89. }
  90. } else if (self.state == MJRefreshStatePulling) {// 即将刷新 && 手松开
  91. // 开始刷新
  92. [self beginRefreshing];
  93. } else if (pullingPercent < 1) {
  94. self.pullingPercent = pullingPercent;
  95. }
  96. }
  97. - (void)setState:(MJRefreshState)state
  98. {
  99. MJRefreshCheckState
  100. // 根据状态做事情
  101. if (state == MJRefreshStateIdle) {
  102. if (oldState != MJRefreshStateRefreshing) return;
  103. [self headerEndingAction];
  104. } else if (state == MJRefreshStateRefreshing) {
  105. [self headerRefreshingAction];
  106. }
  107. }
  108. - (void)headerEndingAction {
  109. // 保存刷新时间
  110. [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey];
  111. [[NSUserDefaults standardUserDefaults] synchronize];
  112. // 默认使用 UIViewAnimation 动画
  113. if (!self.isCollectionViewAnimationBug) {
  114. // 恢复inset和offset
  115. [UIView animateWithDuration:self.slowAnimationDuration animations:^{
  116. self.scrollView.mj_insetT += self.insetTDelta;
  117. if (self.endRefreshingAnimationBeginAction) {
  118. self.endRefreshingAnimationBeginAction();
  119. }
  120. // 自动调整透明度
  121. if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0;
  122. } completion:^(BOOL finished) {
  123. self.pullingPercent = 0.0;
  124. if (self.endRefreshingCompletionBlock) {
  125. self.endRefreshingCompletionBlock();
  126. }
  127. }];
  128. return;
  129. }
  130. /**
  131. 这个解决方法的思路出自 https://github.com/CoderMJLee/MJRefresh/pull/844
  132. 修改了用+ [UIView animateWithDuration: animations:]实现的修改contentInset的动画
  133. fix issue#225 https://github.com/CoderMJLee/MJRefresh/issues/225
  134. 另一种解法 pull#737 https://github.com/CoderMJLee/MJRefresh/pull/737
  135. 同时, 处理了 Refreshing 中的动画替换.
  136. */
  137. // 由于修改 Inset 会导致 self.pullingPercent 联动设置 self.alpha, 故提前获取 alpha 值, 后续用于还原 alpha 动画
  138. CGFloat viewAlpha = self.alpha;
  139. self.scrollView.mj_insetT += self.insetTDelta;
  140. // 禁用交互, 如果不禁用可能会引起渲染问题.
  141. self.scrollView.userInteractionEnabled = NO;
  142. //CAAnimation keyPath 不支持 contentInset 用Bounds的动画代替
  143. CABasicAnimation *boundsAnimation = [CABasicAnimation animationWithKeyPath:@"bounds"];
  144. boundsAnimation.fromValue = [NSValue valueWithCGRect:CGRectOffset(self.scrollView.bounds, 0, self.insetTDelta)];
  145. boundsAnimation.duration = self.slowAnimationDuration;
  146. //在delegate里移除
  147. boundsAnimation.removedOnCompletion = NO;
  148. boundsAnimation.fillMode = kCAFillModeBoth;
  149. boundsAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
  150. boundsAnimation.delegate = self;
  151. [boundsAnimation setValue:MJRefreshHeaderRefreshing2IdleBoundsKey forKey:@"identity"];
  152. [self.scrollView.layer addAnimation:boundsAnimation forKey:MJRefreshHeaderRefreshing2IdleBoundsKey];
  153. if (self.endRefreshingAnimationBeginAction) {
  154. self.endRefreshingAnimationBeginAction();
  155. }
  156. // 自动调整透明度的动画
  157. if (self.isAutomaticallyChangeAlpha) {
  158. CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
  159. opacityAnimation.fromValue = @(viewAlpha);
  160. opacityAnimation.toValue = @(0.0);
  161. opacityAnimation.duration = self.slowAnimationDuration;
  162. opacityAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
  163. [self.layer addAnimation:opacityAnimation forKey:@"MJRefreshHeaderRefreshing2IdleOpacity"];
  164. // 由于修改了 inset 导致, pullingPercent 被设置值, alpha 已经被提前修改为 0 了. 所以这里不用置 0, 但为了代码的严谨性, 不依赖其他的特殊实现方式, 这里还是置 0.
  165. self.alpha = 0;
  166. }
  167. }
  168. - (void)headerRefreshingAction {
  169. // 默认使用 UIViewAnimation 动画
  170. if (!self.isCollectionViewAnimationBug) {
  171. [UIView animateWithDuration:self.fastAnimationDuration animations:^{
  172. if (self.scrollView.panGestureRecognizer.state != UIGestureRecognizerStateCancelled) {
  173. CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
  174. // 增加滚动区域top
  175. self.scrollView.mj_insetT = top;
  176. // 设置滚动位置
  177. CGPoint offset = self.scrollView.contentOffset;
  178. offset.y = -top;
  179. [self.scrollView setContentOffset:offset animated:NO];
  180. }
  181. } completion:^(BOOL finished) {
  182. [self executeRefreshingCallback];
  183. }];
  184. return;
  185. }
  186. if (self.scrollView.panGestureRecognizer.state != UIGestureRecognizerStateCancelled) {
  187. CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
  188. // 禁用交互, 如果不禁用可能会引起渲染问题.
  189. self.scrollView.userInteractionEnabled = NO;
  190. // CAAnimation keyPath不支持 contentOffset 用Bounds的动画代替
  191. CABasicAnimation *boundsAnimation = [CABasicAnimation animationWithKeyPath:@"bounds"];
  192. CGRect bounds = self.scrollView.bounds;
  193. bounds.origin.y = -top;
  194. boundsAnimation.fromValue = [NSValue valueWithCGRect:self.scrollView.bounds];
  195. boundsAnimation.toValue = [NSValue valueWithCGRect:bounds];
  196. boundsAnimation.duration = self.fastAnimationDuration;
  197. //在delegate里移除
  198. boundsAnimation.removedOnCompletion = NO;
  199. boundsAnimation.fillMode = kCAFillModeBoth;
  200. boundsAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
  201. boundsAnimation.delegate = self;
  202. [boundsAnimation setValue:MJRefreshHeaderRefreshingBoundsKey forKey:@"identity"];
  203. [self.scrollView.layer addAnimation:boundsAnimation forKey:MJRefreshHeaderRefreshingBoundsKey];
  204. } else {
  205. [self executeRefreshingCallback];
  206. }
  207. }
  208. #pragma mark . 链式语法部分 .
  209. - (instancetype)linkTo:(UIScrollView *)scrollView {
  210. scrollView.mj_header = self;
  211. return self;
  212. }
  213. #pragma mark - CAAnimationDelegate
  214. - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
  215. NSString *identity = [anim valueForKey:@"identity"];
  216. if ([identity isEqualToString:MJRefreshHeaderRefreshing2IdleBoundsKey]) {
  217. self.pullingPercent = 0.0;
  218. self.scrollView.userInteractionEnabled = YES;
  219. if (self.endRefreshingCompletionBlock) {
  220. self.endRefreshingCompletionBlock();
  221. }
  222. } else if ([identity isEqualToString:MJRefreshHeaderRefreshingBoundsKey]) {
  223. // 避免出现 end 先于 Refreshing 状态
  224. if (self.state != MJRefreshStateIdle) {
  225. CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
  226. self.scrollView.mj_insetT = top;
  227. // 设置最终滚动位置
  228. CGPoint offset = self.scrollView.contentOffset;
  229. offset.y = -top;
  230. [self.scrollView setContentOffset:offset animated:NO];
  231. }
  232. self.scrollView.userInteractionEnabled = YES;
  233. [self executeRefreshingCallback];
  234. }
  235. if ([self.scrollView.layer animationForKey:MJRefreshHeaderRefreshing2IdleBoundsKey]) {
  236. [self.scrollView.layer removeAnimationForKey:MJRefreshHeaderRefreshing2IdleBoundsKey];
  237. }
  238. if ([self.scrollView.layer animationForKey:MJRefreshHeaderRefreshingBoundsKey]) {
  239. [self.scrollView.layer removeAnimationForKey:MJRefreshHeaderRefreshingBoundsKey];
  240. }
  241. }
  242. #pragma mark - 公共方法
  243. - (NSDate *)lastUpdatedTime
  244. {
  245. return [[NSUserDefaults standardUserDefaults] objectForKey:self.lastUpdatedTimeKey];
  246. }
  247. - (void)setIgnoredScrollViewContentInsetTop:(CGFloat)ignoredScrollViewContentInsetTop {
  248. _ignoredScrollViewContentInsetTop = ignoredScrollViewContentInsetTop;
  249. self.mj_y = - self.mj_h - _ignoredScrollViewContentInsetTop;
  250. }
  251. @end