【问题标题】:How to detect scrolling to a new section in a UICollectionView?如何检测滚动到 UICollectionView 中的新部分?
【发布时间】:2014-08-05 03:47:14
【问题描述】:

我正在实现一个无限滚动的日历。我的问题是我想将当前月份设置为导航栏中的标题,并且它应该在滚动时更新 - 一旦您通过部分标题视图,标题应该在导航栏中更新。

一个可能的解决方案是在名为- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath 的方法中设置视图标题,这样,当我计算一个新的节标题时,它也会更新标题。这样做的问题是,当新部分位于页面底部时,标题会发生变化。

一旦用户滚动到UICollectionView 的“当前部分”,有没有办法知道它?或者你能想出一种方法来改进我目前的解决方案吗?

为了帮助这篇文章的读者,我在GitHub repo 上发布了我自己针对这个问题的示例代码。

【问题讨论】:

  • 您是否找到了获得所需行为的解决方案?
  • 你好@Joey 检查我的答案。

标签: ios objective-c uicollectionview nsindexpath uicollectionreusableview


【解决方案1】:

这里没有解决方案是令人满意的,所以我想出了我自己想要完全分享的解决方案。但是,如果使用它,则必须进行一些像素调整。

extension MyViewControllerVC: UIScrollViewDelegate {

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView == self.myCollectionView {
            let rect = CGRect(origin: self.myCollectionView.contentOffset, size: self.cvProductItems.bounds.size)
            let cellOffsetX: CGFloat = 35 // adjust this
            let cellOffsetAheadY: CGFloat = 45 // adjust this
            let cellOffsetBehindY: CGFloat = 30 // adjust this
            var point: CGPoint = CGPoint(x: rect.minX + cellOffsetX, y: rect.minY + cellOffsetAheadY) // position of cell that is ahead

            var indexPath = self.myCollectionView.indexPathForItem(at: point)
            if indexPath?.section != nil { // reached next section
                // do something with your section (indexPath!.section)
            } else {
                point = CGPoint(x: rect.minX + cellOffsetX, y: rect.minY - cellOffsetBehindY) // position of cell that is behind
                indexPath = self.myCollectionView.indexPathForItem(at: point)
                if indexPath?.section != nil { // reached previous section
                        // do something with your section (indexPath!.section)
                }
            }
        }
    }

}

UICollectionView 继承了UIScrollView,所以我们可以在viewDidLoad() 中做self.myCollectionView.delegate = self 并为其实现UIScrollViewDelegate

scrollViewDidScroll回调中,我们将首先获取下面一个单元格的点,适当调整cellOffsetXcellOffsetAheadY,这样当单元格到达该点时,您的部分将被选中。您还可以修改 CGPoint 以获得与可见矩形不同的点,即对于 x,您还可以使用 rect.midX / rect.maxX 和任何自定义偏移量。

当您点击具有这些坐标的单元格时,将从indexPathForItem(at: GCPoint) 返回一个 indexPath。

当您向上滚动时,您可能想要向前看,可能在您的UICollectionReusableView 页眉和页脚之前,为此我还检查了在cellOffsetBehindY 中设置了负Y 偏移的点。这具有较低的优先级。

因此,一旦您传递了标题,此示例将获取下一部分,一旦上一部分的单元格即将进入视图,则将获取上一部分。您必须对其进行调整以满足您的需求,并且您应该将值存储在某处,并且仅在当前部分更改时执行您的操作,因为 此回调将在滚动时在每一帧上调用

【讨论】:

    【解决方案2】:

    我一直在思考一种算法,该算法可以让您知道用户何时滚动过节标题以更新标题,并且经过一些实验后,我已经弄清楚了如何实现所需的行为。

    基本上,每次滚动位置发生变化时,您都需要知道用户所在的部分并更新标题。您可以通过UIScrollViewDelegate 上的scrollViewDidScroll 执行此操作——记住集合视图是滚动视图。遍历所有标题并找到最接近当前滚动位置的标题,而没有负偏移量。为此,我使用了一个属性来存储每个节标题位置的数组。创建标头时,我将其位置存储在数组中适当的索引处。找到最接近滚动位置(或所述标题的索引位置)的标题后,只需使用适当的标题更新导航栏中的标题。

    viewDidLoad 中,为您拥有的每个部分用NSNull 填充数组属性:

    self.sectionHeaderPositions = [[NSMutableArray alloc] init];
    for (int x = 0; x < self.sectionTitles.count; x++) {
        [self.sectionHeaderPositions addObject:[NSNull null]];
    }
    

    collectionView:viewForSupplementaryElementOfKind:atIndexPath:中,用创建的头部视图的位置更新数组:

    NSNumber *position = [NSNumber numberWithFloat:headerView.frame.origin.y + headerView.frame.size.height];
    [self.sectionHeaderPositions replaceObjectAtIndex:indexPath.section withObject:position];
    

    scrollViewDidScroll: 中,执行计算以确定哪个标题适合显示该滚动位置:

    CGFloat currentScrollPosition = self.collectionView.contentOffset.y + self.collectionView.contentInset.top;
    CGFloat smallestPositiveHeaderDifference = CGFLOAT_MAX;
    int indexOfClosestHeader = NSNotFound;
    
    //find the closest header to current scroll position (excluding headers that haven't been reached yet)
    int index = 0;
    for (NSNumber *position in self.sectionHeaderPositions) {
        if (![position isEqual:[NSNull null]]) {
            CGFloat floatPosition = position.floatValue;
            CGFloat differenceBetweenScrollPositionAndHeaderPosition = currentScrollPosition - floatPosition;
            if (differenceBetweenScrollPositionAndHeaderPosition >= 0 && differenceBetweenScrollPositionAndHeaderPosition <= smallestPositiveHeaderDifference) {
                smallestPositiveHeaderDifference = differenceBetweenScrollPositionAndHeaderPosition;
                indexOfClosestHeader = index;
            }
        }
        index++;
    }
    if (indexOfClosestHeader != NSNotFound) {
        self.currentTitle.text = self.sectionTitles[indexOfClosestHeader];
    } else {
        self.currentTitle.text = self.sectionTitles[0];
    }
    

    一旦用户滚动到某个部分的标题,这将正确更新导航栏中的标题。如果他们向上滚动,它也会正确更新。当他们没有滚动到第一部分时,它也会正确设置标题。然而,它不能很好地处理旋转。如果您有动态内容,它也不会很好地工作,这可能会导致标题视图的存储位置不正确。如果您支持跳转到特定部分,则用户会跳转到上一个部分的部分标题尚未创建的部分,并且该部分不够高,以至于部分标题位于导航栏下方(最后一个部分也许),错误的标题将显示在导航栏中。

    如果有人可以对此进行改进以使其更高效或更好,请这样做,我会相应地更新答案。

    【讨论】:

      【解决方案3】:

      是的,问题是在 visibleCells 集合中不存在页脚和页眉。还有其他方法可以检测部分页眉/页脚的滚动。只需在那里添加一个控件并找到它的矩形。像这样:

      func scrollViewDidScroll(scrollView: UIScrollView) {
      
          if(footerButton.tag == 301)
          {
              let frame : CGRect = footerButton.convertRect(footerButton.frame, fromView: self.view)
              //some process for frame
          }
      
      
      }
      

      【讨论】:

        【解决方案4】:

        在方法viewForSupplementaryElementOfKind中更改以下行:

        self.title = [df stringFromDate:[self dateForFirstDayInSection:indexPath.section]];
        

        到这里:

            if(![[self dateForFirstDayInSection:indexPath.section-1] isKindOfClass:[NSNull class]]){
                self.title = [df stringFromDate:[self dateForFirstDayInSection:indexPath.section-1]];
        }
        

        希望对你有帮助。

        【讨论】:

        • 当下一个标题在屏幕上不可见时,这将不起作用,并且当您向上滚动时,它会在上一个部分可见之前更改标题。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-05-09
        • 2014-08-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-06-29
        相关资源
        最近更新 更多