本文中所有代码演示均有GitHub源码,点击下载
简介:
UIKit动力学最大的特点是将现实世界动力驱动的动画引入了UIKit,比如动力,铰链连接,碰撞,悬挂等效果,即将2D物理引擎引入了UIKit。注意:UIKit动力学的引入,并不是为了替代CA或者UIView动画,在绝大多数情况下CA或者UIView动画仍然是最有方案,只有在需要引入逼真的交互设计的时候,才需要使用UIKit动力学它是作为现有交互设计和实现的一种补充。其他2D仿真引擎:
BOX2D:C语言框架,免费Chipmunk:C语言框架免费,其他版本收费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]; 重力仿真效果图 01重力效果无边界检测.gif第二种情况——增加边缘检测
默认情况下没有任何阻挡控件直接掉出屏幕,可以通过添加边缘检测行为防止掉出。
// 3. 碰撞检测 UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:@[view, redView]]; // 设置不要出边界,碰到边界会被反弹 collision.translatesReferenceBoundsIntoBoundary = YES; // 4. 开始仿真 [animator addBehavior:collision]; 增加边缘检测效果图 02重力有边界.gif 第三种情况——旋转 让控件旋转45°后,控件并不会倒下,因为控件的重心就在45°的那条线上。如果修改为别的角度就会倒下 view.transform = CGAffineTransformMakeRotation(M_PI_4); 旋转效果图 03旋转.gif 第四种情况——碰撞再增加一个红色的控件的时候就会发生碰撞的效果。碰撞效果图为了演示其他的几种行为效果,案例中需要用到
UINavigationController[导航控制器],根控制器为列表控制器UITableViewController[列表控制器],用来展示所有的行为列表UIViewController[普通控制器],用来演示各种不同行为的效果在显示各种行为的普通控制器中有2个共同点:
相同的背景效果都有一个小方块所以为了避免每个行为都要写一个控制器,然后写对应的背景及方块图片代码,就抽出一个示例控制器,用来显示所有的行为效果
只不过示例控制器要加载和显示的view,要根据要展示的行为去加载不同的view(多态的合理运用)第一步加载显示导航控制器及列表控制器
通过属性列表或者数据源的方式加载所有的行为名词 _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; } 列表控制器效果图 05行为列表.png在跳转的时候将索引及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]; }介绍
推行为可以为一个视图施加一个作用力,该力可以是持续的,也可以是一次性的可以设置力的大小,方向和作用点等信息
属性:
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]; } 推行为效果图 08推行为.gif简介:
附着行为是描述一个视图与一个锚点或者另一个视图相连接的情况附着行为描述的是两点之间的连接情况,可以模拟刚性或者弹性连接在多个物理键设定多个UIAttachment,可以模拟多物体连接。属性
attachedBehaviorType: 连接类型(连接到锚点或视图)items: 连接到视图数组anchorPoint: 连接锚点length: 距离连接锚点的距离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); } 刚性附着行为效果图 中心点没有偏移 09刚性附着行为.gif * 中心点偏移</br> 09刚性附着行为2.gif弹性附着行为与刚性附着行为类似,只需要设置两个属性就好了。
// 振幅 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.只设置了振幅和频率的效果 // 振幅 self.attachment.damping = 0.1f; // 频率 self.attachment.frequency = 1.0f; 10弹性附着行为.gif2.通过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]; } 10弹性附着行为2.gif 3.增加了重力后的效果 // 添加重力 UIGravityBehavior *gravity = [[UIGravityBehavior alloc] initWithItems:@[self.box]]; [self.animator addBehavior:gravity]; 10弹性附着行为3.gif 10弹性附着行为4.gif 4.添加了碰撞检测后的效果 // 添加碰撞检测 UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:@[self.box]]; collision.translatesReferenceBoundsIntoBoundary = YES; [self.animator addBehavior:collision]; 10弹性附着行为5.gif5.补充
各种碰撞行为都有一个action的block,可以通过这个block监听在碰撞行为过程中的动态信息。 collision.action = ^(){ NSLog(@"%@", NSStringFromCGRect(self.box.frame)); };可以设置边缘检测的代理,根据identifer标记去区分碰撞到哪一个边界了。
// 设置代理 collision.collisionDelegate = self; #pragma mark - UICollisionBehaviorDelegate - (void)collisionBehavior:(UICollisionBehavior*)behavior beganContactForItem:(id <UIDynamicItem>)item withBoundaryIdentifier:(nullable id <NSCopying>)identifier atPoint:(CGPoint)p { NSLog(@"%@", identifier); }界面效果:
有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个圆球效果图 13圆球.png 遍历集合中所有的元素并添加附加行为 最后一个要留着,单独处理 // 添加吸附吸附行为 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]; }