【问题标题】:Keep uitableview static when inserting rows at the top在顶部插入行时保持 uitableview 静态
【发布时间】:2011-05-15 20:23:51
【问题描述】:

我有一个 tableView,我要在顶部插入行。

在执行此操作时,我希望当前视图保持完全静止,因此只有在您向上滚动时才会显示行。

我尝试保存底层 UIScrollview 的当前位置并在插入行后重置位置,但这会导致上下抖动,尽管它最终会回到同一个位置。

有没有什么好的方法可以做到这一点?

更新:我使用的是 beginUpdate,然后是 insertRowsAtIndexPath,endUpdates。没有 reloadData 调用。

scrollToRowAtIndexPath 跳转到当前单元格的顶部(在添加行之前保存)。

我尝试过的另一种方法,以完全正确的速度结束,但有点颤抖。

save tableView currentOffset. (Underlying scrollView method)
Add rows (beginUpdates,insert...,endUpdates) 
reloadData ( to force a recalulation of the scrollview size )
Recalculate the correct new offset from the bottom of the scrollview
setContentOffset (Underlying scrollview method)

问题是 reloadData 导致 scrollview/tableview 开始短暂滚动,然后 setContentOffset 将其返回到正确的位置。

有没有办法让 tableView 在不开始显示的情况下计算出它的新大小?

将整个内容包装在 beginAnimation commitAnimation 中也无济于事。

更新 2:这显然可以完成 - 当您下拉更新时,请查看官方 twitter 应用程序。

【问题讨论】:

  • 只是想对没有公认的解决方案表示抱歉。我不再有权访问代码来检查任何解决方案。我很清楚的一件事是我的解决方案很糟糕,但这就是最终发布的!我希望他们现在已经改变了。
  • 喂,你以后有解决办法吗?

标签: iphone uitableview uikit uiscrollview


【解决方案1】:

我想添加附加条件。 如果您的代码在 iOS11 或更高版本,您需要执行以下操作;

在 iOS 11 中,表格视图默认使用估计高度。这意味着 contentSize 与最初的估计值一样。如果您需要使用 contentSize,您需要通过将 3 个估计高度属性设置为零来禁用估计高度:

tableView.estimatedRowHeight = 0 tableView.estimatedSectionHeaderHeight = 0 tableView.estimatedSectionFooterHeight = 0

【讨论】:

    【解决方案2】:

    AmitP 答案,Swift 3 版本

    let beforeContentSize = self.tableView.contentSize
    self.tableView.reloadData()
    let afterContentSize = self.tableView.contentSize
    let afterContentOffset = self.tableView.contentOffset
    let newContentOffset = CGPoint(x: afterContentOffset.x, y: afterContentOffset.y + afterContentSize.height - beforeContentSize.height)
                        self.tableView.contentOffset = newContentOffset
    

    【讨论】:

      【解决方案3】:

      晚了,但即使单元格具有动态高度(又名UITableViewAutomaticDimension),这也有效,无需遍历单元格来计算它们的大小,但仅当在 tableView 的最开头添加项目时才有效没有标题,只需一点数学知识就可以使其适应各种情况:

      func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
              if indexPath.row == 0 {
                  self.getMoreMessages()
              }
      }
      
      private func getMoreMessages(){
              var initialOffset = self.tableView.contentOffset.y
              self.tableView.reloadData()
              //@numberOfCellsAdded: number of items added at top of the table 
              self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: numberOfCellsAdded, inSection: 0), atScrollPosition: .Top, animated: false)
              self.tableView.contentOffset.y += initialOffset
      }
      

      【讨论】:

      • Brilliant!,这适用于 UITableViewAutomaticDimensions(接受的答案不适用),在我的情况下,我正在使用动态大小的单元格构建聊天。谢谢,为此苦苦挣扎了一段时间
      【解决方案4】:

      禁用动画的简单解决方案

      func addNewRows(indexPaths: [NSIndexPath]) {
          let addBlock = { () -> Void in
              self.tableView.beginUpdates()
              self.tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .None)
              self.tableView.endUpdates()
          }
      
          tableView.contentOffset.y >= tableView.height() ? UIView.performWithoutAnimation(addBlock) : addBlock()
      }
      

      【讨论】:

        【解决方案5】:

        @Dean,

        您可以像这样更改代码以防止动画。

        [tableView beginUpdates];
        [UIView setAnimationsEnabled:NO];
        
        // ...
        
        [tableView endUpdates];
        
        [tableView setContentOffset:newOffset animated:NO];
        [UIView setAnimationsEnabled:YES];
        

        【讨论】:

        【解决方案6】:

        真的没有必要总结所有的行高, 重新加载表格后的新 contentSize 已经代表了这一点。 所以你所要做的就是计算 contentSize 高度的增量并将其添加到当前偏移量。

            ...
            CGSize beforeContentSize = self.tableView.contentSize;
            [self.tableView reloadData];
            CGSize afterContentSize = self.tableView.contentSize;
        
            CGPoint afterContentOffset = self.tableView.contentOffset;
            CGPoint newContentOffset = CGPointMake(afterContentOffset.x, afterContentOffset.y + afterContentSize.height - beforeContentSize.height);
            self.tableView.contentOffset = newContentOffset;
            ...
        

        【讨论】:

        • 我认为这是最好的答案,但不够笼统。因此,仅当您在当前可见的 contentOffset 之上添加行时,该解决方案才有效。
        • 漂亮的解决方案。一切都是关于优雅的数学。
        • 远距离的最佳答案。请把这个投票给顶!!
        • 这是最好的解决方案:)
        • 不幸的是,它不适用于自动调整大小的表格 - 在这种情况下 contentSize 不可靠,并且在添加单元格后通常会缩小。
        【解决方案7】:

        提醒一下,如果您返回 tableview 的估计高度,似乎不可能做到这一点。

           - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath ;
        

        如果你实现这个方法并返回一个粗略的高度,你的 tableview 将在重新加载时跳跃,因为它似乎在设置偏移量时使用这些高度。

        要使其正常工作,请使用上述答案之一(我选择了@Mayank Yadav 答案),不要实现estimatedHeight 方法并缓存单元格高度(记住在顶部插入其他单元格时调整缓存) .

        【讨论】:

        • 当我调用开始/结束更新时,导致表格视图来回移动,这让我大吃一惊。删除估计的高度消除了问题。
        【解决方案8】:

        你不需要做那么多困难的操作,而且这些操作不会完美地工作。简单的解决方案是旋转表格视图,然后将单元格旋转到其中。

        tableView.transform = CGAffineTransformMakeRotation(M_PI);
        
        -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
        {
        
           cell.transform = CGAffineTransformMakeRotation(M_PI);
        
        }
        

        使用[tableView setScrollIndicatorInsets:UIEdgeInsetsMake(0, 0, 0, 310)] 设置滚动指示器的相对位置。表格视图旋转后,它将位于右侧。

        【讨论】:

          【解决方案9】:

          我遇到的情况是,由于自定义分组,许多部分在 -reloadData 调用之间可能具有不同的行数,并且行高各不相同。所以这里是基于 AndreyZ 的解决方案。它是 UIScrollView 在 -reloadData 前后的 contentHeight 属性,看起来更通用。

          CGFloat contentHeight = self.tableView.contentSize.height;
          CGPoint offset = self.tableView.contentOffset;
          [self.tableView reloadData];
          
          offset.y += (self.tableView.contentSize.height - contentHeight);
          if (offset.y > [self.tableView contentSize].height)
              offset.y = 0;
          
          [self.tableView setContentOffset:offset];
          

          【讨论】:

            【解决方案10】:

            遇到了同样的问题并找到了解决方案。

                save tableView currentOffset. (Underlying scrollView method)
                //Add rows (beginUpdates,insert...,endUpdates) // don't do this!
                reloadData ( to force a recalulation of the scrollview size )
                add newly inserted row heights to contentOffset.y here, using tableView:heightForRowAtIndexPath:
                setContentOffset (Underlying scrollview method)
            

            像这样:

            - (CGFloat) firstRowHeight
            {
                return [self tableView:[self tableView] heightForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
            }
            
            ...
            CGPoint offset = [[self tableView] contentOffset];
            [self tableView] reloadData];
            offset.y += [self firstRowHeight];
            if (offset.y > [[self tableView] contentSize].height) {
                offset.y = 0;
            }
            [[self tableView] setContentOffset:offset];
            ...
            

            完美运行,没有故障。

            【讨论】:

            • 不要忘记在调用 reloadData 之前更新您的数据源。
            • Cocoa 中 NSTableViews 的类似方法也很有效。
            • 有趣,这行得通,但为什么呢? reloadData 通常不会滚动表格吗?
            • 如果我想添加 10 行不同类型和高度的 :(
            • 这项工作但不适用于 UITableViewAutomaticDimension :(
            【解决方案11】:

            每个人都喜欢复制和粘贴代码示例,所以这里是 Andrey Z. 答案的实现。

            这是我的delegateDidFinishUpdating:(MyDataSourceDelegate*)delegate 方法

            if (self.contentOffset.y <= 0)
            {
                [self beginUpdates];
                [self insertRowsAtIndexPaths:insertedIndexPaths withRowAnimation:insertAnimation];
                [self endUpdates];
            }
            else
            {
                CGPoint newContentOffset = self.contentOffset;
                [self reloadData];
            
                for (NSIndexPath *indexPath in insertedIndexPaths)
                    newContentOffset.y += [self.delegate tableView:self heightForRowAtIndexPath:indexPath];
            
                [self setContentOffset:newContentOffset];
            
                NSLog(@"New data at top of table view");
            }
            

            底部的NSLog 可以替换为调用,以显示表明有新数据的视图。

            【讨论】:

              【解决方案12】:

              根据 Andrey Z 的回答,这是一个非常适合我的现场示例...

              int numberOfRowsBeforeUpdate = [controller.tableView numberOfRowsInSection:0];
              CGPoint currentOffset = controller.tableView.contentOffset;
              
              if(numberOfRowsBeforeUpdate>0)
              {
                  [controller.tableView reloadData];
                  int numberOfRowsAfterUpdate = [controller.tableView numberOfRowsInSection:0];
              
                  float rowHeight = [controller getTableViewCellHeight];  //custom method in my controller
                  float offset = (numberOfRowsAfterUpdate-numberOfRowsBeforeUpdate)*rowHeight;
              
                  if(offset>0)
                  {
                      currentOffset.y = currentOffset.y+offset;
                      [controller.tableView setContentOffset:currentOffset];
                  }
              }
              else
                  [controller.tableView reloadData];
              

              【讨论】:

                【解决方案13】:
                -(void) updateTableWithNewRowCount : (int) rowCount
                {    
                    //Save the tableview content offset 
                    CGPoint tableViewOffset = [self.tableView contentOffset];                                                                                                            
                
                    //Turn of animations for the update block 
                    //to get the effect of adding rows on top of TableView
                    [UIView setAnimationsEnabled:NO];
                
                    [self.tableView beginUpdates];                    
                
                    NSMutableArray *rowsInsertIndexPath = [[NSMutableArray alloc] init];        
                
                    int heightForNewRows = 0;
                
                    for (NSInteger i = 0; i < rowCount; i++) {
                
                        NSIndexPath *tempIndexPath = [NSIndexPath indexPathForRow:i inSection:SECTION_TO_INSERT];
                        [rowsInsertIndexPath addObject:tempIndexPath];
                
                        heightForNewRows = heightForNewRows + [self heightForCellAtIndexPath:tempIndexPath];                        
                    }
                
                    [self.tableView insertRowsAtIndexPaths:rowsInsertIndexPath withRowAnimation:UITableViewRowAnimationNone];                                                                            
                
                    tableViewOffset.y += heightForNewRows;                                                                                 
                
                    [self.tableView endUpdates]; 
                
                    [UIView setAnimationsEnabled:YES];
                
                    [self.tableView setContentOffset:tableViewOffset animated:NO];           
                }
                
                
                -(int) heightForCellAtIndexPath: (NSIndexPath *) indexPath
                {
                
                    UITableViewCell *cell =  [self.tableView cellForRowAtIndexPath:indexPath];
                
                    int cellHeight   =  cell.frame.size.height;
                
                    return cellHeight;
                }
                

                只需传入要插入顶部的新行的行数。

                【讨论】:

                • 谢谢你,@Mayank Yadav。您的解决方案是在使用复杂单元格的实际应用中实现问题功能的良好起点。
                • 我已经多次尝试实现这个效果,但无济于事。谢谢 Mayank Yadav,这真的很有帮助!
                【解决方案14】:

                我对核心数据示例项目进行了一些测试,并让它静止不动,同时在顶部可见单元格上方添加新单元格。这段代码需要调整屏幕上的空白表格,但是一旦屏幕被填满,它就可以正常工作了。

                static CGPoint  delayOffset = {0.0};
                
                - (void)controllerWillChangeContent:(NSFetchedResultsController*)controller {
                    if ( animateChanges )
                        [self.tableView beginUpdates];
                    delayOffset = self.tableView.contentOffset; // get the current scroll setting
                }
                

                在单元格插入点添加了此项。删除单元格可以做对应减法。

                    case NSFetchedResultsChangeInsert:
                
                        delayOffset.y += self.tableView.rowHeight;  // add for each new row
                        if ( animateChanges )   
                            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationNone];
                        break;
                

                最后

                - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
                    if ( animateChanges )   
                    {
                        [self.tableView setContentOffset:delayOffset animated:YES];
                        [self.tableView endUpdates];
                    }
                    else
                    {
                        [self.tableView reloadData];
                        [self.tableView setContentOffset:delayOffset animated:NO];
                    }
                }
                

                使用 animateChanges = NO,添加单元格时我看不到任何移动。

                在使用 animateChanges = YES 进行测试时,“颤抖”就在那里。似乎单元格插入的动画与动画表格滚动的速度不同。虽然最后的结果可能会在它们开始的地方以可见的单元格结束,但整个表格似乎移动了 2 或 3 个像素,然后向后移动。

                如果动画速度可以相等,它可能会保持不变。

                但是,当我在上一个动画完成之前按下按钮添加行时,它会突然停止动画并开始下一个动画,从而突然改变位置。

                【讨论】:

                • 这很好用,谢谢。注意:我更改了 delayOffset.y += self.tableView.rowHeight; To: delayOffset.y += [self tableView:self.tableView heightForRowAtIndexPath:newIndexPath];处理可变单元格高度
                • 什么是animateChanges?在 UITable 文档中找不到此属性
                • animateChanges 是我在此示例中的便利变量,用于指示应用程序或用户已选择让插入动画或不动画。它不是任何 iOS API 的一部分。
                【解决方案15】:

                @Dean 使用图像缓存的方式太老套了,我认为它破坏了 UI 的响应能力。

                一种正确的方法: 使用 UITableView 子类并覆盖 -setContentSize:,您可以在其中通过某种方式计算表格视图被下推的程度并通过设置 contentOffset 来抵消它。

                这是一个最简单的示例代码,用于处理所有插入都发生在表格视图顶部的最简单情况:

                @implementation MyTableView
                
                - (void)setContentSize:(CGSize)contentSize {
                        // I don't want move the table view during its initial loading of content.
                    if (!CGSizeEqualToSize(self.contentSize, CGSizeZero)) {
                        if (contentSize.height > self.contentSize.height) {
                            CGPoint offset = self.contentOffset;
                            offset.y += (contentSize.height - self.contentSize.height);
                            self.contentOffset = offset;
                        }
                    }
                    [super setContentSize:contentSize];
                }
                
                @end
                

                【讨论】:

                • 如果您遇到这种情况(就像我有一段时间那样),请确保您的数据源按预期返回所有行。我在底部以一种意想不到的方式改变了高度(当与可变高度行结合使用时)。
                【解决方案16】:

                我最终解决了这个问题,方法是将当前的 tableview 渲染到 UIImage 中,然后在 tableview 动画时将临时 UIImageView 放在 tableview 上。

                下面的代码会生成图片

                
                // Save the current tableView as an UIImage
                CSize pageSize = [[self tableView] frame].size;
                UIGraphicsBeginImageContextWithOptions(pageSize, YES, 0.0); // 0.0 means scale appropriate for device ( retina or no )
                CGContextRef resizedContext = UIGraphicsGetCurrentContext();
                CGPoint offset = [[self tableView] contentOffset];
                CGContextTranslateCTM(resizedContext,-(offset.x),-(offset.y));
                
                [[[self tableView  ]layer] renderInContext:resizedContext];
                UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
                UIGraphicsEndImageContext();    
                

                您需要在插入行时跟踪 tableview 的增长量,并确保将 tableview 滚动回完全相同的位置。

                【讨论】:

                • 评论者是对的。这是哈克。我建议先尝试替代方案。
                【解决方案17】:

                如何将行添加到表中?

                如果您更改数据源然后调用reloadData,这可能会导致表格再次滚动到顶部。

                但是,如果您使用beginUpdatesinsertRowsAtIndexPaths:withRowAnimation:endUpdates 方法,您应该能够插入行而无需调用reloadData,从而将表格保持在其原始位置。

                在调用 endUpdates 之前不要忘记修改您的数据源,否则您最终会遇到内部不一致异常。

                【讨论】:

                • 请看上面,我没有使用beingUpdates等调用reloadData和ma。
                【解决方案18】:

                使用 scrollToRowAtIndexPath:atScrollPosition:animated: 怎么样?您应该能够将一个元素添加到您的数据源,使用上述方法设置行并重新加载表格...

                【讨论】:

                • Dean 说他已经尝试过了,但结果让人颤抖。不理想。
                • 我不是这么理解他的。我认为他正在尝试一种手动方法(至少这是“保存底层 UIScrollview 的当前位置”所暗示的)。
                • scrollToRow at indexPath 不是无缝的,就像您在滚动后重新定位到顶部时正在查看一个大单元格的中间一样。
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2019-03-17
                • 1970-01-01
                • 2016-11-24
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2016-05-30
                相关资源
                最近更新 更多