8

UICollectionView からアイテムを挿入または削除すると、アニメーション中に余分なセルが表示され、この余分なセルが間違った方向に移動するように見えます。UITableView でまったく同じことを試しましたが、問題はありません。

問題のビデオはこちら: https://dl.dropbox.com/u/11523469/CollectionViewBug.movで、左側にコレクション ビュー、右側にテーブル ビューがあります。各セルの数字は、セルが作成されたときのセルの indexPath.item 値です。

この問題は、ビデオの 0:08 から 0:12 (挿入) の間で最初に顕著になり、次に 0:16 から 0:20 (削除) で再び顕著になります。

プロジェクトはこちらから入手できます: https://dl.dropbox.com/u/11523469/CollectionViewBug.zip

つまり、セルを挿入すると、セルが挿入されている場所の下にあるすべてのセルが下に移動して、新しいセルのためのスペースが作られます。しかし、この余分なセルが現れ、他のセルと重なり、上に移動します。

同様に、セルを削除すると、削除されるセルの下にあるすべてのセルが上に移動して、セルがあった場所のギャップを埋めます。しかし、この余分なセルが現れ、他のセルと重なり、下に移動します。

コレクション ビューで実行される最初のアクション (挿入または削除) では、この問題は発生しません。しかし、その後のすべてのアクションには問題があります。

他の誰かが UICollectionView で同じ問題を経験しましたか? 誰かが解決策または回避策を持っていますか?

ありがとう!

4

3 に答える 3

12

問題を解決しているように見える回避策を思いつきましたが、提供された例に非常に固有のものです。私の推測では、セルが再利用されると、奇妙なアニメーションの原因となる開始点が間違っていると思います。

UICollectionViewFlowLayout のサブクラスを使用するように Storyboard を変更しました。

// MyFlowLayout - subclass of UICollectionViewFlowLayout

#import "MyFlowLayout.h"

@interface MyFlowLayout ()

@property (strong) NSMutableArray *deleteIndexPaths;
@property (strong) NSMutableArray *insertIndexPaths;
@property (assign) float rowOffset;

@end

@implementation MyFlowLayout

-(id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super initWithCoder:aDecoder])
    {
        // minimumInteritemSpacing may be adjusted upwards but this example ignores that
        self.rowOffset = self.itemSize.height + self.minimumInteritemSpacing;
    }

    return self;
}

// As per Mark Pospesel corrections to CircleLayout

- (void)prepareForCollectionViewUpdates:(NSArray *)updateItems
{
    // Keep track of insert and delete index paths
    [super prepareForCollectionViewUpdates:updateItems];

    self.deleteIndexPaths = [NSMutableArray array];
    self.insertIndexPaths = [NSMutableArray array];

    for (UICollectionViewUpdateItem *update in updateItems)
    {
        if (update.updateAction == UICollectionUpdateActionDelete)
        {
            [self.deleteIndexPaths addObject:update.indexPathBeforeUpdate];
        }
        else if (update.updateAction == UICollectionUpdateActionInsert)
        {
            [self.insertIndexPaths addObject:update.indexPathAfterUpdate];
        }
    }
}

- (void)finalizeCollectionViewUpdates
{
    [super finalizeCollectionViewUpdates];
    // release the insert and delete index paths
    self.deleteIndexPaths = nil;
    self.insertIndexPaths = nil;
}

// The next two methods have misleading names as they get called for all visible cells on     both insert and delete

- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    // Must call super
    UICollectionViewLayoutAttributes *attributes = [super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];
    if (!attributes)
        attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];

    if ([self.insertIndexPaths containsObject:itemIndexPath]) {
        // Initial position for an inserted cell is it's final position - fades in
        CGRect frame = attributes.frame;
        frame.origin.y = itemIndexPath.row * self.rowOffset;
        attributes.frame = frame;
        attributes.zIndex = -1; // stop the inserted cell bleeding through too early in the animation
    }
    if ([self.deleteIndexPaths count]) {
        NSIndexPath *deletedPath = self.deleteIndexPaths[0];  // Might be more than one but this example ignores that
        if (itemIndexPath.row > deletedPath.row) {
            // Anything after the deleted cell needs to slide up from the position below it's final position
            // Anything before the deleted cell doesn't need adjusting
            CGRect frame = attributes.frame;
            frame.origin.y = ((itemIndexPath.row + 1) * self.rowOffset);
            attributes.frame = frame;
        }
    }

    return attributes;
}

- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    UICollectionViewLayoutAttributes *attributes = [super finalLayoutAttributesForDisappearingItemAtIndexPath:itemIndexPath];
    if (!attributes)
        attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];

    // I would have expected the final positions to already be correct but my guess is that re-used cells
    // are not considered until after the animation block settings have been generated
    CGRect frame = attributes.frame;
    frame.origin.y = itemIndexPath.row * self.rowOffset;
    attributes.frame = frame;

    if ([self.deleteIndexPaths containsObject:itemIndexPath]) {
        // Fade out the deleted cell
        attributes.alpha = 0.0;
    }

    return attributes;
}

@end
于 2013-01-27T06:34:44.280 に答える
1

誰かが MonoTouch の回答を探してここに来た場合、これがガレスの回答を翻訳して得たものです。

EnableAnimationFixおよび 2 つの仮想メソッドを定義する基本クラス:ApplyAnimationFixToAppearingItemおよびApplyAnimationFixToDisappearingItem.

public class CollectionViewFlowLayout : UICollectionViewFlowLayout
{   
    protected List<int> _insertedItems = new List<int> ();
    protected List<int> _deletedItems = new List<int> ();

    protected virtual bool EnableAnimationFix {
        get { return false; }
    }

    protected virtual void ApplyAnimationFixToAppearingItem (int index, UICollectionViewLayoutAttributes attrs)
    {
        throw new NotImplementedException ();
    }

    protected virtual void ApplyAnimationFixToDisappearingItem (int index, UICollectionViewLayoutAttributes attrs)
    {
        throw new NotImplementedException ();
    }

    public override UICollectionViewLayoutAttributes InitialLayoutAttributesForAppearingItem (NSIndexPath path)
    {
        var attrs = base.InitialLayoutAttributesForAppearingItem (path);
        if (!EnableAnimationFix) {
            return attrs;
        }

        attrs = attrs ?? LayoutAttributesForItem (path);
        if (attrs != null)
            ApplyAnimationFixToAppearingItem (path.Row, attrs);

        return attrs;
    }

    public override UICollectionViewLayoutAttributes FinalLayoutAttributesForDisappearingItem (NSIndexPath path)
    {
        var attrs = base.FinalLayoutAttributesForDisappearingItem (path);
        if (!EnableAnimationFix) {
            return attrs;
        }

        if (attrs == null && _deletedItems.Contains (path.Row)) {
            // Calling LayoutAttributesForItem will cause an exception so we return now.
            // I think this happens when last and only item is deleted, and there are no other cells in cell pool.
            return null;
        }

        attrs = attrs ?? LayoutAttributesForItem (path);
        if (attrs != null)
            ApplyAnimationFixToDisappearingItem (path.Row, attrs);

        return attrs;
    }

    public override void PrepareForCollectionViewUpdates (UICollectionViewUpdateItem [] updateItems)
    {
        base.PrepareForCollectionViewUpdates (updateItems);

        if (!EnableAnimationFix)
            return;

        _insertedItems.Clear ();
        _deletedItems.Clear ();

        foreach (var update in updateItems) {
            if (update.UpdateAction == UICollectionUpdateAction.Insert) {
                _insertedItems.Add (update.IndexPathAfterUpdate.Row);
            } else if (update.UpdateAction == UICollectionUpdateAction.Delete) {
                _deletedItems.Add (update.IndexPathBeforeUpdate.Row);
            }
        }
    }

    public override void FinalizeCollectionViewUpdates ()
    {
        base.FinalizeCollectionViewUpdates ();

        if (!EnableAnimationFix)
            return;

        _insertedItems.Clear ();
        _deletedItems.Clear ();
    }
}

そして、実際のコレクション ビュー レイアウト コードが続きます。

public class DraftsLayout : CollectionViewFlowLayout
{

    // ... 


    protected override bool EnableAnimationFix {
        get { return true; }
    }

    protected override void ApplyAnimationFixToAppearingItem (int index, UICollectionViewLayoutAttributes attrs)
    {
        if (_insertedItems.Contains (index)) {
            SetXByIndex (attrs, index);
            attrs.ZIndex = -1;
        }

        int deletedToTheLeft = _deletedItems.Count (i => i < index);
        if (deletedToTheLeft > 0) {
            SetXByIndex (attrs, index + deletedToTheLeft);
        }
    }

    protected override void ApplyAnimationFixToDisappearingItem (int index, UICollectionViewLayoutAttributes attrs)
    {
        SetXByIndex (attrs, index);

        if (_deletedItems.Contains (index)) {
            attrs.Alpha = 0;
        }
    }

    const int SnapStep = 150;
    static void SetXByIndex (UICollectionViewLayoutAttributes attrs, int index)
    {
        var frame = attrs.Frame;
        frame.X = index * SnapStep;
        attrs.Frame = frame;
    }
}

このコードは、バッチ内の複数の削除を適切に処理する可能性があることに注意してください。
ガレスに敬意を表します。

于 2013-03-27T16:09:26.177 に答える