Использование UIDynamicAnimator из UIKit Dynamics при создании таблиц на основе UICollectionView

Как известно в последней версии SDK (фреймворк UIKit Dynamics) разработчики могут определять динамическое поведение для UIView объектов, а также других объектов, принимающих протокол UIDynamicItem.

В данной статье я хочу поделиться опытом использования таких объектов.

Целью было сделать анимацию, аналогичную той, что используется в приложении Messages на iOS 7:

6a69f6262a634d391a0f3b6f72f2e032

После поисков и изучения документации решено было использовать UIDynamicAnimator в связке с UICollectionView.

Для этого необходимо было создать класс-наследник UICollectionViewFlowLayout:

#import <UIKit/UIKit.h>

@interface DVCollectionViewFlowLayout : UICollectionViewFlowLayout

@end

добавить свойство с типом UIDynamicAnimator


#import "DVCollectionViewFlowLayout.h"

@interface DVCollectionViewFlowLayout()

//объект-аниматор
@property (nonatomic, strong) UIDynamicAnimator *dynamicAnimator;

@end

@implementation DVCollectionViewFlowLayout

@synthesize dynamicAnimator = _dynamicAnimator;

-(id)initr{
    self = [super init];
    if (self){
        _dynamicAnimator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self];
    }
    return self;
}

Динамическое поведение становится активным, когда мы его добавляем к объекту, который является экземпляром UIDynamicAnimator. Аниматор определяет контекст, в котором динамическое поведение выполняется.

После этого мы должны переопределить в нём нижеперечисленные функции:


#import "DVCollectionViewFlowLayout.h"

 // .........

- (void)prepareLayout{

    [super prepareLayout];

    CGSize contentSize = [self collectionViewContentSize];
    NSArray *items = [super layoutAttributesForElementsInRect:CGRectMake(0, 0, contentSize.width, contentSize.height)];

    if (items.count != self.dynamicAnimator.behaviors.count) {
        [self.dynamicAnimator removeAllBehaviors];

        for (UICollectionViewLayoutAttributes *item in items) {
            UIAttachmentBehavior *springBehavior = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:item.center];
//экспериментальным путём подобранные значения для лучшего восприятия (на глаз)
            springBehavior.length = 0.f;
            springBehavior.damping = 1.f;
            springBehavior.frequency = 6.8f;

            [self.dynamicAnimator addBehavior:springBehavior];
        }
    }
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
    return [self.dynamicAnimator itemsInRect:rect];
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
    return [self.dynamicAnimator layoutAttributesForCellAtIndexPath:indexPath];
}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
    CGFloat scrollDelta = newBounds.origin.y - self.collectionView.bounds.origin.y;
    CGPoint touchLocation = [self.collectionView.panGestureRecognizer locationInView:self.collectionView];

    for (UIAttachmentBehavior *springBehavior in self.dynamicAnimator.behaviors) {
        CGPoint anchorPoint = springBehavior.anchorPoint;
        CGFloat touchDistance = fabsf(touchLocation.y - anchorPoint.y);
       //экспериментальным путём подобранные значения для лучшего восприятия (на глаз)
 CGFloat resistanceFactor = 0.002;

        UICollectionViewLayoutAttributes *attributes = springBehavior.items.firstObject;

        CGPoint center = attributes.center;

        float resistedScroll = scrollDelta * touchDistance * resistanceFactor;
        float simpleScroll = scrollDelta;

        float actualScroll = MIN(abs(simpleScroll), abs(resistedScroll));
        if(simpleScroll < 0){
            actualScroll *= -1;
        }

        center.y += actualScroll;
        attributes.center = center;

        [self.dynamicAnimator updateItemUsingCurrentState:attributes];
    }

    return NO;
}

-(void)dealloc{
        [self.dynamicAnimator removeAllBehaviors];
        self.dynamicAnimator = nil;
}

Объект UIAttachmentBehavior определяет связь между динамическим элементом item класса UICollectionViewLayoutAttributes и точкой item.center (центром этого элемента). Когда один элемент или точка перемещается, присоединенный элемент также перемещается. C помощью свойств damping и frequency мы можем указать как изменится поведение с течением времени.

Исходный код

Полезные ссылки про анимации в iOS 7: