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

时间:2014-05-09 12:16:17

标签: ios objective-c cocoa-touch uicollectionview

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

我尝试实现的应用程序的一个功能是允许用户将此UICollectionView的布局从网格视图更改为表格视图。

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

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

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

实施

因为我需要两个不同的单元格(grideCelltableViewCell)来展示不同的东西 - 我认为继承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

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

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

然后在我的viewControllerA课程中,我实现了UICollectionView,如下所示: 创建了属性:

    @property (strong, nonatomic) BBTradeFeedTableViewLayout *tableViewLayout;
@property (strong, nonatomic) BBTradeFeedGridViewLayout *grideLayout;
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"];

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

-(void)changeViewLayoutButtonPressed

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

[self.collectionView performBatchUpdates:^{

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

    } completion:^(BOOL finished) {
    }];

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;
        }

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

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

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

2 个答案:

答案 0 :(得分: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;
}

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

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

设置单元格子视图的动画

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

- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
    [super applyLayoutAttributes:layoutAttributes];
    [self layoutIfNeeded];
}

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

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

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

为了记录,我自己并不热衷于缺乏对此动画的控制,因此实现了与TLLayoutTransitioning类似的功能。如果这也适用于你,那么请忽略我对@ jrturton的严厉批评,否则很好的答案,并查看用计时器实现的TLLayoutTransitioningUICollectionViewTransitionLayout:)

答案 1 :(得分:13)

网格/表格转换并不像简单的演示那么容易让你相信。当您在单元格中间有一个标签并且背景稳固时,它们可以正常工作,但是一旦您有任何真实的内容,它就会失败。这就是原因:

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

有许多不同的解决方案。在没有看到您的手机内容的情况下,很难推荐任何具体内容,但我已经取得了以下成功:

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

我使用here的方法取得了相当不错的效果。

依靠调整面具大小或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;
    }];
}

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

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

-(void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
    self.transitionProgress = 0;
    if ([layoutAttributes isKindOfClass:[HMNTransitionLayoutAttributes class]])
    {
        HMNTransitionLayoutAttributes *attributes = (HMNTransitionLayoutAttributes *)layoutAttributes;
        self.transitionProgress = attributes.progress;
    }
    [super applyLayoutAttributes:layoutAttributes];
}
如果正在进行转换,则单元子类中的

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;
}

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