在顶部插入行时保持uitableview静态

时间:2010-11-25 17:52:28

标签: iphone uitableview uikit uiscrollview

我有一个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应用程序。

18 个答案:

答案 0 :(得分:48)

真的没有必要总结所有行高度, 重新加载表后的新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;
    ...

答案 1 :(得分:24)

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

只需将新行的行数传入顶部即可插入。

答案 2 :(得分:21)

@Dian使用图像缓存的方式过于苛刻,我认为这会破坏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

答案 3 :(得分:17)

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

    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];
...

完美无缺,无故障。

答案 4 :(得分:9)

我使用核心数据样本项目进行了一些测试,并让它静止不动,同时在顶部可见细胞上方添加新细胞。此代码需要调整屏幕上空白空间的表格,但一旦屏幕填满,它就可以正常工作。

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个像素,然后向后移动。

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

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

答案 5 :(得分:5)

@Dean,

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

[tableView beginUpdates];
[UIView setAnimationsEnabled:NO];

// ...

[tableView endUpdates];

[tableView setContentOffset:newOffset animated:NO];
[UIView setAnimationsEnabled:YES];

答案 6 :(得分:4)

每个人都喜欢复制和粘贴代码示例,所以这里是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可以替换为调用,以显示指示有新数据的视图。

答案 7 :(得分:3)

我遇到的情况是,由于自定义分组,有很多部分可能在-reloadData调用之间有不同的行数,行高会有所不同。所以这是基于AndreyZ的解决方案。它在-reloadData之前和之后的UIScrollView的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];

答案 8 :(得分:2)

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

  

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

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

答案 9 :(得分:1)

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

tableView.transform = CGAffineTransformMakeRotation(M_PI);

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{

   cell.transform = CGAffineTransformMakeRotation(M_PI);

}

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

答案 10 :(得分:1)

我最终解决了这个问题,将当前的tableview渲染为UIImage,然后在动画时将临时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滚动回到完全相同的位置。

答案 11 :(得分:1)

如果您为tableview返回估计的高度,那么只是抬头似乎不可能这样做。

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

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

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

答案 12 :(得分:1)

如何将行添加到表中?

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

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

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

答案 13 :(得分:1)

禁用动画的简单解决方案

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()
}

答案 14 :(得分:0)

根据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];

答案 15 :(得分:0)

派对晚了,但即使单元格具有动态高度(又名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
}

答案 16 :(得分:0)

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

答案 17 :(得分:-1)

如何使用scrollToRowAtIndexPath:atScrollPosition:animated:?您应该只能向数据源添加元素,使用上述方法设置行并重新加载表格...