UICollectionView与Dynamic Animator 无脑翻译

二者关系

Dynamic AnimatorUICollectionView动画效果实现的主要方式。其主要是经过UICollectionViewFlowLayout强引用UIDynamicAnimator,根据items的行为属性变化来对试图进行更新。
实现原理是UICollectionViewFlowLayoutUICollectionViewLayoutAttributes进行添加behaviors。UIDynamicAnimator根据自身变化来对视图进行刷新。spring

举个栗子

1)继承一个UICollectionViewFlowLayout类并实现代理方法性能优化

@implementation ASHCollectionViewController

static NSString * CellIdentifier = @"CellIdentifier";

-(void)viewDidLoad 
{
    [super viewDidLoad];
    [self.collectionView registerClass:[UICollectionViewCell class] 
            forCellWithReuseIdentifier:CellIdentifier];
}

-(UIStatusBarStyle)preferredStatusBarStyle 
{
    return UIStatusBarStyleLightContent;
}

-(void)viewDidAppear:(BOOL)animated 
{
    [super viewDidAppear:animated];
    [self.collectionViewLayout invalidateLayout];
}

#pragma mark - UICollectionView Methods

-(NSInteger)collectionView:(UICollectionView *)collectionView 
    numberOfItemsInSection:(NSInteger)section 
{
    return 120;
}

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView 
                 cellForItemAtIndexPath:(NSIndexPath *)indexPath 
{
    UICollectionViewCell *cell = [collectionView 
        dequeueReusableCellWithReuseIdentifier:CellIdentifier 
                                  forIndexPath:indexPath];

    cell.backgroundColor = [UIColor orangeColor];
    return cell;
}

@end

这里有个槽点是
[self.collectionViewLayout invalidateLayout]; 如果使用SB的话要的视图出现时候invalidate一下。函数

2)建立带UIDynamicAnimatorUICollectionViewFlowLayout子类并初始化性能

@interface ASHSpringyCollectionViewFlowLayout ()

@property (nonatomic, strong) UIDynamicAnimator *dynamicAnimator;

@end

- (id)init 
{
    if (!(self = [super init])) return nil;

    self.minimumInteritemSpacing = 10;
    self.minimumLineSpacing = 10;
    self.itemSize = CGSizeMake(44, 44);
    self.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);

    self.dynamicAnimator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self];

    return self;
}

经过父类的prepareLayout的方法能够获取指定区域范围的属性。优化

[super prepareLayout];

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

确认添加animator类的条件是否准备就绪,这里要注意animator不能被重复添加,不然运行时会报错。肯定之后就是一轮迭代对每个item添加behavior类动画

if (self.dynamicAnimator.behaviors.count == 0) {
    [items enumerateObjectsUsingBlock:^(id<UIDynamicItem> obj, NSUInteger idx, BOOL *stop) {
        UIAttachmentBehavior *behaviour = [[UIAttachmentBehavior alloc] initWithItem:obj 
                                                                    attachedToAnchor:[obj center]];

        behaviour.length = 0.0f;
        behaviour.damping = 0.8f;
        behaviour.frequency = 1.0f;

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

经过layoutAttributesForElementsInRect:layoutAttributesForItemAtIndexPath:两个方法来时时获取animator的状态:atom

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

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

3)滑动事件响应
作到动态调整,咱们须要使layout与dynamic animator根据滑动的视图位置来作出反应。对应的方法是shouldInvalidateLayoutForBoundsChange:。这个方法提供了更新已发生变动behaviors的item的时机。更新完,方法返回NO(无需再更新)。代理

-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds 
{
    UIScrollView *scrollView = self.collectionView;
    CGFloat delta = newBounds.origin.y - scrollView.bounds.origin.y;

    CGPoint touchLocation = [self.collectionView.panGestureRecognizer locationInView:self.collectionView];

    [self.dynamicAnimator.behaviors enumerateObjectsUsingBlock:^(UIAttachmentBehavior *springBehaviour, NSUInteger idx, BOOL *stop) {
        CGFloat yDistanceFromTouch = fabsf(touchLocation.y - springBehaviour.anchorPoint.y);
        CGFloat xDistanceFromTouch = fabsf(touchLocation.x - springBehaviour.anchorPoint.x);
        CGFloat scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / 1500.0f;

        UICollectionViewLayoutAttributes *item = springBehaviour.items.firstObject;
        CGPoint center = item.center;
        if (delta < 0) {
            center.y += MAX(delta, delta*scrollResistance);
        }
        else {
            center.y += MIN(delta, delta*scrollResistance);
        }
        item.center = center;

        [self.dynamicAnimator updateItemUsingCurrentState:item];
    }];

    return NO;
}

在滑动事件中,首先我要计算出滑动方向垂直方向的份量变化值:deltaY(这里以垂直滑动做为栗子)。实际上是获取用户手指在屏幕的位置信息。若是咱们想时时根据用户操做作出响应上述两个值相当重要。code

4)新增行
新增行带来的问题是咱们须要动态的为新增行得animator添加behaviors。
so咱们须要一个刷新Layout的接口:继承

@interface ASHSpringyCollectionViewFlowLayout : UICollectionViewFlowLayout
- (void)resetLayout;
@end

- (void)resetLayout {
    [self.dynamicAnimator removeAllBehaviors];
    [self prepareLayout];
}

以后咱们只要在每次从新加载数据源刷新视图以后调用一次该接口就能够了!

[self.collectionView reloadData];
[(ASHSpringyCollectionViewFlowLayout *)[self collectionViewLayout] resetLayout];

上述实现为原生,并没有考虑性能优化。想一步作一步而已。

4)使Dynamic Behaviors Tiling化从而提高性能
上述的代码在小数据量(数百cell)仍是能够应付的,可是当运行时数据量过大的时候可能就要挂了。

OK,那么要解决这个问题切入时间点在于在item出现或者即将出现得时候。这是咱们须要处理的地方。而咱们须要作的处理是保存全部展现并正在动画的items的indexpath。即添加一个属性来作保存。

@property (nonatomic, strong) NSMutableSet *visibleIndexPathsSet;

注:用set的缘由是其查找跟判断的时间消耗为O(N),这里须要大量的查找跟判断。

再重写咱们的prepareLayout方法以前咱们要明确啥是tiling化。简而言之就是在cell出屏幕边界的时候移除behaviors在进入屏幕内的时候添加behaviors。难点在于在咱们新建一个behaviors时候要够轻量级。这意味着咱们须要在用dynamic animator建立以及shouldInvalidateLayoutForBoundsChange: 方法配置以后再次更改一次。
此外为了保证轻量级behaviors咱们还须要保存当前边界滑动的delta值:

@property (nonatomic, assign) CGFloat latestDelta;

同时,咱们还须要在shouldInvalidateLayoutForBoundsChange:添加代码

self.latestDelta = delta;

而用来查询当前排版的两函数layoutAttributesForElementsInRect:layoutAttributesForItemAtIndexPath:无需变更。

如今最复杂的莫过于tiling机制。咱们须要重写prepareLayout

首先咱们要移除屏幕以外items的behaviors,接着咱们须要往屏幕新出现的items添加behaviors。先来看第一步。

首先仍是须要调用 [super prepareLayout] 来获取排版信息,不一样的是再也不加载整个View的排版信息而是屏幕可见区域items的排版信息。

请注意因为可能会快速的滑动,so咱们要稍微的扩大可见区域的范围。否则将形成动画不连贯(闪烁)。

CGRect originalRect = (CGRect){.origin = self.collectionView.bounds.origin, .size = self.collectionView.frame.size};
CGRect visibleRect = CGRectInset(originalRect, -100, -100);

这里对上对下扩展的100要根据cell大小来定制哦。cell太大就操蛋了。。。

而后就是计算可见区域内得index paths了:

NSArray *itemsInVisibleRectArray = [super layoutAttributesForElementsInRect:visibleRect];

NSSet *itemsIndexPathsInVisibleRectSet = [NSSet setWithArray:[itemsInVisibleRectArray valueForKey:@"indexPath"]];

找到index paths集合后紧接着咱们要从这个集合中干掉那些已移除屏幕items中animatorbehaviours(从visibleIndexPathsSet)。

NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(UIAttachmentBehavior *behaviour, NSDictionary *bindings) {
    BOOL currentlyVisible = [itemsIndexPathsInVisibleRectSet member:[[[behaviour items] firstObject] indexPath]] != nil;
    return !currentlyVisible;
}]
NSArray *noLongerVisibleBehaviours = [self.dynamicAnimator.behaviors filteredArrayUsingPredicate:predicate];

[noLongerVisibleBehaviours enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) {
    [self.dynamicAnimator removeBehavior:obj];
    [self.visibleIndexPathsSet removeObject:[[[obj items] firstObject] indexPath]];
}];

第二步是计算出即将可见的index paths集合
(从itemsIndexPathsInVisibleRectSet

NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(UICollectionViewLayoutAttributes *item, NSDictionary *bindings) {
    BOOL currentlyVisible = [self.visibleIndexPathsSet member:item.indexPath] != nil;
    return !currentlyVisible;
}];
NSArray *newlyVisibleItems = [itemsInVisibleRectArray filteredArrayUsingPredicate:predicate];
相关文章
相关标签/搜索