BOSS直聘APP的下拉刷新动画蛮有趣的,我们来尝试实现一下。
先来看看最终效果:
关于实现思路:
实现思路这东西,并不是一成不变的,每个人心中都有自己喜欢的思想和套路,这里仅分享下我的思路,力图起到抛砖引玉的作用,深入思考,也许你会有更好的方法和思路。
动画拆分
再复杂的动画都可以拆分成许多简单的动画组合起来,这个动画大概可以分成两个主体,我把它分别录制出来给大家看看
第一个,下拉过程中的动画
第一个动画又可以拆分为4个大阶段,对应着4个点之间的动画过程:
每个大阶段又可以拆分为2个小阶段(以第一个和第二个点为例):
1)A点到B点之间的动画:B点不出现,以A点为起点,从A点一直“伸”到B点
2)B点到A点之间的动画:B点出现,以B点为终点,从A点一直“缩”到B点
综上,第一个动画可以拆分为8个阶段:
第二个,进入刷新状态的动画
第二个动画又可以拆分为两个单独动画(旋转+移动)的组合:
整体旋转动画:整体不断重复360度旋转
点反复移动动画:4个点在旋转360的周期内进行(内->外->内->外)的移动
动画实现方式
了解了动画的过程,我们来选择动画的实现方式,由于这里仅需要画圆形,我们选择CAShapeLayer来实现。
CAShapeLayer的简介:
CAShapeLayer顾名思义,就是代表一个形状(Shape)的Layer,它是CALayer的子类。
CAShapeLayer初始化需要指定Frame,但它的形状是由path属性来决定,且必须指定path,不然会没有形状。
CAShapeLayer的重要属性:
1、lineWidth 渲染线的宽度
2、lineCap、lineJoin 渲染线两端和转角的样式
3、fillColor、strokeColor 填充、描边的渲染颜色
4、path 指定的绘图路径,path不完整会自动封闭区域
5、strokeStart、strokeEnd 绘制path的起始和结束的百分比
CAShapeLayer的动画特点:
1、CAShapeLayer跟CALayer一样自带动画效果
2、CAShapeLayer的动画效果仅限沿路径变化,不支持填充区域的动画效果
动画实现
我们自定义一个RefreshHeaderView,并通过分类将其和scrollView关联,当进行下拉操作的时候,headerView进行相应的动画。
1)固定位置的4个点
对应4个Layer,Layer的路径是圆形,填充颜色和路径颜色一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
CGPoint topPoint = CGPointMake(centerLine, radius); self.TopPointLayer = [self layerWithPoint:topPoint color:topPointColor]; self.TopPointLayer.hidden = NO; self.TopPointLayer.opacity = 0.f; [self.layer addSublayer:self.TopPointLayer]; CGPoint leftPoint = CGPointMake(radius, centerLine); self.LeftPointLayer = [self layerWithPoint:leftPoint color:leftPointColor]; [self.layer addSublayer:self.LeftPointLayer]; CGPoint bottomPoint = CGPointMake(centerLine, SURefreshHeaderHeight - radius); self.BottomPointLayer = [self layerWithPoint:bottomPoint color:bottomPointColor]; [self.layer addSublayer:self.BottomPointLayer]; CGPoint rightPoint = CGPointMake(SURefreshHeaderHeight - radius, centerLine); self.rightPointLayer = [self layerWithPoint:rightPoint color:rightPointColor]; [self.layer addSublayer:self.rightPointLayer]; - (CAShapeLayer *)layerWithPoint:(CGPoint)center color:(CGColorRef)color { CAShapeLayer * layer = [CAShapeLayer layer]; layer.frame = CGRectMake(center.x - SURefreshPointRadius, center.y - SURefreshPointRadius, SURefreshPointRadius * 2, SURefreshPointRadius * 2); layer.fillColor = color; layer.path = [self pointPath]; layer.hidden = YES; return layer;
} - (CGPathRef)pointPath { return [UIBezierPath bezierPathWithArcCenter:CGPointMake(SURefreshPointRadius, SURefreshPointRadius) radius:SURefreshPointRadius startAngle:0 endAngle:M_PI * 2 clockwise:YES].CGPath;
} |
2)4个点的连接介质
对应一个Layer,Layer的路径是由4段直线拼接而成,直线的直径和圆形的直接一致,初始的渲染结束位置为0。
8个阶段的动画,可以看成是Layer的渲染开始和结束位置不断变化,并通过改变其渲染的起始和结束位置来改变其形状
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
self.lineLayer = [CAShapeLayer layer]; self.lineLayer.frame = self.bounds; self.lineLayer.lineWidth = SURefreshPointRadius * 2; self.lineLayer.lineCap = kCALineCapRound; self.lineLayer.lineJoin = kCALineJoinRound; self.lineLayer.fillColor = topPointColor; self.lineLayer.strokeColor = topPointColor; UIBezierPath * path = [UIBezierPath bezierPath]; [path moveToPoint:topPoint]; [path addLineToPoint:leftPoint]; [path moveToPoint:leftPoint]; [path addLineToPoint:bottomPoint]; [path moveToPoint:bottomPoint]; [path addLineToPoint:rightPoint]; [path moveToPoint:rightPoint]; [path addLineToPoint:topPoint]; self.lineLayer.path = path.CGPath; self.lineLayer.strokeStart = 0.f; self.lineLayer.strokeEnd = 0.f; [self.layer insertSublayer:self.lineLayer above:self.TopPointLayer]; |
3)滑动过程控制动画进度
该步骤的核心是通过下拉的长度计算LineLayer的开始和结束位置,并在适当的时候显示或隐藏对应的点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
- (void)setLineLayerStrokeWithProgress:(CGFloat)progress { float startProgress = 0.f; float endProgress = 0.f; //没有下拉,隐藏动画 if (progress < 0) {
self.TopPointLayer.opacity = 0.f; [self adjustPointStateWithIndex:0]; } //下拉前奏:顶部的Point的可见度渐变的过程 else if (progress >= 0 && progress < (SURefreshPullLen - 40)) {
self.TopPointLayer.opacity = progress / 20; [self adjustPointStateWithIndex:0]; } //开始动画,这里将下拉的进度分为4个大阶段,方便处理,请看前面的描述 else if (progress >= (SURefreshPullLen - 40) && progress < SURefreshPullLen) {
self.TopPointLayer.opacity = 1.0; //大阶段 0 ~ 3 NSInteger stage = (progress - (SURefreshPullLen - 40)) / 10; //对应每个大阶段的前半段,请看前面描述 CGFloat subProgress = (progress - (SURefreshPullLen - 40)) - (stage * 10); if (subProgress >= 0 && subProgress 5 && subProgress < 10) {
[self adjustPointStateWithIndex:stage * 2 + 1]; startProgress = stage / 4.0 + (subProgress - 5) / 40.0 * 2; if (startProgress < (stage + 1) / 4.0 - 0.1) {
startProgress = (stage + 1) / 4.0 - 0.1; } endProgress = (stage + 1) / 4.0; } } //下拉超过一定长度,4个点已经完全显示 else {
self.TopPointLayer.opacity = 1.0; [self adjustPointStateWithIndex:NSIntegerMax]; startProgress = 1.0; endProgress = 1.0; } //计算完毕,设置LineLayer的开始和结束位置 self.lineLayer.strokeStart = startProgress; self.lineLayer.strokeEnd = endProgress; } - (void)adjustPointStateWithIndex:(NSInteger)index { //index : 小阶段: 0 ~ 7
self.LeftPointLayer.hidden = index > 1 ? NO : YES; self.BottomPointLayer.hidden = index > 3 ? NO : YES; self.rightPointLayer.hidden = index > 5 ? NO : YES; self.lineLayer.strokeColor = index > 5 ? rightPointColor : index > 3 ? bottomPointColor : index > 1 ? leftPointColor : topPointColor; } |
4)达到条件时进入刷新状态
进入刷新状态的条件:下拉长度超过我们指定的长度,且手已离开屏幕(即scrollView没有处于拖动的状态),且没有正在播放Loading动画。
进入刷新状态时,同时执行下拉刷新时需要执行的操作(如加载网络数据等等)
1
2
3
4
5
6
7
8
9
10
11
|
//如果不是正在刷新,则渐变动画 if (!self.animating) {
if (progress >= SURefreshPullLen) {
self.y = - (SURefreshPullLen - (SURefreshPullLen - SURefreshHeaderHeight) / 2); } else {
if (progress = SURefreshPullLen && !self.animating && !self.scrollView.dragging) {
[self startAni]; if (self.handle) {
self.handle(); } } |
执行Loading动画,我们采用CA动画来实现
scrollView的下沉动画
1
2
3
4
5
|
[UIView animateWithDuration:0.5 animations:^{ UIEdgeInsets inset = self.scrollView.contentInset; inset.top = SURefreshPullLen; self.scrollView.contentInset = inset; }]; |
4个点的来回移动动画
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
[self addTranslationAniToLayer:self.TopPointLayer xValue:0 yValue:SURefreshTranslatLen]; [self addTranslationAniToLayer:self.LeftPointLayer xValue:SURefreshTranslatLen yValue:0]; [self addTranslationAniToLayer:self.BottomPointLayer xValue:0 yValue:-SURefreshTranslatLen]; [self addTranslationAniToLayer:self.rightPointLayer xValue:-SURefreshTranslatLen yValue:0]; - (void)addTranslationAniToLayer:(CALayer *)layer xValue:(CGFloat)x yValue:(CGFloat)y { CAKeyframeAnimation * translationKeyframeAni = [CAKeyframeAnimation animationWithKeyPath:@ "transform" ];
translationKeyframeAni.duration = 1.0; translationKeyframeAni.repeatCount = HUGE; translationKeyframeAni.removedOnCompletion = NO; translationKeyframeAni.fillMode = kCAFillModeForwards; translationKeyframeAni.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; NSValue * fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0, 0, 0.f)]; NSValue * toValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(x, y, 0.f)]; translationKeyframeAni.values = @[fromValue, toValue, fromValue, toValue, fromValue]; [layer addAnimation:translationKeyframeAni forKey:@ "translationKeyframeAni" ];
} |
RefreshHeader的整体旋转动画
1
2
3
4
5
6
7
8
9
10
11
12
|
[self addRotationAniToLayer:self.layer]; - (void)addRotationAniToLayer:(CALayer *)layer { CABasicAnimation * rotationAni = [CABasicAnimation animationWithKeyPath:@ "transform.rotation.z" ];
rotationAni.fromValue = @(0); rotationAni.toValue = @(M_PI * 2); rotationAni.duration = 1.0; rotationAni.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; rotationAni.repeatCount = HUGE; rotationAni.fillMode = kCAFillModeForwards; rotationAni.removedOnCompletion = NO; [layer addAnimation:rotationAni forKey:@ "rotationAni" ];
} |
5)回复初始状态
当用户拖动的长度达不到临界值,或者结束Loading的状态时,RefreshHeaderView移除所有的动画,回复到初始状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
- (void)removeAni { [UIView animateWithDuration:0.5 animations:^{ UIEdgeInsets inset = self.scrollView.contentInset; inset.top = 0.f; self.scrollView.contentInset = inset; } completion:^(BOOL finished) { [self.TopPointLayer removeAllAnimations]; [self.LeftPointLayer removeAllAnimations]; [self.BottomPointLayer removeAllAnimations]; [self.rightPointLayer removeAllAnimations]; [self.layer removeAllAnimations]; [self adjustPointStateWithIndex:0]; self.animating = NO; }]; } |
动画添加
我们创建一个UIScrollView的分类,添加一个给ScrollView添加RefreshHeader的方法
1
2
3
4
5
6
|
- (void)addRefreshHeaderWithHandle:(void (^)())handle { SURefreshHeader * header = [[SURefreshHeader alloc]init]; header.handle = handle; self.header = header; [self insertSubview:header atIndex:0]; } |
需要注意的是,由于分类中不能直接添加Property,我们采用关联对象的方法将RefreshHeader和ScrollView绑定
1
|
objc_setAssociatedObject(self, @selector(header), header, OBJC_ASSOCIATION_ASSIGN); |
思考:这里为什么用ASSIGN这个关联策略
此外,由于ScrollView销毁的时候,RefreshHeader也销毁,但是由于RefreshHeader是ScrollView的观察者,不移除将导致应用崩溃,因此在销毁ScrollView之前需要将观察者移除,这里采用方法交换在Dealloc方法里面将观察者移除。
1
2
3
4
5
6
7
8
9
|
+ (void)load { Method originalMethod = class_getInstanceMethod([self class], NSSelectorFromString(@ "dealloc" ));
Method swizzleMethod = class_getInstanceMethod([self class], NSSelectorFromString(@ "su_dealloc" ));
method_exchangeImplementations(originalMethod, swizzleMethod); } - (void)su_dealloc { self.header = nil; [self su_dealloc]; } |
思考:在本代码中ScrollView、RefreshHeader、RefreshBlock三者的引用关系是怎样的?尝试画出一个示意图,加深对内存管理的理解。
到这里,我们就可以使用自己写的下拉刷新库应用在工程中了,就像使用MJRefresh一样方便。
1
2
3
4
5
|
[self.tableView addRefreshHeaderWithHandle:^{ //请求网络数据 }]; //请求完成后 [tableView.header endRefreshing]; |
Demo
本文的demo在我的github上可以下载:https://github.com/DaMingShen/SURefresh
相关推荐
低仿boss直聘App的push动画和上一级Controller在堆栈里的透明和缩小状态(不是真透明,只是把效果做出来而已,只用,所以不封装
一个仿boss直聘的简易招聘APP,是转载的,侵删谦
模仿boss直聘的部分页面前端代码,uniapp框架,加上在线兼职发任务的页面。
仿Boss直聘的消息页面 viewPager嵌套
该demo主要功能是做表视图下拉刷新动画,现在很多APP下拉刷新都会触发动画效果
Android 仿抖音APP下拉刷新功能,首先分析这个效果的实现思路,大致如下: 1、上拉时页面有翻页效果,可以用scrollview的pagingEnabled来实现,也就是说列表页不管你用tableview还是collectionview,只要每个cell...
仿最新美团外卖下拉刷新动画http://blog.csdn.net/baiyuliang2013/article/details/50854592
vue2.0全家桶实现Boss直聘 Web APP
jQuery模拟原生态App 上拉刷新 下拉加载 效果代码.zip
5种uni-app 页面下拉刷新方法-源码示例
Android自带下拉刷新的代码例子。用于演示Android原生控件SwipeRefreshLayout下拉刷新布局的功能与用法。
1. 使用iscroll-4 前端技术 2 .H5模拟手机app下拉刷新
自动、定时下拉刷新手机屏幕,用于某些软件自动下拉刷新数据
QT中通过qml中listview 高仿京东移动app刷新控件,代码中通过上下拉方向及边界的控制,有效的达到了类ios移动刷新的效果。学习qt的童鞋可以下载看看
下雨效果的下拉刷新.zip,基于spritekit和core图形的ios简单刷新控制
强大的Android下拉刷新框支持下拉刷新、上拉加载、二级刷新、越界回弹、越界拖动,具有极强的扩展性,并集成了几十种炫酷的Header和 Footer。 支持横向刷新 支持多点触摸 支持淘宝二楼和二级刷新 支持嵌套多层的视图...
Android手把手实战APP首页 下拉刷新 自动加载源码 csdn博客地址:http://blog.csdn.net/chenzheng8975/article/details/54618301
通过下拉webview实现重现加载页面,和tableview的下拉刷新原理一样
HTML5模拟原生APP上拉加载,下拉刷新HTML5模拟原生APP上拉加载,下拉刷新
html5+css3实现上拉和下拉刷新,实现app下拉上拉刷新效果