iOS 7のMessage appのようなUIを作ってみた
UICollectionView + UIKitDynamics
実装方法
UICollectionViewFlowLayoutとUIKitDynamicsを連携させます。
- UICollectionviewFlowLayoutのサブクラスを作成し、CollectionViewに設定する。
- 作成したサブクラスは、UIDyanicsAnimatorのインスタンスを持ち、LayoutAttributesに対して、DynamicBehaviorを作成し、UIDynamicsAnimatorが管理する。
- CollectionViewには、UIDynamicAnimatorの管理するDynamicBehaviorのLayoutAttributesオブジェクトを返すようにする。
手順
UICollectionviewFlowLayoutのサブクラスを作成する
@interface DynamicsFlowLayout : UICollectionViewFlowLayout
@property (nonatomic) UIDynamicAnimator* animator;
@property (nonatomic) NSMutableSet* visibleIndexPathsSet;
@property (nonatomic) CGFloat latestDelta;
@end
- 作成したLayoutクラスにダイナミックアニメーターのインスタンスを保持する
- (id)init
{
self = [super init];
if (nil == self) {
return nil;
}
// Create dynamic animator
self.animator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self];
self.visibleIndexPathsSet = [NSMutableSet set];
// Configure self
self.itemSize = CGSizeMake(310, 60);
self.minimumInteritemSpacing = 1;
self.minimumLineSpacing = 5;
return self;
}
prepareLayoutメソッドをオーバーライドしUIAttachmentBehaviorを作成する
- 画面に表示されなくなったbehaiviorを削除する
- (void)prepareLayout
{
// Invoke super
[super prepareLayout];
// Need to overflow our actual visible rect slightly to avoid flickering.
// Get visible rect
CGRect originalRect = (CGRect){.origin = self.collectionView.bounds.origin, .size = self.collectionView.frame.size};
CGRect visibleRect = CGRectInset(originalRect, -100, -100);
// Get attributes in visible rect
NSArray* itemsInVisibleRectArray;
itemsInVisibleRectArray = [super layoutAttributesForElementsInRect:visibleRect];
// Get their index paths
NSSet *itemsIndexPathsInVisibleRectSet;
itemsIndexPathsInVisibleRectSet = [NSSet setWithArray:[itemsInVisibleRectArray valueForKey:@"indexPath"]];
//
// Step 1: Remove any behaviours that are no longer visible.
//
NSPredicate* predicate;
predicate = [NSPredicate predicateWithBlock:^BOOL(UIAttachmentBehavior* behaviour, NSDictionary* bindings){
BOOL currentlyVisible = ([itemsIndexPathsInVisibleRectSet member:[[[behaviour items] firstObject] indexPath]] != nil);
return !currentlyVisible;
}];
NSArray* noLongerVisibleBehaviours;
noLongerVisibleBehaviours = [self.animator.behaviors filteredArrayUsingPredicate:predicate];
[noLongerVisibleBehaviours enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop){
[self.animator removeBehavior:obj];
[self.visibleIndexPathsSet removeObject:[[[obj items] firstObject] indexPath]];
}];
…中略…
}
- 新しく画面に表示されるattributeから、behaviourを作成する
//
// Step 2: Add any newly visible behaviours.
// A "newly visible" item is one that is in the itemsInVisibleRect(Set|Array) but not in the visibleIndexPathsSet
predicate = [NSPredicate predicateWithBlock:^BOOL(UICollectionViewLayoutAttributes* item, NSDictionary* bindings){
BOOL currentlyVisible = [self.visibleIndexPathsSet member:item.indexPath] != nil;
return !currentlyVisible;
}];
NSArray* newlyVisibleItems;
newlyVisibleItems = [itemsInVisibleRectArray filteredArrayUsingPredicate:predicate];
// Get touch location
CGPoint touchLocation;
touchLocation = [self.collectionView.panGestureRecognizer locationInView:self.collectionView];
[newlyVisibleItems enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes* item, NSUInteger idx, BOOL *stop){
// Get center
CGPoint center = item.center;
// Create attachment behavior
UIAttachmentBehavior *springBehaviour = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:center];
// Configure behaviour
springBehaviour.length = 0.0f;
springBehaviour.damping = 0.8f;
springBehaviour.frequency = 1.0f;
if (!CGPointEqualToPoint(CGPointZero, touchLocation)) {
CGFloat yDistanceFromTouch;
CGFloat xDistanceFromTouch;
CGFloat scrollResistance;
yDistanceFromTouch = fabsf(touchLocation.y - springBehaviour.anchorPoint.y);
xDistanceFromTouch = fabsf(touchLocation.x - springBehaviour.anchorPoint.x);
scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / 1500.0f;
if (self.latestDelta < 0) {
center.y += MAX(self.latestDelta, self.latestDelta * scrollResistance);
}
else {
center.y += MIN(self.latestDelta, self.latestDelta * scrollResistance);
}
item.center = center;
}
[self.animator addBehavior:springBehaviour];
[self.visibleIndexPathsSet addObject:item.indexPath];
}];
- layoutAttributesForElementsInRect:メソッドをオーバーライドし、dynamic animatorのitemsInRect:メソッドを呼び、dynamic animatorに追加されているattachment behaviorのlayout attributesを返す
- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
return [self.animator itemsInRect:rect];
}
- layoutAttributesForItemAtIndexPath:メソッドをオーバーライドし、dynamic animatorのlayoutAttributesForCellAtIndexPath:メソッドを呼び、indexPathで指定されたlayout attributesを返す
- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [self.animator layoutAttributesForCellAtIndexPath:indexPath];
}
- shouldInvalidateLayoutForBoundsChange:メソッドをオーバーライドし、layout attributeの位置を更新する
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
// Calc content offset's y
UIScrollView* scrollView;
CGFloat delta;
scrollView = self.collectionView;
delta = newBounds.origin.y - scrollView.bounds.origin.y;
self.latestDelta = delta;
// Get touch location
CGPoint touchLocation;
touchLocation = [self.collectionView.panGestureRecognizer locationInView:self.collectionView];
[self.animator.behaviors enumerateObjectsUsingBlock:^(UIAttachmentBehavior *springBehaviour, NSUInteger idx, BOOL * stop){
// Calc distancez
CGFloat yDistanceFromTouch;
CGFloat xDistanceFromTouch;
CGFloat scrollResistance;
yDistanceFromTouch = fabsf(touchLocation.y - springBehaviour.anchorPoint.y);
xDistanceFromTouch = fabsf(touchLocation.x - springBehaviour.anchorPoint.x);
scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / 1500.0f;
// Get layout attribute
UICollectionViewLayoutAttributes* item;
item = springBehaviour.items.firstObject;
// Decide layout attribute center
CGPoint center = item.center;
if (delta < 0) {
center.y += MAX(delta, delta * scrollResistance);
}
else {
center.y += MIN(delta, delta * scrollResistance);
}
item.center = center;
// Update dynamic item
[self.animator updateItemUsingCurrentState:item];
}];
return NO;
}
以上でiOS 7のMessage appのようなUIが実現できます。
パフォーマンスについて
prepareLayout:で画面の外に消えたattributesのdynamic behaviorをanimatorからremoveし、 dynamic behaviorを追加する際は、新しく画面に表示される分だけ追加するようにすることで パフォーマンスを考慮した実装となっている。参考情報がわかりやすいのです。
所感
UIDynamicsAnimatorがUICollectionViewLayoutとシームレスに連携できるようになっていてすごい。
参考情報
obj io
UICollectionView-Spring-Demo