两者关系
Dynamic Animator
是UICollectionView
动画效果实现的主要方式。其主要是通过UICollectionViewFlowLayout
强引用UIDynamicAnimator
,根据items的行为属性变化来对试图进行更新。
实现原理是UICollectionViewFlowLayout
对UICollectionViewLayoutAttributes
进行添加behaviors。UIDynamicAnimator
根据自身变化来对视图进行刷新。
举个栗子
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)创建带UIDynamicAnimator
的UICollectionViewFlowLayout
子类并初始化
@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 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的状态:
-(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
(这里以垂直滑动作为栗子)。其实是获取用户手指在屏幕的位置信息
。如果我们想时时根据用户操作做出响应上述两个值至关重要。
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中animator
的behaviours
(从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];