iOS開發概述UIkit動力學
2014-10-18
UIkit動力學是UIkit框架中模擬真實世界的一些特性。
UIDynamicAnimator
主要有UIDynamicAnimator類,通過這個類中的不同行為來實現一些動態特性。
它一般有兩種初始化方法,先講常見的第一種
animator= [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
動態特性的實現主要依靠它所新增的行為,通過以下方法進行新增和移除,
[animator addBehavior:attachmentBehavior];
[animator removeAllBehaviors];
接下來介紹五個不同的行為,UIAttachmentBehavior(吸附),UICollisionBehavior(碰撞),UIGravityBehavior(重力),UIPushBehavior(推動),UISnapBehavior(捕捉)。另外還有一個輔助的行為UIDynamicItemBehavior,用來在item層級設定一些引數,比如item的摩擦,阻力,角阻力,彈性密度和可允許的旋轉等等。
UIAttachmentBehavior(吸附) 先講吸附行為,
它的初始化方法
attachmentBehavior = [[UIAttachmentBehavior alloc] initWithItem:iv
offsetFromCenter:centerOffset
attachedToAnchor:location];
item是實現UIDynamicItem協議的id型別,這裡設定吸附一個UIImageView的例項iv。offset可以設定吸附的偏移,anchor是設定錨點。
UIAttachmentBehavior有幾個屬性,例如damping,frequency。damping是阻尼數值,frequency是震動頻率
直接上程式碼,實現一個pan手勢,讓一個image跟著手勢跑
-(void)gesture:(UIPanGestureRecognizer *)gesture{
CGPoint location = [gesture locationInView:self.view];
CGPoint boxLocation = [gesture locationInView:iv];
switch (gesture.state) {
case UIGestureRecognizerStateBegan:{
NSLog(@"you touch started position %@",NSStringFromCGPoint(location));
NSLog(@"location in image started is %@",NSStringFromCGPoint(boxLocation));
[animator removeAllBehaviors];
// Create an attachment binding the anchor point (the finger's current location)
// to a certain position on the view (the offset)
UIOffset centerOffset = UIOffsetMake(boxLocation.x - CGRectGetMidX(iv.bounds),
boxLocation.y - CGRectGetMidY(iv.bounds));
attachmentBehavior = [[UIAttachmentBehavior alloc] initWithItem:iv
offsetFromCenter:centerOffset
attachedToAnchor:location];
attachmentBehavior.damping=0.5;
attachmentBehavior.frequency=0.8;
// Tell the animator to use this attachment behavior
[animator addBehavior:attachmentBehavior];
break;
}
case UIGestureRecognizerStateEnded: {
[animator removeBehavior:attachmentBehavior];
break;
}
default:
[attachmentBehavior setAnchorPoint:[gesture locationInView:self.view]];
break;
}
}
UIPushBehavior(推動) UIPushBehavior 可以為一個UIView施加一個力的作用,這個力可以是持續的,也可以只是一個衝量。我們可以指定力的大小,方向和作用點等等資訊。
pushBehavior = [[UIPushBehavior alloc]
initWithItems:@[iv]
mode:UIPushBehaviorModeInstantaneous];
UIPushBehavior 有pushDirection、magnitude等屬性,
//1
CGPoint velocity = [gesture velocityInView:self.view];
CGFloat magnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y));
if (magnitude > ThrowingThreshold) {
//2
pushBehavior = [[UIPushBehavior alloc]
initWithItems:@[iv]
mode:UIPushBehaviorModeInstantaneous];
pushBehavior.pushDirection = CGVectorMake((velocity.x / 10) , (velocity.y / 10));
pushBehavior.magnitude = magnitude / ThrowingvelocityPadding;
[animator addBehavior:pushBehavior];
//3
// UIDynamicItemBehavior 其實是一個輔助的行為,用來在item層級設定一些引數,比如item的摩擦,阻力,角阻力,彈性密度和可允許的旋轉等等
NSInteger angle = arc4random_uniform(20) - 10;
itemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[iv]];
itemBehavior.friction = 0.2;
itemBehavior.allowsRotation = YES;
[itemBehavior addAngularVelocity:angle forItem:iv];
[animator addBehavior:itemBehavior];
//4
[self performSelector:@selector(resetDemo) withObject:nil afterDelay:0.4];
}
UIGravityBehavior(重力) 直接上程式碼,實現隨機掉落一張圖片的程式碼
// Set up
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
self.gravityBeahvior = [[UIGravityBehavior alloc] initWithItems:nil];
[self.animator addBehavior:self.gravityBeahvior];
- (void)tapped:(UITapGestureRecognizer *)gesture {
NSUInteger num = arc4random() % 40 + 1;
NSString *filename = [NSString stringWithFormat:@"m%lu", (unsigned long)num];
UIImage *image = [UIImage imageNamed:filename];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
[self.view addSubview:imageView];
CGPoint tappedPos = [gesture locationInView:gesture.view];
imageView.center = tappedPos;
[self.gravityBeahvior addItem:imageView];
}
UICollisionBehavior(碰撞) 繼續上面的程式碼,當圖片快掉落出邊界的時候有 碰撞效果,這個就是UICollisionBehavior實現的。
// Set up
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
self.gravityBeahvior = [[UIGravityBehavior alloc] initWithItems:nil];
self.collisionBehavior = [[UICollisionBehavior alloc] initWithItems:nil];
self.collisionBehavior.translatesReferenceBoundsIntoBoundary = YES;
self.itemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:nil];
self.itemBehavior.elasticity = 0.6;
self.itemBehavior.friction = 0.5;
self.itemBehavior.resistance = 0.5;
[self.animator addBehavior:self.gravityBeahvior];
[self.animator addBehavior:self.collisionBehavior];
[self.animator addBehavior:self.itemBehavior];
- (void)tapped:(UITapGestureRecognizer *)gesture {
NSUInteger num = arc4random() % 40 + 1;
NSString *filename = [NSString stringWithFormat:@"m%lu", (unsigned long)num];
UIImage *image = [UIImage imageNamed:filename];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
[self.view addSubview:imageView];
CGPoint tappedPos = [gesture locationInView:gesture.view];
imageView.center = tappedPos;
[self.gravityBeahvior addItem:imageView];
[self.collisionBehavior addItem:imageView];
[self.itemBehavior addItem:imageView];
}
另外,UICollisionBehavior有它的代理,其中列舉兩個方法,它們表示行為開始和結束的時候的代理。
- (void)collisionBehavior:(UICollisionBehavior*)behavior beganContactForItem:(id <UIDynamicItem>)item withBoundaryIdentifier:(id <NSCopying>)identifier atPoint:(CGPoint)p;
- (void)collisionBehavior:(UICollisionBehavior*)behavior endedContactForItem:(id <UIDynamicItem>)item withBoundaryIdentifier:(id <NSCopying>)identifier;
UISnapBehavior(捕捉) UISnapBehavior 將UIView通過動畫吸附到某個點上。
- (void) handleTap:(UITapGestureRecognizer *)paramTap{
CGPoint tapPoint = [paramTap locationInView:self.view];
if (self.snapBehavior != nil){
[self.animator removeBehavior:self.snapBehavior];
}
self.snapBehavior = [[UISnapBehavior alloc] initWithItem:self.squareView snapToPoint:tapPoint];
self.snapBehavior.damping = 0.5f; //劇列程度
[self.animator addBehavior:self.snapBehavior];
}
UICollectionView與UIDynamicAnimator 文章開頭說到UIDynamicAnimator有兩種初始化方法,這裡介紹它與UICollectionView的完美結合,讓UICollectionView產生各種動態特性的行為。
你是否記得iOS系統中資訊應用中的附有彈性的訊息列表,他就是加入了UIAttachmentBehavior吸附行為,這裡通過一個UICollectionView實現類似效果。
主要是複寫UICollectionViewFlowLayout,在layout中為每一個佈局屬性元素加上吸附行為就可以了。
關於複寫layout,可以參考onevcat的部落格
下面就直接上程式碼了
首先遍歷每個 collection view layout attribute 來建立和新增新的 dynamic animator
-(void)prepareLayout {
[super prepareLayout];
if (!_animator) {
_animator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self];
CGSize contentSize = [self collectionViewContentSize];
NSArray *items = [super layoutAttributesForElementsInRect:CGRectMake(0, 0, contentSize.width, contentSize.height)];
for (UICollectionViewLayoutAttributes *item in items) {
UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:item.center];
attachment.length = 0;
attachment.damping = self.damping;
attachment.frequency = self.frequency;
[_animator addBehavior:attachment];
}
}
}
接下來我們現在需要實現 layoutAttributesForElementsInRect: 和 layoutAttributesForItemAtIndexPath: 這兩個方法,UIKit 會呼叫它們來詢問 collection view 每一個 item 的佈局資訊。我們寫的程式碼會把這些查詢交給專門做這些事的 dynamic animator
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
return [_animator itemsInRect:rect];
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
return [_animator layoutAttributesForCellAtIndexPath:indexPath];
}
然後是響應滾動事件的方法
這個方法會在 collection view 的 bound 發生改變的時候被呼叫,根據最新的 content offset 調整我們的 dynamic animator 中的 behaviors 的引數。在重新調整這些 behavior 的 item 之後,我們在這個方法中返回 NO;因為 dynamic animator 會關心 layout 的無效問題,所以在這種情況下,它不需要去主動使其無效
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
UIScrollView *scrollView = self.collectionView;
CGFloat scrollDelta = newBounds.origin.y - scrollView.bounds.origin.y;
NSLog(@" %f %f",newBounds.origin.y,scrollView.bounds.origin.y);
CGPoint touchLocation = [scrollView.panGestureRecognizer locationInView:scrollView];
for (UIAttachmentBehavior *behavior in _animator.behaviors) {
CGPoint anchorPoint = behavior.anchorPoint;
CGFloat distanceFromTouch = fabsf(touchLocation.y - anchorPoint.y);
CGFloat scrollResistance = distanceFromTouch / self.resistanceFactor;
UICollectionViewLayoutAttributes *item = [behavior.items firstObject];
CGPoint center = item.center;
center.y += (scrollDelta > 0) ? MIN(scrollDelta, scrollDelta * scrollResistance)
: MAX(scrollDelta, scrollDelta * scrollResistance);
item.center = center;
[_animator updateItemUsingCurrentState:item];
}
return NO;
}
讓我們仔細檢視這個程式碼的細節。首先我們得到了這個 scroll view(就是我們的 collection view ),然後計算它的 content offset 中 y 的變化(在這個例子中,我們的 collection view 是垂直滑動的)。一旦我們得到這個增量,我們需要得到使用者接觸的位置。這是非常重要的,因為我們希望離接觸位置比較近的那些物體能移動地更迅速些,而離接觸位置比較遠的那些物體則應該滯後些。
對於 dynamic animator 中的每個 behavior,我們將接觸點到該 behavior 物體的 y 的距離除以 500。分母越小,這個 collection view 的的互動就越有彈簧的感覺。