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

我想出了一个解决方法,它似乎可以解决问题,但对所提供的示例非常具体。我的猜测是,当单元格被重用时,它们的起点错误,这会导致奇怪的动画。

我将 Storyboard 更改为使用 UICollectionViewFlowLayout 的子类:

// 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 的答案,这就是我通过翻译Gareth 的答案得到的。

一个基类,定义EnableAnimationFix了两个虚拟方法:ApplyAnimationFixToAppearingItemApplyAnimationFixToDisappearingItem.

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 回答
0

我一直在使用 reloadData 或 reloadItemsAtIndexPaths 遇到各种奇怪的动画行为。当我不使用这些方法时,动画行为似乎像宣传的那样工作。

于 2013-02-14T22:53:51.683 回答