[译]使用水平和垂直滑动的UICollectionView来制作粘性的行和列

一个tableView可以被水平和垂直滑动吗?

免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!

应用程序日常开发工作的现实之一就是克服日常的挑战.原文

远非是我们要抱怨它。我们喜欢每一个新的障碍并且爱解决各种各样的困惑和难题为我们的客户服务。

我们最近的挑战就是用一个tableView来实现水平和垂直的滑动。条件是:当水平滑动时第一列和垂直滑动时的第一行要保持静止.

最后,我们需要给这个tableView包含一个巨大的数据,因此它重要性就是能够平稳的滑动。


挑战

首先,我们简单说明下挑战--有时在开发的第一步就是要明确实际的问题是什么:

  • 创建一个水平和垂直滑动的tableView.
  • 水平滑动第一列和垂直滑动第一行的粘性.
  • 使用大数据的情况下,必须要保持平滑的滚动.

第一次尝试-UITableView

我们开始考虑在UIScrollView中包含一个UITableView,但经过一些尝试,我们意识到:定制它的滚动和粘性是一个棘手的任务。

使用UITableView来做出必要的定制工作,真的很痛苦.因此,我们决定使用可定制的UICollectionView.

第二次尝试-UICollectionView

UICollectionView提供了强大的定制化服务,允许你设置一个自定义的UICollectionFlowLayout.不幸的是,网格布局的滚动只能沿着一个轴滑动:要么是水平要么是垂直.

我们希望我们能嵌入一个UIScrollView的集合,允许我们能够两个方向都能滑动,因此多方考虑我们决定使用UICollectionFlowLayout的父类UICollectionViewLayout的子类。

UICollectionViewLayout子类

以下是创建collectionView 的步骤:

  1. 创建一个UICollectionViewLayout的子类CustomCollectionViewLayout.
  2. storyboard中拖动一个UICollectionView.我们也可以使用代码创建,这取决于你自己.
  3. storyboard中的Attributes Inspector设置一些属性.我们设置LayoutCustom,设置ClassCustomCollectionViewLayout.Scrolling Enabled为选中状态,如果我们想要每次只滑动一个方向,选中Direction Lock Enabled.否则,会同一时刻滑动两个方向.
  1. 最后,将UICollectionView连接到UIViewController.不要忘记连接dataSourcedelegate.
  2. 在我们的CollectionView中,这个视图表示row列所在的row,因此记住sections的索引下标和item的索引下标.这取决我们设置collectionView flow layout的方向.
  1. 一旦完成了设置collectionView的方式,我们就需要设置我们自己所要创建的custom collection view layout.首先,我们要设置3个管理所有数据的属性.

    ```
    @property (nonatomic,strong) NSMutableArray *itemAttributes;
    @property (nonatomic,strong) NSMutableArray *itemsSize;
    @property (nonatomic, assign) CGSize contentSize;
    ```
    
  2. 如果你要自定义item的大小,你需要定义以下方法:

     - (CGSize)sizeForItemWithColumnIndex:(NSUInteger)columnIndex;
     ```
    我强烈推荐你计算item大小存储到`itemsSize`中,是为了每一个列只计算一次.提高性能.
    
  3. 写如下方法并返回YES,是为了调用当你每次滑动CollectionView的时候都能够调用prepareLayout方法.你必须设置这个。

     ```
    -(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
    {  
    return YES; 
    }
     ```
    
  4. 你需要调用以下方法布局显示的items

        /**
         *  CGRectOffset: 返回一个原点在源矩形基础上进行了偏移的矩  形。
         *  CGRectInset: 返回一个与源矩形共中心点的,或大些或小些的新 矩形。
         *  CGRectIntegral: 返回包围源矩形的最小整数矩形。
         *  const CGRect CGRectZero: 一个原点在(0, 0),且长宽均为  0 的常数矩形。这个零矩形与 CGRectMake(0.0f, 0.0f, 0.0f, 0.0f) 是等    价的。
         *  const CGRect CGRectNull: 空矩形。这个会在,比如说,求两    个不相交的矩形的相交部分时返回。注意,空矩形不是零矩形。
         *  const CGRect CGRectInfinite: 无穷大矩形。
         *  CGRectDivide: 将源矩形分为两个子矩形。
         */
      - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
              {
             NSMutableArray *attributes = [@[] mutableCopy];
              for (NSArray *section in self.itemAttributes) {
                     [attributes addObjectsFromArray:[section filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(UICollectionViewLayoutAttributes *evaluatedObject, NSDictionary *bindings) {
                 return CGRectIntersectsRect(rect, [evaluatedObject frame]);
              }]]];
            }
                  return attributes;
            }
    
  5. 你需要根据indexPath返回layout attributes,如果才能返回出正确的item attribute.

    - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
    {
        return      self.itemAttributes[indexPath.section]  [indexPath.row];
    }   
    
  6. 你还需要返回collectionView内容视图的滑动大小.

         - (CGSize)collectionViewContentSize
         {
                 return self.contentSize;
         }
    
  7. 最后,需要设prepareLayout方法.我们重画collectionView这将要被每次调用.

     ```
     - (void)prepareLayout;
     ```
    
  8. 最后,我们要确定要展示的数据,否则返回void

    if ([self.collectionView numberOfSections] == 0) 
    {
           return;
    }
    
  9. 现在必须检查我们是否已经计算了item的attribute.在确定的情况下,我们将要使用这个attributes来粘性头部(第15步).否则,我们要每一次都计算,并且保存他们到itemAttributes数组中.

    基本上,我们为了保证它们可见,是通过遍历所有的items然后计算itemsizeframezIndex.

    最后,我们使用last item来计算contentSize为了计算出content的全部高度.

        // 以下代码只在prepareLayout中执行一次
         self.itemAttributes = [@[] mutableCopy];
         self.itemsSize = [@[] mutableCopy];
      
        // Tip:我们可以使用NSUInteger来代替numberOfItemsInSection
        // NSUInteger numberOfItems = [self.collectionView numberOfItemsInSection:section];
      
        //计算每一列中item 的大小
        if (self.itemsSize.count != NUMBEROFCOLUMNS) {
          [self calculateItemsSize];
        }
        // 通过循环计算items
          for (int section = 0; section < [self.collectionView numberOfSections]; section++) {
          NSMutableArray *sectionAttributes = [@[] mutableCopy];
          for (NSUInteger index = 0; index < NUMBEROFCOLUMNS; index++) {
              CGSize itemSize = [self.itemsSize[index] CGSizeValue];
              // 每一次都创建一个UICollectionViewLayoutAttributes,并且保存到数组中
              NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:section];
              UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
              attributes.frame = CGRectIntegral(CGRectMake(xOffset, yOffset, itemSize.width, itemSize.height));
                  if (section == 0 && index == 0) {
                  attributes.zIndex = 1024; //设置(Sec0Row0)的第一项,使其能在第一列和第一行之上
              } else if (section == 0 || index == 0) {
                  attributes.zIndex = 1023; //设置第一列和第一行在其余为设置的item之上
              }
              if (section == 0) {
                  CGRect frame = attributes.frame;
                  frame.origin.y = self.collectionView.contentOffset.y;
                  attributes.frame = frame; // 顶部(第一行)黏性
              }
              if (index == 0) {
                  CGRect frame = attributes.frame;
                  frame.origin.x = self.collectionView.contentOffset.x;
                  attributes.frame = frame; // 左部(第一列)黏性
              }
              [sectionAttributes addObject:attributes];
              xOffset = xOffset+itemSize.width;
              column++;
              // 如果最后的列,定义最终的宽度
              if (column == NUMBEROFCOLUMNS) {
                  if (xOffset > contentWidth) {
                      contentWidth = xOffset;
                  }  
                  // Reset values
                  column = 0;
                  xOffset = 0;
                  yOffset += itemSize.height;
              }
          }
          [self.itemAttributes addObject:sectionAttributes];
        }
          //获取最后的一个item 来计算 content的全部高度
        UICollectionViewLayoutAttributes *attributes = [[self.itemAttributes lastObject] lastObject];
        contentHeight = attributes.frame.origin.y+attributes.frame.size.height;
        self.contentSize = CGSizeMake(contentWidth, contentHeight);
    
  10. 事实上我们已经在之前计算过items attributes,我们只需要(垂直滑动的时候)通过设置first rowy-offset和(水平滑动)设置first columnsx-offset即可.

    //如果不进入以下if语句,就会执行计算方法
    if (self.itemAttributes.count > 0) {
         for (int section = 0; section < [self.collectionView numberOfSections]; section++) {
            NSUInteger numberOfItems = [self.collectionView numberOfItemsInSection:section];
            for (NSUInteger index = 0; index < numberOfItems; index++) {
                 if (section != 0 && index != 0)      {         //这是我们不需要黏性的内容
                  continue;
               }
            UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:section]];
            if (section == 0) { // 顶部(第一行)黏性
                CGRect frame = attributes.frame;
                frame.origin.y = self.collectionView.contentOffset.y;
                attributes.frame = frame;
            }
            if (index == 0) { // 左部(第一列)黏性
                CGRect frame = attributes.frame;
                frame.origin.x = self.collectionView.contentOffset.x;
                attributes.frame = frame;
            }
        }
    }
    return;
    }
    

如若喜欢请点赞或关注,不定期的发布文章或译文.

源代码下载

ps: 转载注明出处

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容