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];
相關文章
相關標籤/搜索