在UICollectionView布局转换中完善子视图大小调整

Rob*_*egg 11 cocoa-touch objective-c ios uicollectionview

我正在开发一个具有以下功能的应用程序UICollectionView- 集合视图的工作是显示来自Web服务的数据.

我试图实现的应用程序的一个功能是使用户能够将其布局UICollectionView从网格视图更改为表格视图.

我花了很多时间来完善这个,并设法让它发挥作用.但是有一些问题.两个布局之间的转换看起来不太好,有时它在切换视图之间断开,我的应用程序留下了处于意外状态的视图.只有当用户在网格和表格视图之间快速切换(changeLayoutButton连续按下)时才会发生这种情况.

所以,显然存在一些问题,我觉得代码有点脆弱.我还需要解决上面提到的问题.

我将从如何实现此视图开始.

履行

因为我需要两个不同的单元格(grideCell和tableViewCell)来显示不同的东西 - 我认为继承它会更好,UICollectionViewFlowLayout因为它可以完成我需要的一切 - 我需要做的就是改变单元格大小.

考虑到这一点,我创建了两个子类 UICollectionViewFlowLayout

这就是这两个类的外观:

BBTradeFeedTableViewLayout.m

    #import "BBTradeFeedTableViewLayout.h"

@implementation BBTradeFeedTableViewLayout

-(id)init
{
    self = [super init];

    if (self){

        self.itemSize = CGSizeMake(320, 80);
        self.minimumLineSpacing = 0.1f;
    }

    return self;
}

@end
Run Code Online (Sandbox Code Playgroud)

BBTradeFeedGridViewLayout.m

#import "BBTradeFeedGridViewLayout.h"

@implementation BBTradeFeedGridViewLayout

-(id)init
{
    self = [super init];

    if (self){

        self.itemSize = CGSizeMake(159, 200);
        self.minimumInteritemSpacing = 2;
        self.minimumLineSpacing = 3;
    }
    return self; 
}

@end
Run Code Online (Sandbox Code Playgroud)

非常简单,正如您所看到的 - 只需更改单元格大小即可.

然后在我的viewControllerA课上我实现了UICollectionView这样的:创建了属性:

    @property (strong, nonatomic) BBTradeFeedTableViewLayout *tableViewLayout;
@property (strong, nonatomic) BBTradeFeedGridViewLayout *grideLayout;
Run Code Online (Sandbox Code Playgroud)

在 viewDidLoad

/* Register the cells that need to be loaded for the layouts used */
    [self.tradeFeedCollectionView registerNib:[UINib nibWithNibName:@"BBItemTableViewCell" bundle:nil] forCellWithReuseIdentifier:@"TableItemCell"];
    [self.tradeFeedCollectionView registerNib:[UINib nibWithNibName:@"BBItemGridViewCell" bundle:nil] forCellWithReuseIdentifier:@"GridItemCell"];
Run Code Online (Sandbox Code Playgroud)

用户点击按钮可在布局之间切换:

-(void)changeViewLayoutButtonPressed
Run Code Online (Sandbox Code Playgroud)

我使用BOOL来确定哪个布局当前处于活动状态,并根据我使用此代码进行切换:

[self.collectionView performBatchUpdates:^{

        [self.collectionView.collectionViewLayout invalidateLayout];
        [self.collectionView setCollectionViewLayout:self.grideLayout animated:YES];

    } completion:^(BOOL finished) {
    }];
Run Code Online (Sandbox Code Playgroud)

在 cellForItemAtIndexPath

我确定应该使用哪些单元格(grid或tableView)并加载数据 - 该代码如下所示:

if (self.gridLayoutActive == NO){

            self.switchToTableLayout = NO;
            BBItemTableViewCell *tableItemCell = [collectionView dequeueReusableCellWithReuseIdentifier:tableCellIdentifier forIndexPath:indexPath];

            if ([self.searchArray count] > 0){

                self.switchToTableLayout = NO;

                tableItemCell.gridView = NO;
                tableItemCell.backgroundColor = [UIColor whiteColor];
                tableItemCell.item = self.searchArray[indexPath.row];

            }
            return tableItemCell;
        }else

        {

            BBItemTableViewCell *gridItemCell= [collectionView dequeueReusableCellWithReuseIdentifier:gridCellIdentifier forIndexPath:indexPath];

            if ([self.searchArray count] > 0){

                self.switchToTableLayout = YES;

                gridItemCell.gridView = YES;
                gridItemCell.backgroundColor = [UIColor whiteColor];
                gridItemCell.item = self.searchArray[indexPath.row];

            }

            return gridItemCell;
        }
Run Code Online (Sandbox Code Playgroud)

最后在两个单元类中 - 我只是使用数据来设置我需要的图像/文本.

同样在网格单元格中 - 图像更大,我删除了我不想要的文本 - 这是使用两个单元格的主要原因.

我对如何使这个视图在UI中看起来更流畅,更少有问题感兴趣.我想要的外观就像eBays iOS应用程序 - 它们在三种不同的视图之间切换.我只需要在两个不同的视图之间切换.

Stu*_*art 18

@ jrturton的回答是有帮助的,但是除非我遗漏了一些东西,否则它真的过于复杂,非常简单.我将从我们同意的观点开始......

在更改布局时防止交互

首先,我同意在布局转换开始时禁用用户交互并在结束时(在完成块中)重新启用的方法[[UIApplication sharedApplication] begin/endIgnoringInteractionEvents]- 这比尝试取消正在进行的转换动画并立即开始反向要好得多从当前状态转变.

使用单个单元类简化布局转换

另外,我非常同意为每个布局使用相同的单元类的建议.在中注册单个单元类viewDidLoad,并简化您的collectionView:cellForItemAtIndexPath:方法以使单元格出列并设置其数据:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    BBItemCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellID forIndexPath:indexPath];
    if ([self.searchArray count] > 0) {
        cell.backgroundColor = [UIColor whiteColor];
        cell.item = self.searchArray[indexPath.row];
    }
    return cell;
}
Run Code Online (Sandbox Code Playgroud)

(请注意,单元本身不应该(除了特殊情况外)需要知道与当前使用的布局有关的任何内容,布局是否正在转换,或者当前的转换进度是什么)

然后当你调用setCollectionViewLayout:animated:completion:集合视图时不需要重新加载任何新单元格,它只是设置一个动画块来改变每个单元格的布局属性(你不需要从一个performBatchUpdates:块里面调用这个方法,你也不需要手动使布局无效).

动画单元子视图

然而,正如所指出的,您会注意到单元格的子视图会立即跳转到新布局的框架.解决方案是在更新布局属性时简单地强制单元子视图的立即布局:

- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
    [super applyLayoutAttributes:layoutAttributes];
    [self layoutIfNeeded];
}
Run Code Online (Sandbox Code Playgroud)

(无需为转换创建特殊的布局属性)

为什么这样做?当集合视图更改布局时,applyLayoutAttributes:将为每个单元调用,因为集合视图正在为该过渡设置动画块.但是单元格子视图的布局不会立即完成 - 它会延迟到后来的运行循环 - 导致实际的子视图布局更改没有合并到动画块中,因此子视图会立即跳转到其最终位置.调用layoutIfNeeded意味着我们告诉单元格我们希望子视图布局立即发生,因此布局在动画块中完成,子视图的框架与单元格本身一起动画化.

确实,使用标准setCollectionViewLayout:...API会限制对动画计时的控制.如果您想应用自定义缓动动画曲线,那么解决方案就像TLLayoutTransitioning展示了利用交互式UICollectionViewTransitionLayout对象来控制动画时序的便捷方式.但是,只要只需要子视图的线性动画,我想大多数人都会对默认动画感到满意,特别是考虑到实现它的单行简单性.

为了记录,我并不热衷于自己对这个动画缺乏控制,所以实现了类似的东西TLLayoutTransitioning.如果这也适用于你,那么请忽略我对@ jrturton的其他很好的答案的严厉批评,并调查TLLayoutTransitioning或UICollectionViewTransitionLayout实现与计时器:)

  • 我下次肯定会这样尝试,总是很高兴听到更好的答案.+1 (2认同)

jrt*_*ton 13

网格/表格转换并不像您所相信的那么简单.当你在单元格的中间有一个标签和一个坚实的背景时,它们可以正常工作,但是一旦你有任何真正的内容,它就会失败.这就是为什么:

  • 您无法控制动画的时间和性质.
  • 虽然布局中的单元格框架是从一个值到下一个值的动画,但单元格本身(特别是如果您使用两个单独的单元格)似乎不会为动画的每个步骤执行内部布局,因此它似乎"轻弹"从一个布局到每个单元格内的下一个布局 - 您的网格单元格在表格大小上看起来不对,反之亦然.

有许多不同的解决方案.如果没有看到你的细胞内容,很难推荐任何具体的东西,但我已经取得了以下成功:

  • 使用这里显示的技术控制动画.您还可以查看Facebook Pop以更好地控制转换,但我没有详细研究过.
  • 两个布局使用相同的单元格.在layoutSubviews中,计算从一个布局到另一个布局的过渡距离,并使用它来淡出或未使用的元素,并为其他元素计算漂亮的过渡帧.这可以防止从一个单元类到另一个单元类的切换.

这是我在这里使用的方法,效果相当不错.

依靠调整面具或Autolayout的大小是一项艰巨的工作,但这是额外的工作,使事情看起来很好.

至于用户可以在布局之间切换太快的问题 - 只需在转换开始时禁用按钮,并在完成后重新启用它.

作为一个更实际的例子,这里是上面链接的应用程序的布局更改示例(其中一些被省略).请注意,在转换发生时禁用交互,我使用上面链接的项目的转换布局,并且有一个完成处理程序:

-(void)toggleLayout:(UIButton*)sender
{
    [[UIApplication sharedApplication] beginIgnoringInteractionEvents];

    HMNNewsLayout newLayoutType = self.layoutType == HMNNewsLayoutTable ? HMNNewsLayoutGrid : HMNNewsLayoutTable;

    UICollectionViewLayout *newLayout = [HMNNewsCollectionViewController collectionViewLayoutForType:newLayoutType];

    HMNTransitionLayout *transitionLayout = (HMNTransitionLayout *)[self.collectionView transitionToCollectionViewLayout:newLayout duration:0.5 easing:QuarticEaseInOut completion:^(BOOL completed, BOOL finish)
            {
                [[NSUserDefaults standardUserDefaults] setInteger:newLayoutType forKey:HMNNewsLayoutTypeKey];
                self.layoutType = newLayoutType;
                sender.selected = !sender.selected;
                for (HMNNewsCell *cell in self.collectionView.visibleCells)
                {
                    cell.layoutType = newLayoutType;
                }
                [[UIApplication sharedApplication] endIgnoringInteractionEvents];
            }];

    [transitionLayout setUpdateLayoutAttributes:^UICollectionViewLayoutAttributes *(UICollectionViewLayoutAttributes *layoutAttributes, UICollectionViewLayoutAttributes *fromAttributes, UICollectionViewLayoutAttributes *toAttributes, CGFloat progress)
    {
        HMNTransitionLayoutAttributes *attributes = (HMNTransitionLayoutAttributes *)layoutAttributes;
        attributes.progress = progress;
        attributes.destinationLayoutType = newLayoutType;
        return attributes;
    }];
}
Run Code Online (Sandbox Code Playgroud)

在单元格内部,这两个布局都是相同的单元格,我有一个图像视图和一个标签容器.标签容器包含所有标签,并使用自动布局在内部放置它们.每个布局中的图像视图和标签容器都有常量框架变量.

转换布局中的布局属性是一个自定义子类,其中包含一个转换进度属性,在上面的更新布局属性块中设置.这是使用applyLayoutAttributes方法传递给单元格(省略了一些其他代码):

-(void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
    self.transitionProgress = 0;
    if ([layoutAttributes isKindOfClass:[HMNTransitionLayoutAttributes class]])
    {
        HMNTransitionLayoutAttributes *attributes = (HMNTransitionLayoutAttributes *)layoutAttributes;
        self.transitionProgress = attributes.progress;
    }
    [super applyLayoutAttributes:layoutAttributes];
}
Run Code Online (Sandbox Code Playgroud)

layoutSubviews 如果正在进行转换,则在单元子类中对图像和标签的两个帧之间进行插值的艰苦工作:

-(void)layoutSubviews
{
    [super layoutSubviews];

    if (!self.transitionProgress)
    {
        switch (self.layoutType)
        {
            case HMNNewsLayoutTable:
                self.imageView.frame = imageViewTableFrame;
                self.labelContainer.frame = labelContainerTableFrame;
                break;
            case HMNNewsLayoutGrid:
                self.imageView.frame = imageViewGridFrame;
                self.labelContainer.frame = self.originalGridLabelFrame;
                break;
        }
    }
    else
    {
        CGRect fromImageFrame,toImageFrame,fromLabelFrame,toLabelFrame;
        if (self.layoutType == HMNNewsLayoutTable)
        {
            fromImageFrame = imageViewTableFrame;
            toImageFrame = imageViewGridFrame;
            fromLabelFrame = labelContainerTableFrame;
            toLabelFrame = self.originalGridLabelFrame;
        }
        else
        {
            fromImageFrame = imageViewGridFrame;
            toImageFrame = imageViewTableFrame;
            fromLabelFrame = self.originalGridLabelFrame;
            toLabelFrame = labelContainerTableFrame;
        }

        CGFloat from = 1.0 - self.transitionProgress;
        CGFloat to = self.transitionProgress;

        self.imageView.frame = (CGRect)
        {
            .origin.x = from * fromImageFrame.origin.x + to * toImageFrame.origin.x,
            .origin.y = from * fromImageFrame.origin.y + to * toImageFrame.origin.y,
            .size.width = from * fromImageFrame.size.width + to * toImageFrame.size.width,
            .size.height = from * fromImageFrame.size.height + to * toImageFrame.size.height
        };

        self.labelContainer.frame = (CGRect)
        {
            .origin.x = from * fromLabelFrame.origin.x + to * toLabelFrame.origin.x,
            .origin.y = from * fromLabelFrame.origin.y + to * toLabelFrame.origin.y,
            .size.width = from * fromLabelFrame.size.width + to * toLabelFrame.size.width,
            .size.height = from * fromLabelFrame.size.height + to * toLabelFrame.size.height
        };
    }
    self.headlineLabel.preferredMaxLayoutWidth = self.labelContainer.frame.size.width;
}
Run Code Online (Sandbox Code Playgroud)

这就是它.基本上你需要一种方法来告诉单元格过渡到底有多远,你需要布局转换库(或者,正如我所说,Facebook pop可能会这样做),然后你需要确保你得到很好的价值用于在两者之间转换时的布局.