再次受 Kitten 大神的一篇妙文所启发,想要自己来实现原文中 Kitten 已实现的动画效果。
最终效果如下:
下面我们来一步一步地实现它。有一些基础的知识需要先事先了解:
- CADisplayLink,可理解为跟屏幕刷新频率同步的定时器。可见 Kitten 的博文
- UIKit Dynamics,iOS7 之后 UIKit 中包含的可应用于 View 对象的“物理引擎”。可见 Pandara 的这篇博文。
首先,我们需要一个能够互相碰撞小球跟地面。小球为 SBJellyBall
类对象,实现它时有一个地方需要注意一下。在 iOS9 里面,协议 UIDynamicItem 新引入一个属性,可以让我们设置 item 的碰撞边缘类型:
1 | @property(nonatomic, readonly) UIDynamicItemCollisionBoundsType collisionBoundsType; |
注意它是 readonly 的,所以只能在 SBJellyBall.m 文件里这样实现它:1
2
3
4
5
6// SBJellyBall.m
- (UIDynamicItemCollisionBoundsType)collisionBoundsType
{
return UIDynamicItemCollisionBoundsTypeEllipse;
}
然后我们设置 UIDynamic 相关的东东:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// SBJellyRefreshView.m
//gravity
UIGravityBehavior *gravityBehavior = [[UIGravityBehavior alloc] initWithItems:@[self.ball]];
[self.animator addBehavior:gravityBehavior];
//collision
UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[self.ball]];
collisionBehavior.collisionMode = UICollisionBehaviorModeEverything;
collisionBehavior.translatesReferenceBoundsIntoBoundary = YES;
[self.animator addBehavior:collisionBehavior];
//ball property
UIDynamicItemBehavior *ballBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.ball]];
ballBehavior.elasticity = 0.4;
ballBehavior.allowsRotation = YES;
ballBehavior.friction = 1;
ballBehavior.resistance = 0.5;
[self.animator addBehavior:ballBehavior];
接着,尝试给 View 添加一个弧形的下边界。首先初始化我们的贝塞尔曲线路径:1
2
3
4
5
6
7
8
9
10
11- (UIBezierPath *)getPathFromDistance:(CGFloat)Distance
{
UIBezierPath *bezierPath = [UIBezierPath bezierPath];
[bezierPath moveToPoint:CGPointMake(0, 0)];
[bezierPath addLineToPoint:CGPointMake(self.frame.size.width, 0)];
[bezierPath addLineToPoint:CGPointMake(self.frame.size.width, self.frame.size.height)];
[bezierPath addQuadCurveToPoint:CGPointMake(0, self.frame.size.height) controlPoint:CGPointMake(self.frame.size.width / 2.0f, self.frame.size.height * 1.5)];
[bezierPath closePath];
return bezierPath;
}
然后把它添加到碰撞边界中,这一步是关键,将上面有关碰撞的设置代码修改成下面这段:1
2
3
4
5
6
7//collision
self.bezierPath = [self getPathFromDistance:0];
UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[self.ball]];
collisionBehavior.collisionMode = UICollisionBehaviorModeEverything;
[collisionBehavior addBoundaryWithIdentifier:COLLISION_BOUNDARY_BEZIER forPath:self.bezierPath];
[self.animator addBehavior:collisionBehavior];
到这里,实现这个Q弹效果的核心部分已经讲完了。接下来大致说说自定义下拉刷新相关的杂事。小球在下拉之前处于悬空状态,此时应该:
- 将小球从 gravity behavior 中移除
- 重置小球的位置,记得设置完成之后调用 UIDynamicAnimator 的
updateItemUsingCurrentState:
来更新小球的位置
执行刷新动画时,需要将小球所有 dynamic 相关的东东移除:
- 从 gravity behavior 中移除
- 将 ballBehavior 从 animator 中移除,免得对后面要加入的 animation 造成冲突
- 将 collision 移除
有那么一个特殊情况,如果用户滑动得太过猛烈,小球会掉出来,这时候需要一个方法将小球拽回来:1
2
3
4
5
6
7- (void)dragBallBackIfNeeded
{
if (![self.bezierPath containsPoint:self.ball.center]) {
self.ball.center = CGPointMake(self.ball.center.x, self.ball.center.y - BALL_W);
[self.animator updateItemUsingCurrentState:self.ball];
}
}
最后说说我自定义下拉刷新的大致流程:
1.定义自己的 refreshView 类,并定义触发刷新的下拉距离1
2.在 refreshView 的 drawRect: 中获取 parent scrollView:1
2
3
4
5
6
7
8
9
10
11
12
13- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
UIView *superView = self.superview;
while (superView) {
if ([superView isKindOfClass:[UIScrollView class]]) {
self.parentScrollView = (UIScrollView *)superView;
break;
} else {
superView = superView.superview;
}
}
}
3.实现三个方法,用处嘛,看方法名字就知道了: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- (void)scrollViewDidScroll
{
// 更新 refreshView 的 UI
// 注意区分此时是否处于 refreshing 的状态
// 为了能流畅地设置 contentInset
if (_toRefresh) {
…
if (self.parentScrollView.contentOffset.y >= -REFRESH_H && self.parentScrollView.contentInset.top == 0) {
self.parentScrollView.contentInset = UIEdgeInsetsMake(REFRESH_H, 0, 0, 0);
self.parentScrollView.contentOffset = CGPointMake(0, -REFRESH_H);
}
}
}
- (void)scrollViewDidEndDragging
{
// 判断是否需要执行刷新
}
- (void)endRefresh
{
// 重置所有
}
最终效果: