UIDynamic簡介
-
簡介:
- UIKit動力學最大的特點是將現實世界動力驅動的動畫引入了UIKit,比如動力,鉸鏈連接,碰撞,懸掛等效果,即將2D物理引擎引入了UIKit。
- 注意:UIKit動力學的引入,并不是為了替代CA或者UIView動畫,在絕大多數情況下CA或者UIView動畫仍然是最有方案,只有在需要引入逼真的交互設計的時候,才需要使用UIKit動力學它是作為現有交互設計和實現的一種補充。
-
其他2D仿真引擎:
- BOX2D:C語言框架,免費
- Chipmunk:C語言框架免費,其他版本收費
UIDynamic中的三個重要概念
Dynamic Animator:動畫者,為動力學元素提供物理學相關的能力及動畫,同時為這些元素提供相關的上下文,是動力學元素與底層iOS物理引擎之間的中介,將Behavior對象添加到Animator即可實現動力仿真。
Dynamic Animator Item:動力學元素,是任何遵守了UIDynamic協議的對象,從iOS7開始,UIView和UICollectionViewLayoutAttributes默認實現協議,如果自定義對象實現了該協議,即可通過Dynamic Animator實現物理仿真。
UIDynamicBehavior:仿真行為,是動力學行為的父類,基本的動力學行為類UIGravityBehavior、UICollisionBehavior、UIAttachmentBehavior、UISnapBehavior、UIPushbehavior以及UIDynamicItemBehavior均繼承自該父類。
項目搭建演練
模擬重力體驗物理仿真效果
-
要使用物理仿真,最基本的使用步驟是:
- 1> 要有一個 仿真者[UIDynamicAnimator] 用來仿真所有的物理行為
- 2> 要有物理 仿真行為[如重力UIGravity] 用來模擬重力的行為
- 3> 將物理仿真行為添加給仿真者實現仿真效果。
-
第一種情況——重力仿真
// 1. 誰來仿真?UIDynamicAnimator來負責仿真 UIDynamicAnimator *animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view]; // 2. 仿真個什么動作?自由落體 UIGravityBehavior *gravity = [[UIGravityBehavior alloc] initWithItems:@[view, redView]]; // 3. 開始仿真 [animator addBehavior:gravity];
-
重力仿真效果圖</br>
01重力效果無邊界檢測.gif -
第二種情況——增加邊緣檢測
- 默認情況下沒有任何阻擋控件直接掉出屏幕,可以通過添加邊緣檢測行為防止掉出。
// 3. 碰撞檢測 UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:@[view, redView]]; // 設置不要出邊界,碰到邊界會被反彈 collision.translatesReferenceBoundsIntoBoundary = YES; // 4. 開始仿真 [animator addBehavior:collision];
增加邊緣檢測效果圖</br>
- 第三種情況——旋轉
- 讓控件旋轉45°后,控件并不會倒下,因為控件的重心就在45°的那條線上。
- 如果修改為別的角度就會倒下
view.transform = CGAffineTransformMakeRotation(M_PI_4);
- 旋轉效果圖</br>
- 第四種情況——碰撞
- 再增加一個紅色的控件的時候就會發生碰撞的效果。
- 碰撞效果圖</br>
項目框架搭建
一、結構分析
-
為了演示其他的幾種行為效果,案例中需要用到
- UINavigationController[導航控制器],根控制器為列表控制器
- UITableViewController[列表控制器],用來展示所有的行為列表
- UIViewController[普通控制器],用來演示各種不同行為的效果
-
在顯示各種行為的普通控制器中有2個共同點:
- 相同的背景效果
- 都有一個小方塊
-
所以為了避免每個行為都要寫一個控制器,然后寫對應的背景及方塊圖片代碼,就抽出一個示例控制器,用來顯示所有的行為效果
- 只不過示例控制器要加載和顯示的view,要根據要展示的行為去加載不同的view(多態的合理運用)
二、代碼實現
1> 列表控制器
- 第一步加載顯示導航控制器及列表控制器
- 通過屬性列表或者數據源的方式加載所有的行為名詞
_dynamicArr = @[@"吸附行為", @"推動行為", @"剛性附著行為", @"彈性附著行為", @"碰撞檢測"];
- 通過給組尾設置一個空的view來隱藏多余行
self.tableView.tableFooterView = [[UIView alloc] init];
- 實現數據源方法顯示出來
#pragma mark - 數據源方法 // 幾行 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return _dynamicArr.count; } // 每行的具體內容 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { // 1. 設置可重用標識符 static NSString *ID = @"cell"; // 2. 根據可重用標識符去tableView 緩存區去取 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID]; // 3. 設置每行cell 的文字 cell.textLabel.text = _functions[indexPath.row]; return cell;
}
```
- 列表控制器效果圖</br>
2> 跳轉到演示控制器
- 實現代理方法,實現跳轉
- 在跳轉的時候將索引及cell的標題傳過去
#pragma mark - 代理方法
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// 1. 實例化一個仿真管理器
WPFDemoController *demoVc = [[WPFDemoController alloc] init];
// 2. 設置標題
demoVc.title = _dynamicArr[indexPath.row];
// 3. 傳遞功能類型
demoVc.function = (int)indexPath.row;
// 4. 跳轉界面
[self.navigationController pushViewController:demoVc animated:YES];
}
3> 演示控制器根據索引去加載不同的view
- 做一個基本的view只用來設置背景及方塊圖片及仿真者,其他的view在此基礎上添加功能,新建類 WPFBaseView
// 重寫其initWithFrame 方法,設置基本信息
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// 以平鋪的方式設置背景圖片
self.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"BackgroundTile"]];
// 設置方塊
UIImageView *boxView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Box1"]];
boxView.center = CGPointMake(200, 220);
[self addSubview:boxView];
self.boxView = boxView;
// 初始化仿真者
UIDynamicAnimator *animator = [[UIDynamicAnimator alloc] initWithReferenceView:self];
self.animator = animator;
}
return self;
}
- WPFDemoController:根據傳入的索引去判斷加載那個行為的view
// 在此之前要先在頭文件中定義枚舉類型
- (void)viewDidLoad {
[super viewDidLoad];
// 新建一個空的baseView
WPFBaseView *baseView = nil;
// 根據不同的功能類型選擇不同的視圖
// 運用了多態
switch (self.function) {
case kDemoFunctionSnap:
baseView = [[WPFSnapView alloc] init];
break;
case kDemoFunctionPush:
baseView = [[WPFPushView alloc] init];
break;
case kDemoFunctionAttachment:
baseView = [[WPFAttachmentView alloc] init];
break;
case kDemoFunctionSpring:
baseView = [[WPFSpringView alloc] init];
break;
case kDemoFunctionCollision:
baseView = [[WPFCollisionView alloc] init];
break;
default:
break;
}
baseView.frame = self.view.bounds;
[self.view addSubview:baseView];
}
- 加載不同view的效果</br>
吸附行為:WPFSnapView
- UISnapBehavior吸附行為
- 在點擊屏幕的時候獲取觸摸點
- 需要在創建吸附行為的時候指定要吸附的位置
- 創建好之后將吸附行為添加到仿真者上
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 0. 觸摸之前要清零之前的吸附事件,否則動作越來越小
[self.animator removeAllBehaviors];
// 1. 獲取觸摸對象
UITouch *touch = [touches anyObject];
// 2. 獲取觸摸點
CGPoint loc = [touch locationInView:self];
// 3 添加吸附事件
UISnapBehavior *snap = [[UISnapBehavior alloc] initWithItem:self.boxView snapToPoint:loc];
// 改變震動幅度,0表示振幅最大,1振幅最小
snap.damping = 0.5;
// 4. 將吸附事件添加到仿真者行為中
[self.animator addBehavior:snap];
}
- 吸附行為效果圖</br>
推動行為:WPFPushView
UIPushBehavior推動行為
-
介紹
推行為可以為一個視圖施加一個作用力,該力可以是持續的,也可以是一次性的
可以設置力的大小,方向和作用點等信息
-
屬性:
- mode: 推動類型(一次性推動或是持續推送)
- active: 是否激活,如果是一次性推動,需要激活
- angle: 推動角度
- 推動力量
實例化推行為
通過拖拽手勢獲取起始點及其他狀態的點
設置全局變量
@interface WPFPushView ()
{
UIImageView *_smallView; // 顯示在第一個觸摸點位置的圖片框
UIPushBehavior *_push; // 推動的行為
CGPoint _firstPoint; // 手指點擊的第一個點
CGPoint _currentPoint; // 當前觸摸點
}
@end
- 推行為的創建
// 重寫init 方法
- (instancetype)init {
if (self = [super init]) {
// 1. 添加藍色view
UIView *blueView = [[UIView alloc] initWithFrame:CGRectMake(150, 300, 20, 20)];
blueView.backgroundColor = [UIColor blueColor];
[self addSubview:blueView];
// 2. 添加圖片框,拖拽起點
UIImageView *smallView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"AttachmentPoint_Mask"]];
// 該圖片框默認是隱藏的,在觸摸屏幕的時候再顯示出來
smallView.hidden = YES;
[self addSubview:smallView];
// 建立全局關系
_smallView = smallView;
// 3. 添加推動行為
UIPushBehavior *push = [[UIPushBehavior alloc] initWithItems:@[self.boxView] mode:UIPushBehaviorModeInstantaneous];
[self.animator addBehavior:push];
_push = push;
// 4. 增加碰撞檢測
UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:@[blueView, self.boxView]];
collision.translatesReferenceBoundsIntoBoundary = YES;
[self.animator addBehavior:collision];
// 5. 添加拖拽手勢
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)];
[self addGestureRecognizer:pan];
}
return self;
}
- 通過拖拽手勢根據不同狀態去確定力的方向和大小
// 監聽開始拖拽的方法
- (void)panAction:(UIPanGestureRecognizer *)pan {
// 如果是剛開始拖拽,則設置起點處的小圓球
if (pan.state == UIGestureRecognizerStateBegan) {
_firstPoint = [pan locationInView:self];
_smallView.center = _firstPoint;
_smallView.hidden = NO;
// 如果當前拖拽行為正在移動
} else if (pan.state == UIGestureRecognizerStateChanged) {
_currentPoint = [pan locationInView:self];
// 重繪當前頁面
[self setNeedsDisplay];
// 如果當前拖拽行為結束
} else if (pan.state == UIGestureRecognizerStateEnded){
// 1. 計算偏移量
CGPoint offset = CGPointMake(_currentPoint.x - _firstPoint.x, _currentPoint.y - _firstPoint.y);
// 2. 計算角度
CGFloat angle = atan(offset.y / offset.x);
if (_currentPoint.x > _firstPoint.x) {
angle = angle - M_PI;
}
_push.angle = angle;
// 3. 計算距離
CGFloat distance = hypot(offset.y, offset.x);
// 4. 設置推動的力度,與線的長度成正比
_push.magnitude = directtion / 10;
// 5. 使單次推行為有效
_push.active = YES;
// 6. 將拖拽的線隱藏
_firstPoint = CGPointZero;
_currentPoint = CGPointZero;
// 7. 將起點的小圓隱藏
_smallView.hidden = YES;
// 8. 進行重繪
[self setNeedsDisplay];
}
}
- 設置劃線操作
- (void)drawRect:(CGRect)rect {
// 1. 開啟上下文對象
CGContextRef ref = UIGraphicsGetCurrentContext();
// 2. 獲取路徑對象
UIBezierPath *path = [UIBezierPath bezierPath];
// 3. 劃線
[path moveToPoint:_firstPoint];
[path addLineToPoint:_currentPoint];
CGContextAddPath(ref, path.CGPath);
// 4. 設置線寬
path.lineWidth = 7;
// 5. 線的顏色
[[UIColor greenColor] setStroke];
// 6. 渲染
[path stroke];
}
- 推行為效果圖</br>
剛性附著行為:WPFAttachmentView
-
簡介:
- 附著行為是描述一個視圖與一個錨點或者另一個視圖相連接的情況
- 附著行為描述的是兩點之間的連接情況,可以模擬剛性或者彈性連接
- 在多個物理鍵設定多個UIAttachment,可以模擬多物體連接。
-
屬性
- attachedBehaviorType: 連接類型(連接到錨點或視圖)
- items: 連接到視圖數組
- anchorPoint: 連接錨點
- length: 距離連接錨點的距離
注意: 只要設置了以下兩個屬性,即為彈性連接
damping: 振幅大小
frequency: 震動頻率
設置全局變量
@interface WPFPushView ()
{
// 附著點圖片框
UIImageView *_anchorImgView;
// 參考點圖片框(boxView 內部)
UIImageView *_offsetImgView;
}
@end
- 創建附著行為
- (instancetype)init {
if (self = [super init]) {
// 1. 設置boxView 的中心點
self.boxView.center = CGPointMake(200, 200);
// 2. 添加附著點
CGPoint anchorPoint = CGPointMake(200, 100);
UIOffset offset = UIOffsetMake(20, 20);
// 3. 添加附著行為
UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:self.boxView offsetFromCenter:offset attachedToAnchor:anchorPoint];
[self.animator addBehavior:attachment];
self.attachment = attachment;
// 4. 設置附著點圖片(即直桿與被拖拽圖片的連接點)
UIImage *image = [UIImage imageNamed:@"AttachmentPoint_Mask"];
UIImageView *anchorImgView = [[UIImageView alloc] initWithImage:image];
anchorImgView.center = anchorPoint;
[self addSubview:anchorImgView];
_anchorImgView = anchorImgView;
// 3. 設置參考點
_offsetImgView = [[UIImageView alloc] initWithImage:image];
CGFloat x = self.boxView.bounds.size.width * 0.5 + offset.horizontal;
CGFloat y = self.boxView.bounds.size.height * 0.5 + offset.vertical;
_offsetImgView.center = CGPointMake(x, y);
[self.boxView addSubview:_offsetImgView];
// 4. 增加拖拽手勢
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)];
[self addGestureRecognizer:pan];
}
return self;
}
- 添加拖拽手勢,在拖拽手勢移動的時候根據附著點及其軸點去繪制線段
- 通過兩個屬性保存附著點,及軸點
// 拖拽的時候會調用的方法
- (void)panAction:(UIPanGestureRecognizer *)pan {
// 1. 獲取觸摸點
CGPoint loc = [pan locationInView:self];
// 2. 修改附著行為的附著點
_anchorImgView.center = loc;
self.attachment.anchorPoint = loc;
// 3. 進行重繪
[self setNeedsDisplay];
}
- 在繪制的時候需要注意將圖片框的軸點進行坐標轉換
- (void)drawRect:(CGRect)rect {
// 1.獲取圖形上下文
CGContextRef context = UIGraphicsGetCurrentContext();
// 2.設置路徑起點
CGContextMoveToPoint(context, _anchorImage.center.x, _anchorImage.center.y);
// 2.2設置路徑畫線的點,注意需要將軸點的坐標進行轉換
// 使得兩個點的坐標位于同一個坐標系下
// addline
// 去偏移點相對于父視圖的坐標
CGPoint p = [self convertPoint:_offsetImage.center fromView:self.box];
CGContextAddLineToPoint(context, p.x, p.y);
// 2.3設置虛線樣式
CGFloat lengths[] = {10.0f, 8.0f};
CGContextSetLineDash(context, 0.0, lengths, 2);
// 2.4設置線寬
CGContextSetLineWidth(context, 5.0f);
// 3.渲染,繪制路徑
CGContextDrawPath(context, kCGPathStroke);
}
- 剛性附著行為效果圖</br>
- 中心點沒有偏移</br>
* 中心點偏移</br>
彈性附著行為:WPFSpringView
- 彈性附著行為與剛性附著行為類似,只需要設置兩個屬性就好了。
// 振幅
self.attachment.damping = 0.1f;
// 頻率
self.attachment.frequency = 1.0f;
- 彈性附著行為的view只需要繼承剛性附著行為就可以了。
// WPFAttachView是剛性附著行為的view,WPFSpringView為彈性附著行為的view
@interface WPFSpringView : WPFAttachView
- 但是需要在后面需要修改彈性附著行為的效果,所以要將剛性附著行為內部的附著行為暴露在.h文件中
@property (nonatomic, weak) UIAttachmentBehavior *attachment;
- 1.只設置了振幅和頻率的效果</br>
// 振幅
self.attachment.damping = 0.1f;
// 頻率
self.attachment.frequency = 1.0f;
- 2.通過KVO監聽方塊的中心點的變化,實時去更新繪圖后的效果
// KVO監聽boxcenter的改變
[self.box addObserver:self forKeyPath:@"center" options:NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
[self setNeedsDisplay];
}
- 3.增加了重力后的效果</br>
// 添加重力
UIGravityBehavior *gravity = [[UIGravityBehavior alloc] initWithItems:@[self.box]];
[self.animator addBehavior:gravity];
- 4.添加了碰撞檢測后的效果
// 添加碰撞檢測
UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:@[self.box]];
collision.translatesReferenceBoundsIntoBoundary = YES;
[self.animator addBehavior:collision];
碰撞檢測
- 在碰撞界面添加一個紅色的view
- 1.紅色的view也添加到碰撞檢測行為中</br>
// 增加一個紅色的條狀view
UIView *redV = [[UIView alloc] initWithFrame:CGRectMake(0, 400, 180, 30)];
redV.backgroundColor = [UIColor redColor];
[self addSubview:redV];
- 2.紅色view不添加到任何行為中</br>
- 3.如果只想在碰到紅色view的時候方塊掉下去,紅色view不動,需要給碰撞檢測增減一條碰撞邊界
#pragma mark - 在紅色view的上方添加一個邊界到邊界檢測行為中
// 添加一個邊界,起點,終點作為一條直線。
CGPoint fromP = CGPointMake(0, 400);
CGPoint toP = CGPointMake(180, 400);
[collision addBoundaryWithIdentifier:@"line" fromPoint:fromP toPoint:toP];
- 4.也可以增加一個路徑
注意:在增加路徑的時候碰撞會碰在弧線兩個頂點的連線的位置上,可以通過填充的模式看的更清楚一些。
// 添加路徑
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(200, 300) radius:100 startAngle:M_PI_2 endAngle:M_PI clockwise:YES];
[collision addBoundaryWithIdentifier:@"bcd" forPath:path];
- 5.補充
- 各種碰撞行為都有一個action的block,可以通過這個block監聽在碰撞行為過程中的動態信息。
collision.action = ^(){
NSLog(@"%@", NSStringFromCGRect(self.box.frame));
};
* 可以設置邊緣檢測的代理,根據identifer標記去區分碰撞到哪一個邊界了。
objc
// 設置代理
collision.collisionDelegate = self;
#pragma mark - UICollisionBehaviorDelegate
- (void)collisionBehavior:(UICollisionBehavior*)behavior beganContactForItem:(id <UIDynamicItem>)item withBoundaryIdentifier:(nullable id <NSCopying>)identifier atPoint:(CGPoint)p {
NSLog(@"%@", identifier);
}
```
多視圖附加行為
- 1> 創建視圖
- 2> 添加視圖之間的附著行為
- 3> 添加重力行為
- 4> 添加邊緣檢測行為
- 5> 添加拖拽手勢,單獨處理頭部視圖的附著行為。
- 在開始拖拽式,實例化拖拽行為。設置附著點為觸摸點
- 在拖動過程中,實時修改附著點為觸摸點
- 在拖動結束后,移除附著行為,否則不能正常使用。
- 效果圖</br>
- 重力在下</br>
* 重力在上</br>
-
重力的方向可以通過
@property (readwrite, nonatomic) CGVector gravityDirection;去設置- 根據x,y值的幾何方向去確定重力的方向。
-
界面效果:
- 有9個圓形的球,一個比較大的作為頭部。
- 在從屏幕任意位置拖動的時候所有的圓球都成一條串的效果,沿著重力方向下垂。
- 抬起手指后,自由墜落。
-
1.創建9個view,并設置圓角半徑作為圓形,將最后一個修改為更大的效果。
// 添加9個子控件 CGFloat startX = 20; CGFloat startY = 100; CGFloat r = 10; NSMutableArray *arrM = [NSMutableArray arrayWithCapacity:9]; for (int i = 0; i < 9; i++) { CGFloat x = startX + 2 * r * i; CGFloat y = startY; CGFloat width = 2 * r; CGFloat heigth = width; UIView *v = [[UIView alloc] initWithFrame:CGRectMake(x, y, width, heigth)]; v.backgroundColor = [UIColor colorWithRed:arc4random_uniform(256)/255.0 green:arc4random_uniform(256)/255.0 blue:arc4random_uniform(256)/255.0 alpha:1.0]; v.layer.cornerRadius = r; if (i == 8) { r = 20; v.backgroundColor = [UIColor greenColor]; v.frame = CGRectMake(v.frame.origin.x, v.frame.origin.y - 10, 2 * r, 2 * r); v.layer.cornerRadius = r; } [self.view addSubview:v]; // 保存到集合中 [arrM addObject:v]; }
9個圓球效果圖</br>
- 遍歷集合中所有的元素并添加附加行為
- 最后一個要留著,單獨處理
// 添加吸附吸附行為 for (int i = 0; i < 8; i++) { UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:arrM[i] attachedToItem:arrM[i+1]]; [_animator addBehavior:attachment]; }
- 給所有的元素添加重力行為
// 重力仿真
UIGravityBehavior *gravity = [[UIGravityBehavior alloc] initWithItems:arrM];
// 指定重力的方向
gravity.gravityDirection = CGVectorMake(0.0, 1.0);
[_animator addBehavior:gravity];
- 添加邊緣檢測行為
// 邊緣檢測
UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:arrM];
collision.translatesReferenceBoundsIntoBoundary = YES;
[_animator addBehavior:collision];
- 添加拖拽手勢,在拖拽手勢內部根據狀態進行處理
- 在開始拖拽的時候,給頭部的view實例化附著行為,附著點就是觸摸點。
- 在拖拽過程中,附加行為的附著點仍是觸摸點。
- 在拖拽結束后,將附著行為從仿真者中移除。
CGPoint loc = [pan locationInView:self.view];
if (UIGestureRecognizerStateBegan == pan.state) {
// 開始拖拽,實例化附加行為
_attachment = [[UIAttachmentBehavior alloc] initWithItem:_headView attachedToAnchor:loc];
[_animator addBehavior:_attachment];
} else if (UIGestureRecognizerStateChanged == pan.state) {
// 拖拽過程中
_attachment.anchorPoint = loc;
} else if (UIGestureRecognizerStateEnded == pan.state){
// 結束拖拽
[_animator removeBehavior:_attachment];
}