UIDynamicAnimator

什麼是 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,動力項,相當於現實世界中的一個基本物體

image

這三個類的結構是:UIDynamicAnimator 需要一個 refrence view 作爲物理引擎的座標系統,再根據不同需求添加各種動力行爲(UIDynamicBehavior),而每個動力行爲都可以指定一個或多個動力項(UIDynamicItem),常用的動力項就是一個普通的 View。

UIDynamicAnimator

UIDynamicAnimator 封裝了底層 iOS 物理引擎,爲動力項(UIDynamicItem)提供物理相關的功能和動畫,併爲這些動畫提供上下文。Animator 作爲底層 iOS 物理引擎和動力項(UIDynamicItem)之間的中介,通過- (void)addBehavior:(UIDynamicBehavior *)behavior; 方法添加不同的動力行爲,讓動力項擁有物理功能和動畫。

現在來看看 UIDynamicAnimator 都有哪些方法:

  • 初始化和管理一個 Dynamic Animator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 傳入一個 Reference view 創建一個 Dynamic Animator
- (instancetype)initWithReferenceView:(UIView*)view;

// 獲取在 CGRect 內所有的動力項,這個 CGRect 是基於 Reference view 的二維座標系統的
- (NSArray*)itemsInRect:(CGRect)rect;

// 添加動力行爲
- (void)addBehavior:(UIDynamicBehavior *)behavior;

// 刪除指定的動力行爲
- (void)removeBehavior:(UIDynamicBehavior *)behavior;

// 刪除所有的動力行爲
- (void)removeAllBehaviors;
  • 獲取 Dynamic Animator’s 的狀態
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 是否正在運行
@property (nonatomic, readonly, getter = isRunning) BOOL running;

// 獲取所有的 Behaviors
@property (nonatomic, readonly, copy) NSArray* behaviors;

@property (nonatomic, readonly) UIView* referenceView;

// 這個 delegate 中有兩個回調方法,一個是在 animator 暫停的時候調用,一個是在將要恢復的時候調用
@property (nonatomic, assign) id <UIDynamicAnimatorDelegate> delegate;

// 已經運行了多久的時間,是一個 NSTimeInterval
- (NSTimeInterval)elapsedTime;

// 如果動力項不是通過 animator 自動計算改變狀態,比如,通過代碼強制改變一個 item 的 transfrom 時,可以用這個方法通知 animator 這個 item 的改變。如果不用這個方法,animator 之後的動畫會覆蓋代碼中對 item 做的改變,相當於代碼改變 transform 變得沒有意義。
- (void)updateItemUsingCurrentState:(id <UIDynamicItem>)item;
  • Collection View Additions
1
2
3
4
- (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout*)layout // 傳入一個 CollectionViewLayout 創建一個 Dynamic Animator
 layoutAttributesForCellAtIndexPath:
 layoutAttributesForDecorationViewOfKind:atIndexPath:
 layoutAttributesForSupplementaryViewOfKind:atIndexPath:

從這裏開始,讓我們先創建一個項目,取名 DynamicDemo,選擇 single view project。

在 ViewController.m 文件修改成如下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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 的主要方法和屬性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在將要進行動畫時的 block 回調
@property(nonatomic, copy) void (^action)(void)

// 添加到該動態行爲中的子動態行爲
@property(nonatomic, readonly, copy) NSArray *childBehaviors

//  該動態行爲相關聯的dynamicAnimator
@property(nonatomic, readonly) UIDynamicAnimator *dynamicAnimator

//添加一個子動態行爲
- (void)addChildBehavior:(UIDynamicBehavior *)behavior

// 移除一個子動態行爲
- (void)removeChildBehavior:(UIDynamicBehavior *)behavior

// 當該動態行爲將要被添加到一個UIDynamicAnimator中時,這個方法會被調用。
- (void)willMoveToAnimator:(UIDynamicAnimator *)dynamicAnimator

在開發中,大部分情況下使用 UIDynamicBehavior 的子類就足夠了,因爲UIKit 中已經有幾個現成的模擬現實的 UIDynamicBehavior 類。

UIDynamicBehavior的子類有:

UIGravityBehavior

重力行爲,可以指定重力的方向和大小。用gravityDirection指定一個向量,或者設置 angle 和 magnitude。

打開剛纔的項目,DynamicDemo,在 ViewController.m 中添加如下代碼:

1
2
3
4
5
6
7
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    UIGravityBehavior *gravity = [[UIGravityBehavior alloc] initWithItems:@[self.squareView]]; // 創建一個重力行爲
    gravity.gravityDirection = CGVectorMake(0, 1); // 在垂直向下方向 1000 點/平方秒 的速度
    [self.animator addBehavior:gravity];
}

運行項目可以看到效果:

image

UICollisionBehavior

碰撞行爲,指定一個邊界,物體在到達這個邊界的時候會發生碰撞行爲。通過實現 UICollisionBehaviorDelegate 可以跟蹤物體什麼時候開始碰撞和結束碰撞。

現在將下面代碼添加到[self.animator addBehavior:gravity];之後

1
2
3
4
5
6
7
8
9
10
// 創建碰撞行爲
UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:balls];

// 指定 Reference view 的邊界爲可碰撞邊界
collision.translatesReferenceBoundsIntoBoundary = YES;

// UICollisionBehaviorModeItems:item 只會和別的 item 發生碰撞;UICollisionBehaviorModeBoundaries:item 只和碰撞邊界進行碰撞;UICollisionBehaviorModeEverything:item 和 item 之間會發生碰撞,也會和指定的邊界發生碰撞。
collision.collisionMode = UICollisionBehaviorModeEverything;

[self.animator addBehavior:collision];

現在運行項目:

image

UICollisionBehavior通過下面兩個方法來添加碰撞邊界,可以根據貝塞爾曲線或者一條直線生成碰撞邊界。

1
2
- (void)addBoundaryWithIdentifier:(id <NSCopying>)identifier forPath:(UIBezierPath*)bezierPath;
- (void)addBoundaryWithIdentifier:(id <NSCopying>)identifier fromPoint:(CGPoint)p1 toPoint:(CGPoint)p2;

UICollisionBehavior 裏的 item 每次發生碰撞都可以通過 delegate 來監聽事件。

1
2
3
4
5
6
7
8
9
10
11
12
// 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 爲下面樣子:

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
47
48
49
50
51
@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];
}

#pragma mark - UICollisionBehaviorDelegate

- (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

現在運行項目將會看到如下效果:

collision delegate

UIAttachmentBehavior

附着行爲,讓物體附着在某個點或另外一個物體上。可以設置附着點的到物體的距離,阻尼係數和振動頻率等。

在 ViewController.m 的 - (void)viewDidAppear:(BOOL)animated 末尾添加如下代碼:

1
2
3
4
5
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];

運行項目看到效果:

attachment

屬性詳細說明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// UIAttachmentBehaviorTypeAnchor類型的依賴行爲的錨點,錨點與行爲相關的動力動畫的座標系統有關。
@property(readwrite, nonatomic) CGPoint anchorPoint

// 吸附行爲的類型
@property(readonly, nonatomic) UIAttachmentBehaviorType attachedBehaviorType

// 描述吸附行爲減弱的阻力大小
@property(readwrite, nonatomic) CGFloat damping

// 吸附行爲震盪的頻率
@property(readwrite, nonatomic) CGFloat frequency

// 與吸附行爲相連的動態項目,當吸附行爲類型是UIAttachmentBehaviorTypeItems時有2個元素,當吸附行爲類型是UIAttachmentBehaviorTypeAnchor時只有一個元素。
@property(nonatomic, readonly, copy) NSArray *items

// 吸附行爲中的兩個吸附點之間的距離,通常用這個屬性來調整吸附的長度,可以創建吸附行爲之後調用。系統基於你創建吸附行爲的方法來自動初始化這個長度
@property(readwrite, nonatomic) CGFloat length

UIDynamicItemBehavior

物體屬性,如密度、彈性係數、摩擦係數、阻力、轉動阻力等。

接下來我們修改物體的物理屬性,爲了能看到這個效果,我們先刪除 UIAttachmentBehavior 相關的代碼,並在- (void)viewDidAppear:(BOOL)animated 末尾添加如下代碼:

1
2
3
4
5
6
UIDynamicItemBehavior *itemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.squareView]];
itemBehavior.elasticity = 0.8; // 改變彈性
itemBehavior.allowsRotation = YES; // 允許旋轉
[itemBehavior addAngularVelocity:1 forItem:self.squareView]; // 讓物體旋轉

[self.animator addBehavior:itemBehavior];

現在我們看到,方塊的彈性變大了,並且伴隨着旋轉:

item behavior

屬性詳細說明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 彈力,通常設置 0~1 之間
@property (readwrite, nonatomic) CGFloat elasticity;

// 摩擦力,0表示完全光滑無摩擦
@property (readwrite, nonatomic) CGFloat friction;

// 密度,一個 100x100 points(1 point 在 retina 屏幕上等於2像素,在普通屏幕上爲1像素。)大小的物體,密度1.0,在上面施加 1.0 的力,會產生 100 point/平方秒 的加速度。
@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 末尾添加如下代碼:

1
2
UITapGestureRecognizer *viewTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapViewHandler:)];
    [self.view addGestureRecognizer:viewTapGesture];

在ViewController.m中添加方法:

1
2
3
4
5
6
7
8
- (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讓物體產生一個推向點擊點的力。說得有點抽象,看看現實效果

push behavior

主要的屬性和方法

1
2
3
4
5
6
7
8
// 推力模式,UIPushBehaviorModeContinuous:持續型。UIPushBehaviorModeInstantaneous:一次性推力。
@property (nonatomic, readonly) UIPushBehaviorMode mode;

// 推力是否被激活,在激活狀態下,物體纔會受到推力效果
@property(nonatomic, readwrite) BOOL active

// 推力的大小和方向
@property (readwrite, nonatomic) CGVector pushDirection;

UISnapBehavior

將一個物體釘在某一點。它只有一個初始化方法和一個屬性。

1
2
3
4
5
// 根據 item 和 point 來確定一個 item 要被定到哪個點上。
- (instancetype)initWithItem:(id <UIDynamicItem>)item snapToPoint:(CGPoint)point;

// 減震係數,範圍在0.0~1.0
@property (nonatomic, assign) CGFloat damping;

這個就留給大家自己實驗了。XD

Demo

整個 Demo 的代碼:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
@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];
//    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];

    // 吸附於某一點,也可以吸附與某個實現了 UIDynamicItem 的對象
//    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];
//    [self addViewAtPoint:self.squareView.center];

    // 物體屬性:質量、摩擦、阻力等
    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];
}

#pragma mark - Helper

- (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];
}

#pragma mark - UICollisionBehaviorDelegate

- (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

現實中的使用場景

  • AlertView 彈出和隱藏

alertview

圖片來自teehan+lax

  • 類似於系統通知的彈性效果,側邊欄菜單彈性效果

slide menu

圖片來自teehan+lax

  • 類似於系統 Message 信息拉動時的彈簧效果

spring collection view

圖片來自obj.io

  • 還有很多使用場景期待大家共同挖掘補充

總結

UIKit Dynamic 爲開發者提供了模擬現實的交互動畫。

從例子中來看,使用 UIKit Dynamic 實際上真的很簡單,只需要幾行或者十幾行代碼就能寫出很棒的模擬真實世界的交互效果。

UIKit Dynamic 是 UIKit 的一部分,這意味着使用它不需要添加其它額外的framework,所以如果應用只支持 iOS 7 以上,可以在項目中多多使用,讓應用中的動畫效果瞬間提升好幾個檔次。

參考

2013 WWDC

Blog

轉自:http://vit0.com/blog/2014/03/08/ios-7-uikit-dynamic-xue-xi-zong-jie/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章