再次受 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
|
- (UIDynamicItemCollisionBoundsType)collisionBoundsType { return UIDynamicItemCollisionBoundsTypeEllipse; }
|
然后我们设置 UIDynamic 相关的东东:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
UIGravityBehavior *gravityBehavior = [[UIGravityBehavior alloc] initWithItems:@[self.ball]]; [self.animator addBehavior:gravityBehavior];
UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[self.ball]]; collisionBehavior.collisionMode = UICollisionBehaviorModeEverything; collisionBehavior.translatesReferenceBoundsIntoBoundary = YES; [self.animator addBehavior:collisionBehavior];
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
| 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 类,并定义触发刷新的下拉距离
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 {
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 { }
|
最终效果:
Author:
Gowen
Permalink:
http://pandara.xyz/2015/10/29/jelly_refresh/
License:
Copyright (c) 2019 CC-BY-NC-4.0 LICENSE
Slogan:
Do you believe in DESTINY?