【问题标题】:Using Invalidation Contexts for UICollectionViewLayout对 UICollectionViewLayout 使用无效上下文
【发布时间】:2013-11-20 15:57:01
【问题描述】:

所以我通过从shouldInvalidateLayoutForBoundsChange: 返回YES 部分地在我的UICollectionView 中实现了工作粘性标题。但是,这会影响性能,我不想让整个布局失效,只想让我的标题部分失效。

现在,根据官方文档,我可以使用UICollectionViewLayoutInvalidationContext 为我的布局定义自定义失效上下文,但是文档非常缺乏。它要求我“定义代表可以独立重新计算的布局数据部分的自定义属性”,但我不明白它们的意思。

有没有人有任何继承UICollectionViewLayoutInvalidationContext的经验?

【问题讨论】:

  • 我会在接下来的几天里研究这个。如果我得到什么,我会添加一个答案。同意文档有点轻!我找不到的是:您需要覆盖UICollectionViewLayoutinvalidationContextForBoundsChange: 以提供您的自定义UICollectionViewLayoutInvalidationContext 的实例,上面写着“仅使页眉/页脚无效”。您还需要覆盖invalidateLayoutWithContext:,以便它只会在上下文所说的情况下使页眉和页脚无效。 但是我不确定如何实际将特定的UICollectionViewCells 标记为无效。
  • 对! UICollectionViewCellUICollectionReusableView 子类。这意味着它实现了applyLayoutAttributes:——正是我们所需要的。 UICollectionView 的文档提到 performBatchUpdates:completion: 可用于“……更改与一个或多个单元格关联的布局参数……”。我怀疑你的UICollectionViewLayout 应该覆盖invalidateLayoutWithContext:,检测边界变化的情况(见前面的评论),在这种情况下,只更新可见的页眉和页脚单元格。或者我可能完全错了:-)
  • 我覆盖了我的UICollectionViewCell 子类的applyLayoutAttributes: 来调用[super …] 并记录调用。它肯定会在更新期间被使用,所以它似乎有理由直接在performBatchUpdates:completion: 中调用。无论如何绝对值得一试。
  • 嗨@mattsson——你愿意把“正确答案”从我身边转移到meelawsh吗?他们为 iOS 8 及以后的版本提供了更好的答案,这可能对来这里的人更有用。谢谢!
  • 冬青牛!我为此失去了 15 个代表。 :-) 我喜欢因为做正确的事而受到惩罚。

标签: ios cocoa-touch uicollectionview uicollectionviewlayout


【解决方案1】:

这是为 iOS8 设计的

我做了一些试验,我想我找到了使用失效布局的简洁方法,至少在 Apple 稍微扩展一下文档之前。

我试图解决的问题是在集合视图中获得粘性标题。我使用 FlowLayout 的子类和覆盖 layoutAttributesForElementsInRect: 的工作代码:(你可以在谷歌上找到工作示例)。这要求我始终从 shouldInvalidateLayoutForBoundsChange: 返回 true:这是苹果希望我们通过上下文失效避免的主要性能提升。

干净的上下文失效

您只需要子类化 UICollectionViewFlowLayout。我不需要 UICollectionViewLayoutInvalidationContext 的子类,但这可能是一个非常简单的用例。

随着集合视图的滚动,流布局将开始接收 shouldInvalidateLayoutForBoundsChange: 调用。由于流布局已经可以处理这个问题,我们将在函数末尾返回超类的答案。通过简单的滚动,这将是错误的,并且不会重新布局元素。但是我们需要重新布局标题并让它们保持在屏幕顶部,因此我们将告诉集合视图仅使我们将提供的上下文无效:

override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
    invalidateLayoutWithContext(invalidationContextForBoundsChange(newBounds))
    return super.shouldInvalidateLayoutForBoundsChange(newBounds)
}

这意味着我们也需要重写 invalidationContextForBoundsChange: 函数。由于这个函数的内部工作是未知的,我们只需向超类询问失效上下文对象,确定我们想要使哪些集合视图元素失效,并将这些元素添加到失效上下文中。我把一些代码拿出来专注于这里的要点:

override func invalidationContextForBoundsChange(newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext! {

    var context = super.invalidationContextForBoundsChange(newBounds)

    if /... we find a header in newBounds that needs to be invalidated .../ {

            context.invalidateSupplementaryElementsOfKind(UICollectionElementKindSectionHeader, atIndexPaths:[NSIndexPath(forItem: 0, inSection:headerIndexPath.section)] )
    }
    return context
}

就是这样。标头和标头都无效。流布局将只接收一次对 layoutAttributesForSupplementaryViewOfKind: 的调用,其中 indexPath 在失效上下文中。如果您需要使单元格或装饰器失效,UICollectionViewLayoutInvalidationContext 上还有其他 invalidate* 函数。

最难的部分实际上是在 invalidationContextForBoundsChange: 函数中确定标头的 indexPaths。我的页眉和单元格都是动态调整大小的,仅查看边界 CGRect 就需要一些杂技才能使其工作,因为最明显有用的函数 indexPathForItemAtPoint: 如果该点位于页眉、页脚、装饰器或行间距。

至于性能,我没有进行全面测量,但是在滚动时快速浏览一下 Time Profiler 表明它正在做正确的事情(右侧较小的尖峰是在滚动时)。

【讨论】:

  • 在 iOS 8 上就像一个魅力。在 iOS 7 上,我不得不切换到 @Fabien Warniez 的解决方案。
  • 嘿,您介意分享一些有关此解决方案的粘性标题的其他详细信息吗?即:您使用的是 FlowLayout 吗?如果是这样,您如何使补充索引无效?您是否使用UICollectionElementKindSectionHeader 作为补充视图?提前谢谢你
  • 这个有点老了,但是不需要继承invalidationContextForBoundsChange方法,你可以很容易的在shouldInvalidateLayoutForBoundsChange中设置失效的补充视图。并且没有必要返回 shouldInvalidateLayoutForBoundsChange 的超级实现,因为文档清楚地说它只是返回 NO。
  • @GeorgeBrown 实际上我认为原始答案可能存在错误。您根本不需要手动调用invalidationContext(forBoundsChange:)。只需从shouldInvalidateLayout(forBoundsChange:) 返回“是”即可自动访问它。
  • 实际上,由于没有使用自定义失效上下文,我怀疑invalidateEverything 是真的。根据文档,这仍然会导致每个项目都被刷新。
【解决方案2】:

我今天只是问了同样的问题,也对这部分感到困惑: “定义自定义属性,代表可以独立重新计算的布局数据部分”

我所做的是子类UICollectionViewLayoutInvalidationContext(我们称之为CustomInvalidationContext)并添加了我自己的属性。 出于测试目的,我想找出可以从上下文中配置和检索这些属性的位置,因此我只是添加了一个数组作为名为“attributes”的属性。

然后在我的子类UICollectionViewLayout 中覆盖+invalidationContextClass 以返回CustomInvalidationContext 的实例,该实例在我覆盖的另一个方法中返回,即: -invalidationContextForBoundsChange。在此方法中,您需要调用 super 返回 CustomInvalidationContext 的实例,然后配置属性并返回。我将属性数组设置为具有对象@["a","b","c"];

这随后在另一个被覆盖的方法中检索 -invalidateLayoutWithContext:。我能够从传入的上下文中检索我设置的属性。

因此,您可以设置属性,稍后您可以计算要提供给-layoutAttributesForElementsInRect: 的 indexPaths。

希望对你有帮助。

【讨论】:

    【解决方案3】:

    自 iOS 8 起

    这个答案是在 iOS 8 种子之前写的。它提到了希望在 iOS 7 中不完全存在的功能,并提供了解决方法。此功能现在确实存在并且有效。另一个答案,目前在页面下方,描述了an approach for iOS 8


    讨论

    首先 - 对于任何类型的优化,真正重要的谨慎是首先分析并了解您的性能瓶颈到底在哪里。

    我查看了UICollectionViewLayoutInvalidationContext 并同意它似乎可以提供所需的功能。在关于这个问题的 cmets 中,我描述了我试图让它发挥作用的尝试。我现在怀疑虽然它允许您删除布局重新计算,但它不会帮助您避免对内容单元格进行布局更改。就我而言,布局计算并不是特别昂贵,但我确实想避免框架将布局更改应用于简单的滚动单元格(其中我有很多),而只将它们应用于“特殊”单元格。

    实施总结

    鉴于没有按照 Apple 的意图去做,我作弊了。我使用 2 UICollectionView 实例。我在背景视图上有正常的滚动内容,在第二个前景视图上有标题。视图的布局指定背景视图不会因边界更改而失效,而前景视图会。

    实现细节

    为了使这项工作正常进行,您需要做一些不明显的事情,而且我还有一些实施技巧,我发现这些技巧让我的生活更轻松。我将通过这个并提供从我的应用程序中提取的代码片段。我不会在这里提供完整的解决方案,但我会提供您需要的所有部分。

    UICollectionView 有一个backgroundView 属性。

    我在UICollectionViewControllerviewDidLoad 方法中创建了背景视图。至此,视图控制器的collectionView 属性中已经有一个UICollectionView 实例。这将是前景视图,将用于具有特殊滚动行为的项目,例如固定。

    我创建了第二个UICollectionView 实例并将其设置为前台集合视图的backgroundView 属性。我将背景设置为也使用UICollectionViewController 子类作为它的数据源和委托。我禁用了背景视图上的用户交互,因为否则它似乎会获取所有事件。如果您想要选择等,您可能需要比这更微妙的行为:

    …
    UICollectionView *const foregroundCollectionView = [self collectionView];
    UICollectionView *const backgroundCollectionView = [[UICollectionView alloc] initWithFrame: [foregroundCollectionView frame] collectionViewLayout: [[STGridLayout alloc] init]];
    [backgroundCollectionView setDataSource: self];
    [backgroundCollectionView setDelegate: self];
    [backgroundCollectionView setUserInteractionEnabled: NO];
    [foregroundCollectionView setBackgroundView: backgroundCollectionView];
    [(STGridLayout*)[backgroundCollectionView collectionViewLayout] setInvalidateLayoutForBoundsChange: NO];
    [(STGridLayout*)[foregroundCollectionView collectionViewLayout] setInvalidateLayoutForBoundsChange: YES];
    …
    

    总而言之——在这一点上,我们有两个相互叠加的集合视图。后面的将用于静态内容。前面的将用于固定内容等。它们都指向同一个 UICollectionViewController 作为它们的委托和数据源。

    STGridLayout 上的invalidateLayoutForBoundsChange 属性是我添加到自定义布局中的。当调用-(BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds 时,布局只会返回它。

    这两个视图的更多设置在我的例子中看起来像这样:

    for(UICollectionView *collectionView in @[foregroundCollectionView, backgroundCollectionView])
    {
      // Configure reusable views.
      [STCollectionViewStaticCVCell registerForReuseInView: collectionView];
      [STBlockRenderedCollectionViewCell registerForReuseInView: collectionView];
    }
    

    registerForReuseInView: 方法是按类别添加到UICollectionReusableView 的东西,以及dequeueFromView:。这些代码在答案的末尾。

    viewDidLoad 的下一篇文章是这种方法唯一令人头疼的问题。

    当您拖动前景视图时,您需要背景视图随之滚动。稍后我将展示此代码:它只是将前景视图的contentOffset 镜像到背景视图。但是,您可能希望滚动视图能够在内容的边缘“反弹”。 UICollectionView 似乎会在以编程方式设置时clamp contentOffset,这样内容就不会脱离UICollectionView 的边界。如果不采取补救措施,只有前景的粘性元素会反弹,这看起来很可怕。但是,将以下内容添加到您的 viewDidLoad 将解决此问题:

    CGSize size = [foregroundCollectionView bounds].size;
    [backgroundCollectionView setContentInset: UIEdgeInsetsMake(size.width, size.height, size.width, size.height)];
    

    不幸的是,此修复将意味着当您在屏幕上显示视图时,背景的内容偏移量将与前景不匹配。要解决此问题,您需要实现:

    -(void) viewDidAppear:(BOOL)animated
    {
      [super viewDidAppear: animated];
      UICollectionView *const foregroundCollectionView = [self collectionView];
      UICollectionView *const backgroundCollectionView = (UICollectionView *)[foregroundCollectionView backgroundView];
      [backgroundCollectionView setContentOffset: [foregroundCollectionView contentOffset]];
    }
    

    我确信在 viewDidAppear: 中执行此操作会更有意义,但这对我不起作用。

    您需要做的最后一件重要的事情是让背景滚动与前景保持同步,如下所示:

    -(void) scrollViewDidScroll:(UIScrollView *const)scrollView
    {
      UICollectionView *const collectionView = [self collectionView];
      if(scrollView == collectionView)
      {
        const CGPoint contentOffset = [collectionView contentOffset];
        UIScrollView *const backgroundView = (UIScrollView*)[collectionView backgroundView];
        [backgroundView setContentOffset: contentOffset];
      }
    }
    

    实施技巧

    这些是帮助我实现UICollectionViewController 的数据源方法的一些建议。

    首先,我为分层的每种不同类型的视图使用了一个部分。这对我来说效果很好。我没有使用UICollectionView 的补充或装饰视图。我在视图控制器开头的枚举中为每个部分命名,如下所示:

    enum STSectionNumbers
    {
      number_the_first_section_0_even_if_they_are_moved_during_editing = -1,
    
      // Section names. Order implies z with earlier sections drawn behind latter sections.
      STBackgroundCellsSection,
      STDataCellSection,
      STDayHeaderSection,
      STColumnHeaderSection,
    
      // The number of sections.
      STSectionCount,
    };
    

    在我的UICollectionViewLayout 子类中,当要求布局属性时,我设置了 z 属性以满足如下顺序:

    -(UICollectionViewLayoutAttributes*) layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
    {
      UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath: indexPath];
      const CGRect frame = …
      [attributes setFrame: frame];
      [attributes setZIndex: [indexPath section]];
      return attributes;
    }
    

    对我来说,如果我将 bothUICollectionView 实例 all 部分都提供给我,那么数据源逻辑会简单得多,但是通过将部分留空来控制哪个视图真正获取它们。

    这是一个方便的方法,我可以用它来检查给定的UICollectionView真的是否有特定的节号:

    -(BOOL) collectionView:(UICollectionView *const)collectionView hasSection:(const NSUInteger)section
    {
      const BOOL isForegroundView = collectionView == [self collectionView];
      const BOOL isBackgroundView = !isForegroundView;
    
      switch (section)
      {
        case STBackgroundCellsSection:
        case STDataCellSection:
        {
          return isBackgroundView;
        }
    
        case STColumnHeaderSection:
        case STDayHeaderSection:
        {
          return isForegroundView;
        }
    
        default:
        {
          return NO;
        }
      }
    }
    

    有了这个,编写数据源方法真的很容易。正如我所说,两个视图的节数相同,所以:

    -(NSInteger) numberOfSectionsInCollectionView:(UICollectionView *)collectionView
    {
      return STSectionCount;
    }
    

    但是,它们在部分中的单元格计数不同,但这很容易适应

    -(NSInteger) collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
    {
      if(![self collectionView: collectionView hasSection: section])
      {
        return 0;
      }
    
      switch(section)
      {
        case STDataCellSection:
        {
          return … // (actual logic not shown)
        }
    
        case STBackgroundCellsSection:
        {
          return …
        }
    
        … // similarly for other sections.
    
        default:
        {
          return 0;
        }
      }
    }
    

    我的UICollectionViewLayout 子类也有一些依赖于视图的方法,它委托给 UICollectionViewController 子类,但使用上面的模式可以轻松处理这些方法:

    -(NSArray*) collectionViewRowRanges:(UICollectionView *)collectionView inSection:(NSInteger)section
    {
      if(![self collectionView: collectionView hasSection: section])
      {
        return [NSArray array];
      }
    
      switch(section)
      {
        case STDataCellSection:
        {
          return … // (actual logic omitted)
          }
        }
    
        case STBackgroundCellsSection:
        {
          return …
        }
    
        … // etc for other sections
    
        default:
        {
          return [NSArray array];
        }
      }
    }
    

    作为健全性检查,我确保集合视图只要求它们应该显示的部分中的单元格:

    -(UICollectionViewCell*) collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
    {
      assert([self collectionView: collectionView hasSection: [indexPath section]] && "Check views are only asking for the sections they own.");
    
      switch([indexPath section])
      {
        case STBackgroundCellsSection:
        … // You get the idea.
    

    最后,值得注意的是,如another SA answer 所示,粘性部分的数学比我想象的要简单,前提是您将所有内容(包括设备的屏幕)都视为在集合视图的内容空间中.

    UICollectionReusableView 重用类别的代码

    @interface UICollectionReusableView (Reuse)
    +(void) registerForReuseInView: (UICollectionView*) view;
    +(id) dequeueFromView: (UICollectionView*) view withIndexPath: (NSIndexPath *) indexPath;
    @end
    

    它的实现是:

    @implementation UICollectionReusableView (Reuse)
    +(void) registerForReuseInView: (UICollectionView*) view
    {
      [view registerClass: self forCellWithReuseIdentifier: NSStringFromClass(self)];
    }
    
    +(instancetype) dequeueFromView: (UICollectionView*) view withIndexPath: (NSIndexPath *) indexPath
    {
      return [view dequeueReusableCellWithReuseIdentifier:NSStringFromClass(self) forIndexPath: indexPath];
    }
    @end
    

    【讨论】:

      【解决方案4】:

      我通过设置一个标志告诉我为什么调用 prepareLayout,然后只重新计算粘性单元格的位置,从而完全实现了您想要做的事情。

      - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
      {
          _invalidatedBecauseOfBoundsChange = YES;
          return YES;
      }
      

      然后在prepareLayout 我愿意:

      if (!_invalidatedBecauseOfBoundsChange)
      {
          [self calculateStickyCellsPositions];
      }
      _invalidateBecauseOfBoundsChange = NO;
      

      【讨论】:

      • 感谢您的替代建议。
      • 伟大的 iOS 7 替代 @meelawsh 的 iOS 8 及更高版本的解决方案。
      【解决方案5】:

      我正在开发自定义 UICollectionViewLayout 子类。我尝试使用UICollectionViewLayoutInvalidationContext。当我更新不需要整个 UICollectionViewLayout 来重新计算其所有属性的布局/视图时,我在invalidateLayoutWithContext:prepareLayout 中使用我的UICollectionViewLayoutInvalidationContext 子类,我只重新计算在我的UICollectionViewLayoutInvalidationContext 中指定的属性子类属性,而不是重新计算所有属性。

      【讨论】:

        猜你喜欢
        • 2022-12-17
        • 2019-04-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-10-30
        • 2012-09-27
        相关资源
        最近更新 更多