Appleのドキュメント(およびWWDC 2012で宣伝)によると、UICollectionView
のレイアウトを動的に設定し、変更をアニメーション化することも可能です。
通常、コレクションビューの作成時にレイアウトオブジェクトを指定しますが、コレクションビューのレイアウトを動的に変更することもできます。レイアウトオブジェクトは、collectionViewLayoutプロパティに格納されます。このプロパティを設定すると、変更をアニメーション化せずに、レイアウトが直接更新されます。変更をアニメーション化する場合は、代わりにsetCollectionViewLayout:animated:メソッドを呼び出す必要があります。
ただし、実際には、UICollectionView
がcontentOffset
に不可解で無効な変更を加え、セルを不正確に移動させ、機能を事実上使用できなくすることがわかりました。問題を説明するために、ストーリーボードにドロップされたデフォルトのコレクションビューコントローラーに添付できる次のサンプルコードをまとめました。
#import <UIKit/UIKit.h>
@interface MyCollectionViewController : UICollectionViewController
@end
@implementation MyCollectionViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"CELL"];
self.collectionView.collectionViewLayout = [[UICollectionViewFlowLayout alloc] init];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 1;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:@"CELL" forIndexPath:indexPath];
cell.backgroundColor = [UIColor whiteColor];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(@"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y);
[self.collectionView setCollectionViewLayout:[[UICollectionViewFlowLayout alloc] init] animated:YES];
NSLog(@"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y);
}
@end
コントローラーはUICollectionViewFlowLayout
にデフォルトのviewDidLoad
を設定し、画面に単一のセルを表示します。セルが選択されると、コントローラーは別のデフォルトUICollectionViewFlowLayout
を作成し、animated:YES
フラグを使用してコレクションビューに設定します。予想される動作は、セルが移動しないことです。ただし、実際の動作は、セルが画面外にスクロールすることです。この時点では、セルを画面上にスクロールして戻すことさえできません。
コンソールログを見ると、contentOffsetが不可解に変更されていることがわかります(私のプロジェクトでは、(0、0)から(0、205)に)。 非アニメーションの場合の解決策 (i.e. animated:NO
)の解決策を投稿しましたが、アニメーションが必要なので、誰かに解決策や回避策があるかどうかを知りたいと思いますアニメーションケース。
補足として、カスタムレイアウトをテストし、同じ動作を取得しました。
私はこれまで何日も髪を引っ張っていましたが、私の状況に役立つ解決策を見つけました。私の場合、iPadの写真アプリのような折りたたみ式の写真レイアウトがあります。写真が重ねられたアルバムが表示され、アルバムをタップすると写真が展開されます。だから私は2つの別々のUICollectionViewLayoutsであり、_[self.collectionView setCollectionViewLayout:myLayout animated:YES]
_でそれらの間で切り替えています。アニメーションの前にセルがジャンプするという正確な問題があり、contentOffset
であることに気付きました。 contentOffset
ですべてを試しましたが、アニメーション中にジャンプしました。上記のタイラーのソリューションは機能しましたが、アニメーションをいじっていました。
それから、スクリーン上にいくつかのアルバムがあり、スクリーンをいっぱいにするのに十分ではないときにのみ起こることに気づきました。推奨されるように、私のレイアウトは-(CGSize)collectionViewContentSize
をオーバーライドします。アルバムの数が少ない場合、コレクションビューのコンテンツサイズは、ビューのコンテンツサイズよりも小さくなります。コレクションのレイアウトを切り替えると、ジャンプが発生します。
そのため、レイアウトにminHeightというプロパティを設定し、コレクションビューの親の高さに設定します。次に、-(CGSize)collectionViewContentSize
に戻る前に高さをチェックします。高さが最小高さ以上であることを確認します。
本当の解決策ではありませんが、今はうまく機能しています。コレクションビューのcontentSize
を、少なくとも含まれているビューの長さに設定してみます。
編集: UICollectionViewFlowLayoutから継承する場合、Manicaesarは簡単な回避策を追加しました:
_-(CGSize)collectionViewContentSize { //Workaround
CGSize superSize = [super collectionViewContentSize];
CGRect frame = self.collectionView.frame;
return CGSizeMake(fmaxf(superSize.width, CGRectGetWidth(frame)), fmaxf(superSize.height, CGRectGetHeight(frame)));
}
_
UICollectionViewLayout
にはオーバーライド可能なメソッドが含まれています targetContentOffsetForProposedContentOffset:
これにより、レイアウトの変更中に適切なコンテンツオフセットを提供でき、これにより正しくアニメーション化されます。これはiOS 7.0以降で利用可能です
私自身の質問に遅れて答えて飛び込みます。
TLLayoutTransitioning ライブラリは、iOS7のインタラクティブな移行APIを再タスクして非インタラクティブなレイアウトからレイアウトへの移行を行うことにより、この問題に対する優れたソリューションを提供します。コンテンツオフセットの問題を解決し、いくつかの機能を追加することで、setCollectionViewLayout
の代替として効果的に使用できます。
カスタムイージング曲線は、AHEasingFunction
関数として定義できます。最終コンテンツオフセットは、最小、中央、上、左、下、または右の配置オプションを使用して、1つ以上のインデックスパスに関して指定できます。
意味を確認するには、例のワークスペースで Resize demo を実行し、オプションを試してみてください。
使い方はこんな感じです。まず、TLTransitionLayout
のインスタンスを返すようにView Controllerを構成します。
- (UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout
{
return [[TLTransitionLayout alloc] initWithCurrentLayout:fromLayout nextLayout:toLayout];
}
次に、setCollectionViewLayout
を呼び出す代わりに、transitionToCollectionViewLayout:toLayout
カテゴリで定義されているUICollectionView-TLLayoutTransitioning
を呼び出します。
UICollectionViewLayout *toLayout = ...; // the layout to transition to
CGFloat duration = 2.0;
AHEasingFunction easing = QuarticEaseInOut;
TLTransitionLayout *layout = (TLTransitionLayout *)[collectionView transitionToCollectionViewLayout:toLayout duration:duration easing:easing completion:nil];
この呼び出しは、インタラクティブなトランジションを開始し、内部で、指定された期間とイージング関数でトランジションの進行を駆動するCADisplayLink
コールバックを開始します。
次のステップでは、最終的なコンテンツオフセットを指定します。任意の値を指定できますが、UICollectionView-TLLayoutTransitioning
で定義されているtoContentOffsetForLayout
メソッドは、1つ以上のインデックスパスに対するコンテンツオフセットを計算するエレガントな方法を提供します。たとえば、特定のセルを可能な限りコレクションビューの中心に近づけるには、transitionToCollectionViewLayout
の直後に次の呼び出しを行います。
NSIndexPath *indexPath = ...; // the index path of the cell to center
TLTransitionLayoutIndexPathPlacement placement = TLTransitionLayoutIndexPathPlacementCenter;
CGPoint toOffset = [collectionView toContentOffsetForLayout:layout indexPaths:@[indexPath] placement:placement];
layout.toContentOffset = toOffset;
この問題は私にも噛み付き、移行コードのバグのようです。私が知ることができることから、遷移前のビューレイアウトの中心に最も近いセルに焦点を合わせようとします。ただし、遷移前のビューの中心にセルが存在しない場合、遷移後のセルの中心に配置しようとします。 alwaysBounceVertical/HorizontalをYESに設定し、単一のセルでビューをロードしてからレイアウトの移行を実行すると、これは非常に明確です。
これを回避するには、レイアウトの更新をトリガーした後、特定のセル(この例では最初のセルが表示されるセル)にフォーカスするようにコレクションに明示的に指示しました。
[self.collectionView setCollectionViewLayout:[self generateNextLayout] animated:YES];
// scroll to the first visible cell
if ( 0 < self.collectionView.indexPathsForVisibleItems.count ) {
NSIndexPath *firstVisibleIdx = [[self.collectionView indexPathsForVisibleItems] objectAtIndex:0];
[self.collectionView scrollToItemAtIndexPath:firstVisibleIdx atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES];
}
簡単です。
新しいレイアウトとcollectionViewのcontentOffsetを同じアニメーションブロックでアニメーション化します。
[UIView animateWithDuration:0.3 animations:^{
[self.collectionView setCollectionViewLayout:self.someLayout animated:YES completion:nil];
[self.collectionView setContentOffset:CGPointMake(0, -64)];
} completion:nil];
self.collectionView.contentOffset
定数。
Swift 3。
UICollectionViewLayoutサブクラス。方法cdemiris99:
override public func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
return collectionView?.contentOffset ?? CGPoint.zero
}
私は自分のカスタム layout を使用して自分でテストしました
レイアウトからの移行時に変化しないコンテンツオフセットを単に探している場合は、カスタムレイアウトを作成し、いくつかのメソッドをオーバーライドして、古いcontentOffsetを追跡して再利用できます。
@interface CustomLayout ()
@property (nonatomic) NSValue *previousContentOffset;
@end
@implementation CustomLayout
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
CGPoint previousContentOffset = [self.previousContentOffset CGPointValue];
CGPoint superContentOffset = [super targetContentOffsetForProposedContentOffset:proposedContentOffset];
return self.previousContentOffset != nil ? previousContentOffset : superContentOffset ;
}
- (void)prepareForTransitionFromLayout:(UICollectionViewLayout *)oldLayout
{
self.previousContentOffset = [NSValue valueWithCGPoint:self.collectionView.contentOffset];
return [super prepareForTransitionFromLayout:oldLayout];
}
- (void)finalizeLayoutTransition
{
self.previousContentOffset = nil;
return [super finalizeLayoutTransition];
}
@end
これはすべて、prepareForTransitionFromLayout
のレイアウト遷移の前に以前のコンテンツオフセットを保存し、targetContentOffsetForProposedContentOffset
の新しいコンテンツオフセットを上書きし、finalizeLayoutTransition
でクリアすることです。とても簡単です
エクスペリエンスの追加に役立つ場合:コンテンツのサイズ、コンテンツのインセットを設定したかどうか、またはその他の明らかな要因に関係なく、この問題に永続的に遭遇しました。したがって、私の回避策はやや抜本的でした。まず、UICollectionViewをサブクラス化し、不適切なコンテンツオフセット設定に対処するために追加しました。
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
if(_declineContentOffset) return;
[super setContentOffset:contentOffset];
}
- (void)setContentOffset:(CGPoint)contentOffset
{
if(_declineContentOffset) return;
[super setContentOffset:contentOffset];
}
- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated
{
_declineContentOffset ++;
[super setCollectionViewLayout:layout animated:animated];
_declineContentOffset --;
}
- (void)setContentSize:(CGSize)contentSize
{
_declineContentOffset ++;
[super setContentSize:contentSize];
_declineContentOffset --;
}
私はそれを誇りに思っていませんが、唯一の実行可能な解決策は、setCollectionViewLayout:animated:
の呼び出しに起因する独自のコンテンツオフセットを設定するコレクションビューによる試みを完全に拒否することです。経験的には、この変更は直接の呼び出しで直接発生するように見えますが、これは明らかにインターフェイスやドキュメントでは保証されていませんが、Core Animationの観点からは理にかなっているので、おそらく仮定に50%しか不快ではありません。
ただし、2番目の問題がありました。UICollectionViewは、新しいコレクションビューレイアウトで同じ場所に残っていたビューに少しジャンプし、約240ポイント押し下げてから元の位置にアニメートしていました。なぜかはわかりませんが、実際に動いていないセルに追加されたCAAnimation
sを切断することで、コードを修正して対処しました。
- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated
{
// collect up the positions of all existing subviews
NSMutableDictionary *positionsByViews = [NSMutableDictionary dictionary];
for(UIView *view in [self subviews])
{
positionsByViews[[NSValue valueWithNonretainedObject:view]] = [NSValue valueWithCGPoint:[[view layer] position]];
}
// apply the new layout, declining to allow the content offset to change
_declineContentOffset ++;
[super setCollectionViewLayout:layout animated:animated];
_declineContentOffset --;
// run through the subviews again...
for(UIView *view in [self subviews])
{
// if UIKit has inexplicably applied animations to these views to move them back to where
// they were in the first place, remove those animations
CABasicAnimation *positionAnimation = (CABasicAnimation *)[[view layer] animationForKey:@"position"];
NSValue *sourceValue = positionsByViews[[NSValue valueWithNonretainedObject:view]];
if([positionAnimation isKindOfClass:[CABasicAnimation class]] && sourceValue)
{
NSValue *targetValue = [NSValue valueWithCGPoint:[[view layer] position]];
if([targetValue isEqualToValue:sourceValue])
[[view layer] removeAnimationForKey:@"position"];
}
}
}
これは、実際に移動するビューを阻害したり、誤って移動したりするようには見えません(周囲のすべてが約240ポイント下がって、正しい位置にアニメートされると期待しているように)。
これが私の現在の解決策です。
おそらく、2週間ほどかけて、さまざまなレイアウトをスムーズに切り替えられるようにしようとしています。提案されたオフセットのオーバーライドがiOS 10.2で機能していることがわかりましたが、それより前のバージョンではまだ問題が発生します。私の状況を少し悪化させるのは、スクロールの結果として別のレイアウトに移行する必要があるため、ビューがスクロールと移行の両方を同時に行うことです。
Tommyの答えは、10.2より前のバージョンで私のために働いた唯一のものでした。現在、次のことを行っています。
class HackedCollectionView: UICollectionView {
var ignoreContentOffsetChanges = false
override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
guard ignoreContentOffsetChanges == false else { return }
super.setContentOffset(contentOffset, animated: animated)
}
override var contentOffset: CGPoint {
get {
return super.contentOffset
}
set {
guard ignoreContentOffsetChanges == false else { return }
super.contentOffset = newValue
}
}
override func setCollectionViewLayout(_ layout: UICollectionViewLayout, animated: Bool) {
guard ignoreContentOffsetChanges == false else { return }
super.setCollectionViewLayout(layout, animated: animated)
}
override var contentSize: CGSize {
get {
return super.contentSize
}
set {
guard ignoreContentOffsetChanges == false else { return }
super.contentSize = newValue
}
}
}
次に、レイアウトを設定するときにこれを行います...
let theContentOffsetIActuallyWant = CGPoint(x: 0, y: 100)
UIView.animate(withDuration: animationDuration,
delay: 0, options: animationOptions,
animations: {
collectionView.setCollectionViewLayout(layout, animated: true, completion: { completed in
// I'm also doing something in my layout, but this may be redundant now
layout.overriddenContentOffset = nil
})
collectionView.ignoreContentOffsetChanges = true
}, completion: { _ in
collectionView.ignoreContentOffsetChanges = false
collectionView.setContentOffset(theContentOffsetIActuallyWant, animated: false)
})