什么是 UIKit Dynamics
iOS 7 中推出的UIKit Dynamics,主要带来了模拟现实的二维动画效果,Apple 的高度封装让开发者不用知道太多物理知识也可以开发出逼真的物理动画。
- Real word inspired interactions
- Combining predefined and interactive animations
- Designed for UI
Why
苹果鼓励模拟真实世界的交互而不只是简单的像素堆砌的拟物风格,所以苹果这些模拟现实的交互动画封装进了 UIKit,希望开发者能开发出更多模拟现实的交互。
关键类
- UIDynamicAnimator ,封装了底层 iOS 物理引擎,为动力项(UIDynamicItem)提供物理相关的功能和动画。
- UIDynamicBehavior ,动力行为,为动力项提供不同的物理行为
- UIDynamicItem ,动力项,相当于现实世界中的一个基本物体
这三个类的结构是:UIDynamicAnimator 需要一个 refrence view 作为物理引擎的坐标系统,再根据不同需求添加各种动力行为(UIDynamicBehavior),而每个动力行为都可以指定一个或多个动力项(UIDynamicItem),常用的动力项就是一个普通的 View。
UIDynamicAnimator
UIDynamicAnimator 封装了底层 iOS 物理引擎,为动力项(UIDynamicItem)提供物理相关的功能和动画,并为这些动画提供上下文。Animator 作为底层 iOS 物理引擎和动力项(UIDynamicItem)之间的中介,通过 - (void)addBehavior:(UIDynamicBehavior *)behavior;
方法添加不同的动力行为,让动力项拥有物理功能和动画。
现在来看看 UIDynamicAnimator 都有哪些方法:
- 初始化和管理一个 Dynamic Animator
|
- (instancetype)initWithReferenceView:(UIView*)view;
- (NSArray*)itemsInRect:(CGRect)rect;
- (void)addBehavior:(UIDynamicBehavior *)behavior;
- (void)removeBehavior:(UIDynamicBehavior *)behavior;
- (void)removeAllBehaviors;
|
- 获取 Dynamic Animator’s 的状态
|
@property (nonatomic, readonly, getter = isRunning) BOOL running;
@property (nonatomic, readonly, copy) NSArray* behaviors;
@property (nonatomic, readonly) UIView* referenceView;
@property (nonatomic, assign) id <UIDynamicAnimatorDelegate> delegate;
- (NSTimeInterval)elapsedTime;
- (void)updateItemUsingCurrentState:(id <UIDynamicItem>)item;
|
- Collection View Additions
| - (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout*)layout
– layoutAttributesForCellAtIndexPath:
– layoutAttributesForDecorationViewOfKind:atIndexPath:
– layoutAttributesForSupplementaryViewOfKind:atIndexPath:
|
从这里开始,让我们先创建一个项目,取名 DynamicDemo,选择 single view project。
在 ViewController.m 文件修改成如下代码:
| @interface ViewController ()
@property (strong, nonatomic) UIView *squareView;
@property (strong, nonatomic) UIDynamicAnimator *animator;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.squareView = [[UIView alloc] initWithFrame:CGRectMake(100, 200, 200, 200)];
self.squareView.backgroundColor = [UIColor orangeColor];
[self.view addSubview:self.squareView];
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
}
@end
|
上面的代码创建了一个方形 View,橘黄色背景色。还创建了一个UIDynamicAnimator。
UIDynamicBehavior 是具体的物理行为。
UIDynamicBehavior 赋予动态行为给一个或多个动态项(Dynamic Item)。
- UIDynamicBehavior 的主要方法和属性
|
@property(nonatomic, copy) void (^action)(void)
@property(nonatomic, readonly, copy) NSArray *childBehaviors
@property(nonatomic, readonly) UIDynamicAnimator *dynamicAnimator
- (void)addChildBehavior:(UIDynamicBehavior *)behavior
- (void)removeChildBehavior:(UIDynamicBehavior *)behavior
- (void)willMoveToAnimator:(UIDynamicAnimator *)dynamicAnimator
|
在开发中,大部分情况下使用 UIDynamicBehavior 的子类就足够了,因为UIKit 中已经有几个现成的模拟现实的 UIDynamicBehavior 类。
UIDynamicBehavior的子类有:
UIGravityBehavior
重力行为,可以指定重力的方向和大小。用gravityDirection指定一个向量,或者设置 angle 和 magnitude。
打开刚才的项目,DynamicDemo,在 ViewController.m 中添加如下代码:
| - (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
UIGravityBehavior *gravity = [[UIGravityBehavior alloc] initWithItems:@[self.squareView]]; // 创建一个重力行为
gravity.gravityDirection = CGVectorMake(0, 1); // 在垂直向下方向 1000 点/平方秒 的速度
[self.animator addBehavior:gravity];
}
|
运行项目可以看到效果:
UICollisionBehavior
碰撞行为,指定一个边界,物体在到达这个边界的时候会发生碰撞行为。通过实现 UICollisionBehaviorDelegate 可以跟踪物体什么时候开始碰撞和结束碰撞。
现在将下面代码添加到 [self.animator addBehavior:gravity];
之后
|
UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:balls];
collision.translatesReferenceBoundsIntoBoundary = YES;
collision.collisionMode = UICollisionBehaviorModeEverything;
[self.animator addBehavior:collision];
|
现在运行项目:
UICollisionBehavior通过下面两个方法来添加碰撞边界,可以根据贝塞尔曲线或者一条直线生成碰撞边界。
| - (void)addBoundaryWithIdentifier:(id <NSCopying>)identifier forPath:(UIBezierPath*)bezierPath;
- (void)addBoundaryWithIdentifier:(id <NSCopying>)identifier fromPoint:(CGPoint)p1 toPoint:(CGPoint)p2;
|
UICollisionBehavior 里的 item 每次发生碰撞都可以通过 delegate 来监听事件。
| // item 与 item 之间开始碰撞。
- (void)collisionBehavior:(UICollisionBehavior*)behavior beganContactForItem:(id <UIDynamicItem>)item1 withItem:(id <UIDynamicItem>)item2 atPoint:(CGPoint)p;
// item 与 item 之间结束碰撞。
- (void)collisionBehavior:(UICollisionBehavior*)behavior endedContactForItem:(id <UIDynamicItem>)item1 withItem:(id <UIDynamicItem>)item2;
// item 和边界开始碰撞
- (void)collisionBehavior:(UICollisionBehavior*)behavior beganContactForItem:(id <UIDynamicItem>)item withBoundaryIdentifier:(id <NSCopying>)identifier atPoint:(CGPoint)p;
// item 和边界结束碰撞
- (void)collisionBehavior:(UICollisionBehavior*)behavior endedContactForItem:(id <UIDynamicItem>)item withBoundaryIdentifier:(id <NSCopying>)identifier;
|
让我们为项目添加碰撞行为的 delegate ,修改 ViewController.m 为下面样子:
| @interface ViewController () <UICollisionBehaviorDelegate>
@property (strong, nonatomic) UIView *squareView;
@property (strong, nonatomic) UIDynamicAnimator *animator;
@end
@implementation BeginnerViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.squareView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
self.squareView.backgroundColor = [UIColor orangeColor];
[self.view addSubview:self.squareView];
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
UIGravityBehavior *gravity = [[UIGravityBehavior alloc] initWithItems:@[self.squareView]];
[self.animator addBehavior:gravity];
UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:@[self.squareView]];
collision.translatesReferenceBoundsIntoBoundary = YES;
collision.collisionDelegate = self;
[self.animator addBehavior:collision];
}
- (void)collisionBehavior:(UICollisionBehavior*)behavior beganContactForItem:(id <UIDynamicItem>)item withBoundaryIdentifier:(id <NSCopying>)identifier atPoint:(CGPoint)p
{
// 结束碰撞为 squareView 设置一个随机背景
self.squareView.backgroundColor = [UIColor colorWithRed:(float)rand() / RAND_MAX
green:(float)rand() / RAND_MAX
blue:(float)rand() / RAND_MAX
alpha:1];
}
- (void)collisionBehavior:(UICollisionBehavior*)behavior endedContactForItem:(id <UIDynamicItem>)item withBoundaryIdentifier:(id <NSCopying>)identifier
{
// 结束碰撞为 squareView 设置一个随机背景
self.squareView.backgroundColor = [UIColor colorWithRed:(float)rand() / RAND_MAX
green:(float)rand() / RAND_MAX
blue:(float)rand() / RAND_MAX
alpha:1];
}
@end
|
现在运行项目将会看到如下效果:
UIAttachmentBehavior
附着行为,让物体附着在某个点或另外一个物体上。可以设置附着点的到物体的距离,阻尼系数和振动频率等。
在 ViewController.m 的 - (void)viewDidAppear:(BOOL)animated
末尾添加如下代码:
| UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:self.squareView attachedToAnchor:self.squareView.center];
attachment.length = 50;
attachment.damping = 0.5;
attachment.frequency = 1;
[self.animator addBehavior:attachment];
|
运行项目看到效果:
属性详细说明
|
@property(readwrite, nonatomic) CGPoint anchorPoint
@property(readonly, nonatomic) UIAttachmentBehaviorType attachedBehaviorType
@property(readwrite, nonatomic) CGFloat damping
@property(readwrite, nonatomic) CGFloat frequency
@property(nonatomic, readonly, copy) NSArray *items
@property(readwrite, nonatomic) CGFloat length
|
UIDynamicItemBehavior
物体属性,如密度、弹性系数、摩擦系数、阻力、转动阻力等。
接下来我们修改物体的物理属性,为了能看到这个效果,我们先删除 UIAttachmentBehavior 相关的代码,并在 - (void)viewDidAppear:(BOOL)animated
末尾添加如下代码:
| UIDynamicItemBehavior *itemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.squareView]];
itemBehavior.elasticity = 0.8; // 改变弹性
itemBehavior.allowsRotation = YES; // 允许旋转
[itemBehavior addAngularVelocity:1 forItem:self.squareView]; // 让物体旋转
[self.animator addBehavior:itemBehavior];
|
现在我们看到,方块的弹性变大了,并且伴随着旋转:
属性详细说明
|
@property (readwrite, nonatomic) CGFloat elasticity;
@property (readwrite, nonatomic) CGFloat friction;
@property (readwrite, nonatomic) CGFloat density;
@property (readwrite, nonatomic) CGFloat resistance;
@property (readwrite, nonatomic) CGFloat angularResistance;
@property (readwrite, nonatomic) BOOL allowsRotation;
|
UIPushBehavior
对物体施加力,可以是持续性的力也可以是一次性的力。用一个向量(CGVector)来表示力的方向和大小。
这次我们通过手势来动态的为物体添加推力,首先注释重力行为的相关代码,然后在 - (void)viewDidAppear:(BOOL)animated
末尾添加如下代码:
| UITapGestureRecognizer *viewTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapViewHandler:)];
[self.view addGestureRecognizer:viewTapGesture];
|
在ViewController.m中添加方法:
| - (void)tapViewHandler:(UITapGestureRecognizer *)gestureRecognizer
{
UIPushBehavior *push = [[UIPushBehavior alloc] initWithItems:@[self.squareView] mode:UIPushBehaviorModeInstantaneous];
CGPoint location = [gestureRecognizer locationInView:self.view];
CGPoint itemCenter = self.squareView.center;
push.pushDirection = CGVectorMake((location.x - itemCenter.x) / 100, (location.y - itemCenter.y) / 100);
[self.animator addBehavior:push];
}
|
上面代码会根据手指点击,生成一个由物体中心点指向点击位置的点的向量,通过设置UIPushBehavior的pushDirection让物体产生一个推向点击点的力。说得有点抽象,看看现实效果
主要的属性和方法
|
@property (nonatomic, readonly) UIPushBehaviorMode mode;
@property(nonatomic, readwrite) BOOL active
@property (readwrite, nonatomic) CGVector pushDirection;
|
UISnapBehavior
将一个物体钉在某一点。它只有一个初始化方法和一个属性。
| // 根据 item 和 point 来确定一个 item 要被定到哪个点上。
- (instancetype)initWithItem:(id <UIDynamicItem>)item snapToPoint:(CGPoint)point;
// 减震系数,范围在0.0~1.0
@property (nonatomic, assign) CGFloat damping;
|
这个就留给大家自己实验了。XD
Demo
整个 Demo 的代码:
| @interface ViewController () <UICollisionBehaviorDelegate>
@property (strong, nonatomic) UIView *squareView;
@property (strong, nonatomic) UIDynamicAnimator *animator;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.squareView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
self.squareView.backgroundColor = [UIColor orangeColor];
[self.view addSubview:self.squareView];
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:@[self.squareView]];
collision.translatesReferenceBoundsIntoBoundary = YES;
collision.collisionDelegate = self;
[self.animator addBehavior:collision];
UIDynamicItemBehavior *itemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.squareView]];
itemBehavior.elasticity = 0.8;
itemBehavior.allowsRotation = YES;
itemBehavior.resistance = 0.5;
[itemBehavior addAngularVelocity:1 forItem:self.squareView];
[self.animator addBehavior:itemBehavior];
UITapGestureRecognizer *viewTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapViewHandler:)];
[self.view addGestureRecognizer:viewTapGesture];
}
- (void)tapViewHandler:(UITapGestureRecognizer *)gestureRecognizer
{
UIPushBehavior *push = [[UIPushBehavior alloc] initWithItems:@[self.squareView] mode:UIPushBehaviorModeInstantaneous];
CGPoint location = [gestureRecognizer locationInView:self.view];
CGPoint itemCenter = self.squareView.center;
push.pushDirection = CGVectorMake((location.x - itemCenter.x) / 100, (location.y - itemCenter.y) / 100);
[self.animator addBehavior:push];
}
- (void)addViewAtPoint:(CGPoint)center
{
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)];
view.backgroundColor = [UIColor grayColor];
view.center = center;
[self.view addSubview:view];
}
- (void)collisionBehavior:(UICollisionBehavior*)behavior beganContactForItem:(id <UIDynamicItem>)item withBoundaryIdentifier:(id <NSCopying>)identifier atPoint:(CGPoint)p
{
self.squareView.backgroundColor = [UIColor colorWithRed:(float)rand() / RAND_MAX
green:(float)rand() / RAND_MAX
blue:(float)rand() / RAND_MAX
alpha:1];
}
- (void)collisionBehavior:(UICollisionBehavior*)behavior endedContactForItem:(id <UIDynamicItem>)item withBoundaryIdentifier:(id <NSCopying>)identifier
{
self.squareView.backgroundColor = [UIColor colorWithRed:(float)rand() / RAND_MAX
green:(float)rand() / RAND_MAX
blue:(float)rand() / RAND_MAX
alpha:1];
}
@end
|
现实中的使用场景
图片来自 teehan+lax
图片来自 teehan+lax
图片来自 obj.io
总结
UIKit Dynamic 为开发者提供了模拟现实的交互动画。
从例子中来看,使用 UIKit Dynamic 实际上真的很简单,只需要几行或者十几行代码就能写出很棒的模拟真实世界的交互效果。
UIKit Dynamic 是 UIKit 的一部分,这意味着使用它不需要添加其它额外的framework,所以如果应用只支持 iOS 7 以上,可以在项目中多多使用,让应用中的动画效果瞬间提升好几个档次。