iOS开发拓展篇—UIDynamic(简单介绍)
一、简单介绍
1.什么是UIDynamic
UIDynamic是从iOS 7开始引入的一种新技术,隶属于UIKit框架
可以认为是一种物理引擎,能模拟和仿真现实生活中的物理现象
如:重力、弹性碰撞等现象
2.物理引擎的价值
广泛用于游戏开发,经典成功案例是“愤怒的小鸟”
让开发人员可以在远离物理学公式的情况下,实现炫酷的物理仿真效果
提高了游戏开发效率,产生更多优秀好玩的物理仿真游戏
3.知名的2D物理引擎
Box2d
Chipmunk
二、使用步骤
要想使用UIDynamic来实现物理仿真效果,大致的步骤如下
(1)创建一个物理仿真器(顺便设置仿真范围)
(2)创建相应的物理仿真行为(顺便添加物理仿真元素)
(3)将物理仿真行为添加到物理仿真器中 开始仿真
三、相关说明
1.三个概念
(1)谁要进行物理仿真?
物理仿真元素(Dynamic Item)
(2)执行怎样的物理仿真效果?怎样的动画效果?
物理仿真行为(Dynamic Behavior)
(3)让物理仿真元素执行具体的物理仿真行为
物理仿真器(Dynamic Animator)
2.物理仿真元素
注意:
不是任何对象都能做物理仿真元素
不是任何对象都能进行物理仿真
物理仿真元素要素:
任何遵守了UIDynamicItem协议的对象
UIView默认已经遵守了UIDynamicItem协议,因此任何UI控件都能做物理仿真
UICollectionViewLayoutAttributes类默认也遵守UIDynamicItem协议
3.物理仿真行为
(1)UIDynamic提供了以下几种物理仿真行为
UIGravityBehavior:重力行为
UICollisionBehavior:碰撞行为
UISnapBehavior:捕捉行为
UIPushBehavior:推动行为
UIAttachmentBehavior:附着行为
UIDynamicItemBehavior:动力元素行为
(2)物理仿真行为须知
上述所有物理仿真行为都继承自UIDynamicBehavior
所有的UIDynamicBehavior都可以独立进行
组合使用多种行为时,可以实现一些比较复杂的效果
4.物理仿真器
(1)物理仿真器须知
它可以让物理仿真元素执行物理仿真行为
它是UIDynamicAnimator类型的对象
(2)UIDynamicAnimator的初始化
- (instancetype)initWithReferenceView:(UIView *)view;
view参数:是一个参照视图,表示物理仿真的范围
5.物理仿真器的说明
(1)UIDynamicAnimator的常见方法
- (void)addBehavior:(UIDynamicBehavior *)behavior; //添加1个物理仿真行为
- (void)removeBehavior:(UIDynamicBehavior *)behavior; //移除1个物理仿真行为
- (void)removeAllBehaviors; //移除之前添加过的所有物理仿真行为
(2)UIDynamicAnimator的常见属性
@property (nonatomic, readonly) UIView* referenceView; //参照视图
@property (nonatomic, readonly, copy) NSArray* behaviors;//添加到物理仿真器中的所有物理仿真行为
@property (nonatomic, readonly, getter = isRunning) BOOL running;//是否正在进行物理仿真
@property (nonatomic, assign) id <UIDynamicAnimatorDelegate> delegate;//代理对象(能监听物理仿真器的仿真过程,比如开始和结束)
由于博客迁移至www.coderyi.com,文章请看http://www.coderyi.com/archives/426
UIkit动力学是UIkit框架中模拟真实世界的一些特性。
主要有UIDynamicAnimator类,通过这个类中的不同行为来实现一些动态特性。
它一般有两种初始化方法,先讲常见的第一种
动态特性的实现主要依靠它所添加的行为,通过以下方法进行添加和移除,
接下来介绍五个不同的行为,UIAttachmentBehavior(吸附),UICollisionBehavior(碰撞),UIGravityBehavior(重力),UIPushBehavior(推动),UISnapBehavior(捕捉)。另外还有一个辅助的行为UIDynamicItemBehavior,用来在item层级设定一些参数,比如item的摩擦,阻力,角阻力,弹性密度和可允许的旋转等等。
先讲吸附行为,
它的初始化方法
item是实现UIDynamicItem协议的id类型,这里设置吸附一个UIImageView的实例iv。offset可以设置吸附的偏移,anchor是设置锚点。
UIAttachmentBehavior有几个属性,例如damping,frequency。damping是阻尼数值,frequency是震动频率
直接上代码,实现一个pan手势,让一个image跟着手势跑
1 -(void)gesture:(UIPanGestureRecognizer *)gesture{ 2 CGPoint location = [gesture locationInView:self.view]; 3 CGPoint boxLocation = [gesture locationInView:iv]; 4 5 switch (gesture.state) { 6 case UIGestureRecognizerStateBegan:{ 7 NSLog(@"you touch started position %@",NSStringFromCGPoint(location)); 8 NSLog(@"location in image started is %@",NSStringFromCGPoint(boxLocation)); 9 10 [animator removeAllBehaviors]; 11 12 // Create an attachment binding the anchor point (the finger's current location) 13 // to a certain position on the view (the offset) 14 15 UIOffset centerOffset = UIOffsetMake(boxLocation.x - CGRectGetMidX(iv.bounds), 16 boxLocation.y - CGRectGetMidY(iv.bounds)); 17 attachmentBehavior = [[UIAttachmentBehavior alloc] initWithItem:iv 18 offsetFromCenter:centerOffset 19 attachedToAnchor:location]; 20 21 attachmentBehavior.damping=0.5; 22 attachmentBehavior.frequency=0.8; 23 24 25 // Tell the animator to use this attachment behavior 26 [animator addBehavior:attachmentBehavior]; 27 break; 28 } 29 case UIGestureRecognizerStateEnded: { 30 [animator removeBehavior:attachmentBehavior]; 31 32 break; 33 } 34 default: 35 [attachmentBehavior setAnchorPoint:[gesture locationInView:self.view]]; 36 break; 37 } 38 }
UIPushBehavior 可以为一个UIView施加一个力的作用,这个力可以是持续的,也可以只是一个冲量。我们可以指定力的大小,方向和作用点等等信息。
UIPushBehavior 有pushDirection、magnitude等属性,
1 //1 2 CGPoint velocity = [gesture velocityInView:self.view]; 3 CGFloat magnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y)); 4 5 if (magnitude > ThrowingThreshold) { 6 //2 7 pushBehavior = [[UIPushBehavior alloc] 8 initWithItems:@[iv] 9 mode:UIPushBehaviorModeInstantaneous]; 10 pushBehavior.pushDirection = CGVectorMake((velocity.x / 10) , (velocity.y / 10)); 11 pushBehavior.magnitude = magnitude / ThrowingvelocityPadding; 12 13 14 [animator addBehavior:pushBehavior]; 15 16 //3 17 // UIDynamicItemBehavior 其实是一个辅助的行为,用来在item层级设定一些参数,比如item的摩擦,阻力,角阻力,弹性密度和可允许的旋转等等 18 NSInteger angle = arc4random_uniform(20) - 10; 19 20 itemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[iv]]; 21 itemBehavior.friction = 0.2; 22 itemBehavior.allowsRotation = YES; 23 [itemBehavior addAngularVelocity:angle forItem:iv]; 24 [animator addBehavior:itemBehavior]; 25 26 //4 27 [self performSelector:@selector(resetDemo) withObject:nil afterDelay:0.4]; 28 }
直接上代码,实现随机掉落一张图片的代码
1 // Set up 2 self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view]; 3 4 self.gravityBeahvior = [[UIGravityBehavior alloc] initWithItems:nil]; 5 6 7 8 9 [self.animator addBehavior:self.gravityBeahvior]; 10 11 12 - (void)tapped:(UITapGestureRecognizer *)gesture { 13 14 NSUInteger num = arc4random() % 40 + 1; 15 NSString *filename = [NSString stringWithFormat:@"m%lu", (unsigned long)num]; 16 UIImage *image = [UIImage imageNamed:filename]; 17 UIImageView *imageView = [[UIImageView alloc] initWithImage:image]; 18 [self.view addSubview:imageView]; 19 20 CGPoint tappedPos = [gesture locationInView:gesture.view]; 21 imageView.center = tappedPos; 22 23 [self.gravityBeahvior addItem:imageView]; 24 25 }
继续上面的代码,当图片快掉落出边界的时候有 碰撞效果,这个就是UICollisionBehavior实现的。
1 // Set up 2 self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view]; 3 4 self.gravityBeahvior = [[UIGravityBehavior alloc] initWithItems:nil]; 5 6 self.collisionBehavior = [[UICollisionBehavior alloc] initWithItems:nil]; 7 self.collisionBehavior.translatesReferenceBoundsIntoBoundary = YES; 8 9 self.itemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:nil]; 10 self.itemBehavior.elasticity = 0.6; 11 self.itemBehavior.friction = 0.5; 12 self.itemBehavior.resistance = 0.5; 13 14 15 [self.animator addBehavior:self.gravityBeahvior]; 16 [self.animator addBehavior:self.collisionBehavior]; 17 [self.animator addBehavior:self.itemBehavior]; 18 19 20 21 - (void)tapped:(UITapGestureRecognizer *)gesture { 22 23 NSUInteger num = arc4random() % 40 + 1; 24 NSString *filename = [NSString stringWithFormat:@"m%lu", (unsigned long)num]; 25 UIImage *image = [UIImage imageNamed:filename]; 26 UIImageView *imageView = [[UIImageView alloc] initWithImage:image]; 27 [self.view addSubview:imageView]; 28 29 CGPoint tappedPos = [gesture locationInView:gesture.view]; 30 imageView.center = tappedPos; 31 32 [self.gravityBeahvior addItem:imageView]; 33 [self.collisionBehavior addItem:imageView]; 34 [self.itemBehavior addItem:imageView]; 35 }
另外,UICollisionBehavior有它的代理,其中列举两个方法,它们表示行为开始和结束的时候的代理。
1 - (void)collisionBehavior:(UICollisionBehavior*)behavior beganContactForItem:(id <UIDynamicItem>)item withBoundaryIdentifier:(id <NSCopying>)identifier atPoint:(CGPoint)p; 2 - (void)collisionBehavior:(UICollisionBehavior*)behavior endedContactForItem:(id <UIDynamicItem>)item withBoundaryIdentifier:(id <NSCopying>)identifier;
UISnapBehavior 将UIView通过动画吸附到某个点上。
1 - (void) handleTap:(UITapGestureRecognizer *)paramTap{ 2 CGPoint tapPoint = [paramTap locationInView:self.view]; 3 4 if (self.snapBehavior != nil){ 5 [self.animator removeBehavior:self.snapBehavior]; 6 } 7 self.snapBehavior = [[UISnapBehavior alloc] initWithItem:self.squareView snapToPoint:tapPoint]; 8 self.snapBehavior.damping = 0.5f; //剧列程度 9 [self.animator addBehavior:self.snapBehavior]; 10 }
文章开头说到UIDynamicAnimator有两种初始化方法,这里介绍它与UICollectionView的完美结合,让UICollectionView产生各种动态特性的行为。
你是否记得iOS系统中信息应用中的附有弹性的消息列表,他就是加入了UIAttachmentBehavior吸附行为,这里通过一个UICollectionView实现类似效果。
主要是复写UICollectionViewFlowLayout,在layout中为每一个布局属性元素加上吸附行为就可以了。
关于复写layout,可以参考onevcat的博客
http://www.onevcat.com/2012/08/advanced-collection-view/
下面就直接上代码了
首先遍历每个 collection view layout attribute 来创建和添加新的 dynamic animator
1 -(void)prepareLayout { 2 [super prepareLayout]; 3 4 if (!_animator) { 5 _animator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self]; 6 CGSize contentSize = [self collectionViewContentSize]; 7 NSArray *items = [super layoutAttributesForElementsInRect:CGRectMake(0, 0, contentSize.width, contentSize.height)]; 8 9 for (UICollectionViewLayoutAttributes *item in items) { 10 UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:item.center]; 11 12 attachment.length = 0; 13 attachment.damping = self.damping; 14 attachment.frequency = self.frequency; 15 16 [_animator addBehavior:attachment]; 17 } 18 } 19 }
接下来我们现在需要实现 layoutAttributesForElementsInRect: 和 layoutAttributesForItemAtIndexPath: 这两个方法,UIKit 会调用它们来询问 collection view 每一个 item 的布局信息。我们写的代码会把这些查询交给专门做这些事的 dynamic animator
1 -(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { 2 return [_animator itemsInRect:rect]; 3 } 4 5 - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { 6 return [_animator layoutAttributesForCellAtIndexPath:indexPath]; 7 }
然后是响应滚动事件的方法
这个方法会在 collection view 的 bound 发生改变的时候被调用,根据最新的 content offset 调整我们的 dynamic animator 中的 behaviors 的参数。在重新调整这些 behavior 的 item 之后,我们在这个方法中返回 NO;因为 dynamic animator 会关心 layout 的无效问题,所以在这种情况下,它不需要去主动使其无效
1 -(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { 2 UIScrollView *scrollView = self.collectionView; 3 CGFloat scrollDelta = newBounds.origin.y - scrollView.bounds.origin.y; 4 NSLog(@" %f %f",newBounds.origin.y,scrollView.bounds.origin.y); 5 CGPoint touchLocation = [scrollView.panGestureRecognizer locationInView:scrollView]; 6 7 for (UIAttachmentBehavior *behavior in _animator.behaviors) { 8 9 CGPoint anchorPoint = behavior.anchorPoint; 10 CGFloat distanceFromTouch = fabsf(touchLocation.y - anchorPoint.y); 11 CGFloat scrollResistance = distanceFromTouch / self.resistanceFactor; 12 13 UICollectionViewLayoutAttributes *item = [behavior.items firstObject]; 14 CGPoint center = item.center; 15 center.y += (scrollDelta > 0) ? MIN(scrollDelta, scrollDelta * scrollResistance) 16 : MAX(scrollDelta, scrollDelta * scrollResistance); 17 item.center = center; 18 19 [_animator updateItemUsingCurrentState:item]; 20 } 21 return NO; 22 }
让我们仔细查看这个代码的细节。首先我们得到了这个 scroll view(就是我们的 collection view ),然后计算它的 content offset 中 y 的变化(在这个例子中,我们的 collection view 是垂直滑动的)。一旦我们得到这个增量,我们需要得到用户接触的位置。这是非常重要的,因为我们希望离接触位置比较近的那些物体能移动地更迅速些,而离接触位置比较远的那些物体则应该滞后些。
对于 dynamic animator 中的每个 behavior,我们将接触点到该 behavior 物体的 y 的距离除以 500。分母越小,这个 collection view 的的交互就越有弹簧的感觉。