71

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が、アニメーションが必要なので、アニメーション化されたケースの解決策や回避策があるかどうかを知りたいと思っています。

補足として、私はカスタムレイアウトをテストし、同じ動作をしました。

4

10 に答える 10

38

UICollectionViewLayouttargetContentOffsetForProposedContentOffset:レイアウトの変更中に適切なコンテンツオフセットを提供できるオーバーライド可能なメソッドが含まれています。これにより、正しくアニメーション化されます。これはiOS7.0以降で利用可能です

于 2014-08-20T23:48:59.553 に答える
33

私は何日もこれに髪を引っ張ってきて、助けになるかもしれない私の状況の解決策を見つけました。私の場合、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)));
}
于 2013-03-25T15:34:02.727 に答える
7

この問題も私を悩ませました、そしてそれは移行コードのバグのようです。私の知る限り、遷移前のビューレイアウトの中心に最も近いセルに焦点を合わせようとします。ただし、ビューの遷移前の中心にセルが存在しない場合でも、セルが遷移後になる場所の中心に配置しようとします。これは、alwaysBounceVertical /Horizo​​ntalを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];
}
于 2012-12-28T20:21:08.100 に答える
7

私自身の質問への遅い答えで飛び込みます。

TLLayoutTransitioningライブラリは、iOS7のインタラクティブなトランジションAPIを再タスクして、非インタラクティブなレイアウトからレイアウトへのトランジションを実行することにより、この問題に対する優れたソリューションを提供します。setCollectionViewLayoutこれは、コンテンツオフセットの問題を解決し、いくつかの機能を追加するための代替手段を効果的に提供します。

  1. アニメーションの長さ
  2. 30以上のイージングカーブ(Warren MooreのAHEasingライブラリの提供)
  3. 複数のコンテンツオフセットモード

カスタムイージングカーブはAHEasingFunction関数として定義できます。最終的なコンテンツオフセットは、最小、中央、上、左、下、または右の配置オプションを使用して、1つ以上のインデックスパスで指定できます。

私が何を意味するのかを理解するには、ExamplesワークスペースでResizeデモを実行し、オプションを試してみてください。

使い方はこんな感じ。まず、次のインスタンスを返すようにViewControllerを構成しますTLTransitionLayout

- (UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout
{
    return [[TLTransitionLayout alloc] initWithCurrentLayout:fromLayout nextLayout:toLayout];
}

次に、を呼び出す代わりに、カテゴリで定義されたsetCollectionViewLayout呼び出しを呼び出します。transitionToCollectionViewLayout:toLayoutUICollectionView-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は、指定された期間とイージング関数で遷移の進行を駆動するコールバックを開始します。

次のステップは、最終的なコンテンツオフセットを指定することです。任意の値を指定できますが、でtoContentOffsetForLayout定義されているメソッドUICollectionView-TLLayoutTransitioningは、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;
于 2014-02-17T00:29:46.353 に答える
7

2019実際のソリューション

「車」ビューのレイアウトがいくつかあるとします。

3つあるとしましょう。

CarsLayout1: UICollectionViewLayout { ...
CarsLayout2: UICollectionViewLayout { ...
CarsLayout3: UICollectionViewLayout { ...

レイアウト間をアニメーション化するとジャンプします

これはAppleによる否定できない間違いです。アニメートすると、間違いなくジャンプします。

修正はこれです:

グローバルフロートと、次の基本クラスが必要です。

var avoidAppleMessupCarsLayouts: CGPoint? = nil

class FixerForCarsLayouts: UICollectionViewLayout {
    
    override func prepareForTransition(from oldLayout: UICollectionViewLayout) {
        avoidAppleMessupCarsLayouts = collectionView?.contentOffset
    }
    
    override func targetContentOffset(
        forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
        if avoidAppleMessupCarsLayouts != nil {
            return avoidAppleMessupCarsLayouts!
        }
        return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
    }
}

「車」画面の3つのレイアウトは次のとおりです。

CarsLayout1: FixerForCarsLayouts { ...
CarsLayout2: FixerForCarsLayouts { ...
CarsLayout3: FixerForCarsLayouts { ...

それでおしまい。これで動作します。


  1. 信じられないほどあいまいなことに、(車、犬、家などの)レイアウトのさまざまな「セット」があり、(おそらく)衝突する可能性があります。このため、「セット」ごとに上記のグローバルクラスとベースクラスを用意します。

  2. これは、何年も前に、上記のユーザー@Isaacliuを渡すことによって発明されました。

  3. Isaacliuのコードフラグメントの詳細FWIWfinalizeLayoutTransitionが追加されました。実際、論理的には必要ありません。

実際のところ、Appleがその動作を変更するまでは、コレクションビューのレイアウト間でアニメーションを作成するたびに、これを行う必要があります。それが人生!

于 2019-09-26T02:47:02.877 に答える
5

簡単。

新しいレイアウトと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一定に保たれます。

于 2015-02-06T05:35:31.537 に答える
3

レイアウトから移行するときに変更されないコンテンツオフセットを単に探している場合は、カスタムレイアウトを作成し、いくつかのメソッドをオーバーライドして、古い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。かなり簡単

于 2016-02-06T19:49:01.767 に答える
2

それが経験の本体に追加するのに役立つ場合:コンテンツのサイズ、コンテンツインセットを設定したかどうか、またはその他の明らかな要因に関係なく、この問題が永続的に発生しました。したがって、私の回避策はやや抜本的でした。まず、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ポイント下がって、正しい位置にアニメートすることを期待しているかのように)。

これが私の現在の解決策です。

于 2014-02-13T21:16:58.860 に答える
0

さまざまなレイアウトをスムーズに移行できるようにするために、おそらく2週間ほど費やしました。提案されたオフセットのオーバーライドはiOS10.2で機能していることがわかりましたが、それ以前のバージョンでも問題が発生します。私の状況を少し悪化させているのは、スクロールの結果として別のレイアウトに遷移する必要があるため、ビューが同時にスクロールと遷移の両方になっていることです。

トミーの答えは、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)
})
于 2017-02-23T22:28:59.203 に答える
-3

これはついに私のために働いた(Swift 3)

self.collectionView.collectionViewLayout = UICollectionViewFlowLayout()
self.collectionView.setContentOffset(CGPoint(x: 0, y: -118), animated: true)
于 2017-08-29T02:08:01.727 に答える