【问题标题】:Perfecting subview resizing in UICollectionView layout transition完善 UICollectionView 布局转换中的子视图大小调整
【发布时间】:2014-06-27 04:25:18
【问题描述】:

我正在开发一个具有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 中看起来更流畅、更少错误。我想要的外观就像 eBay 的 iOS 应用程序一样——它们在三种不同的视图之间切换。我只需要在两个不同的视图之间切换。

【问题讨论】:

  • 您是否尝试过在过渡期间禁用更改布局按钮?
  • 这可以工作 - 但我觉得它是一个黑客。我想让整个传统更加强大。
  • 这根本不是 hack。如果您考虑一下 - 您不想在原始动画中间来回传输。
  • 我想你是对的。我会在动画期间禁用它并在完成块中启用它吗?
  • @Tender:完全正确

标签: ios objective-c cocoa-touch uicollectionview


【解决方案1】:

网格/表格转换并不像简单的演示那样简单。当您在单元格中间有一个标签和纯色背景时,它们可以正常工作,但是一旦您在其中有任何 真实 内容,它就会倒下。这就是为什么:

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

有许多不同的解决方案。如果没有看到您单元格的内容,很难推荐任何具体的内容,但我在以下方面取得了成功:

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

这就是我使用here 的方法,效果还不错。

依靠调整蒙版大小或自动布局更难,但让事情看起来更好的是额外的工作。

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

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

-(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 可能会这样做),然后您需要确保获得不错的值用于在两者之间转换时的布局。

【讨论】:

  • 非常感谢您提供的信息。我下载了你链接的应用程序,这正是我所追求的效果。我的也是这样。但是它看起来不像你的那么干净。我的单元格内容与您的几乎相同。一张照片和一些文字串。我不知道如何进行您提到的计算。我从来没有对 UICollectionView 做过比我在回答中显示的更多的事情。你能给我一点财富吗?感谢您的帮助!
  • 这必须等到我回到办公室 - 我想在更详细的建议之前确认我是如何做到的。
  • 完全没问题。我会等到你准备好。再次感谢!
  • @jrturon 只是检查您是否有机会确认您是如何在您的应用程序中执行此操作以提供更多详细信息?再次感谢
  • 欣赏代码sn-ps。我今晚回家时会经历它,回来时会更新。再次感谢!
【解决方案2】:

@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: 时,collection view 不需要重新加载任何新的单元格,它只是设置一个动画块来改变每个单元格的布局属性(你不需要从 @ 内部调用这个方法987654327@ 块,也不需要手动使布局无效)。

动画单元格子视图

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

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

(无需为过渡创建特殊的布局属性)

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

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

为了记录,我自己并不热衷于缺乏对这个动画的控制,所以实现了类似于 TLLayoutTransitioning 的东西。如果这也适用于您,那么请忽略我对@jrturton 其他出色答案的严厉谴责,并查看使用计时器实现的TLLayoutTransitioningUICollectionViewTransitionLayouts :)

【讨论】:

  • 感谢您的详细解答。我没有时间更改我当前的设置。我将在几周后重新访问,希望能够做得更好。我也会看看建议的图书馆。但是,我想推出自己的解决方案,因为我想避免过度依赖当前项目中的太多库。再次感谢您!
  • @Tander 没问题,很抱歉我迟到了!在避免依赖方面,我完全支持你……我也推出了自己的解决方案,因为我喜欢完全控制,而且我经常尝试实现一些非常具体和复杂的事情。如果您在实现自己的转换方面需要更多帮助,请发布一个问题,我会看看是否可以提供帮助:)
  • 对我来说就像一个魅力,让我放弃了所有的 UICollectionViewTransitionLayout 代码。 +1,000。
  • 我下次肯定会尝试这种方式,总是很高兴听到更好的答案。 +1
  • 如果一切都可以像添加一个对layoutIfNeeded的调用一样简单。
猜你喜欢
  • 1970-01-01
  • 2016-08-19
  • 2011-01-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-17
相关资源
最近更新 更多