快速滚动后,粘性滚动视图子视图的帧不正确。可编译的例子

Posted

技术标签:

【中文标题】快速滚动后,粘性滚动视图子视图的帧不正确。可编译的例子【英文标题】:Sticky scrollview subview has incorrect frame after scrolling fast. Compilable example 【发布时间】:2013-12-18 05:42:08 【问题描述】:

在过去的 12 个小时里,我一直在做这件事,我快要脑死了。

查看 bitbucket: https://bitbucket.org/burneraccount/scrollviewexample

git https:https://bitbucket.org/burneraccount/scrollviewexample.git

git ssh:git@bitbucket.org:burneraccount/scrollviewexample.git

^ 这是一个浓缩的、自包含的 Xcode 项目,它体现了我在实际工作中遇到的问题。

总结

我正在尝试实现静态图像效果,其中图像(示例中的岩石)似乎粘在屏幕上,而下部内容向上滚动似乎超出了滚动视图。

有一个屏幕大小的(UIScrollView*)mainScrollView。这个滚动视图有一个(UIView*)contentViewcontentView 长约 1200 pts,有两个子视图:(UIScrollView*)imageScroller 和 (UITextView*)textView。

一切正常,直到您向上滚动一点,然后快速向下滚动。运动停止后的结果位置不正确。它在 scrollviewDidScroll 委托方法中以某种方式无法正确更新。快速向下滚动(仅在您向上拖动后)的行为有些正确,但仍会导致视图错位:

轻轻拖动几乎没有任何作用,滚动的速度与不正确的偏移量直接相关。

这是有问题的代码:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView

    if (scrollView == self.mainScrollView)
    

        CGFloat mainOffset = self.mainScrollView.contentOffset.y;
        if (mainOffset > 0 && mainOffset < self.initialImageScrollerFrame.size.height)
        
            CGRect imageFrame = self.imageScroller.frame;
            CGFloat offset = mainOffset-self.lastOffset;
            self.imageScroller.frame = CGRectMake(imageFrame.origin.x,
                                                  imageFrame.origin.y + offset, // scroll up
                                                  imageFrame.size.width,
                                                  imageFrame.size.height - offset); // hide the bottom of the image
            self.lastOffset = mainOffset;
        

    

我几乎以我能想象到的所有方式对其进行了重组,并且总是产生类似的效果。最糟糕的部分是非常简单直接的代码如何无法做到看起来可以保证做的事情。 同样奇怪的是,我在实际项目中使用的缩放效果使用相同的机制来调整相同视图元素的大小可以正常工作。它在(contentOffset &lt; 0) 内部而不是(contentOffset &gt; 0) 中运行,因此当您将视图拉到其正常偏移下方时,它只会放大imageScroller。这导致我密谋当它穿过contentOffset 0 时丢失了一些数据,但我的阴谋整天都被击落了。

这个示例比我正在处理的真实项目要简单得多,但它正确地重现了问题。如果您无法打开我的项目或无法连接到 repo,请告诉我。

对此可能有一个明显的解决方法,但我已经过了几个小时才能获得任何新的观点。如果我能做到这一点,我会感到非常惊讶。

【问题讨论】:

【参考方案1】:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView

    if (scrollView == self.mainScrollView)
    

        CGFloat mainOffset = self.mainScrollView.contentOffset.y;
        if (mainOffset >= 0 && mainOffset < self.initialImageScrollerFrame.size.height)
        
            CGRect imageFrame = self.imageScroller.frame;
            CGFloat offset = self.mainScrollView.contentOffset.y-self.lastOffset;
            if ( imageFrame.origin.y + offset > 0)   //[2]
                self.imageScroller.frame = CGRectMake(imageFrame.origin.x,
                                                      imageFrame.origin.y + offset, 
                                                      imageFrame.size.width,
                                                      imageFrame.size.height - offset); 
                self.lastOffset = mainOffset;
            
        else if (mainOffset < 0)   //[1]
            self.imageScroller.frame = self.initialImageScrollerFrame;
        
    

[1] - 当您的mainOffset 低于零时,您需要重置您的 imageScroller 框架。 ScrollViewDidScroll 不会注册每个像素的移动价值,它只是偶尔调用一次(尝试记录它,你会看到)。所以当你滚动得更快时,它的偏移量就更远了。这可能导致在下一次调用 scrollViewDiDScroll 时,正向滚动说 +15 变成负向滚动。因此,即使您期望它为零,您的 imageScroller.frame.y 也可能会卡在 +15 偏移量上。因此,一旦您检测到 mainOffset 的负值,您就需要重置您的 imageScroller 框架。

[2] - 同样在这里您需要确保您的 imageScroller.frame.y 始终为 +ve。

这些修复可以解决问题,但我仍然认为它们“丑陋且不适合生产”。对于更简洁的方法,您可能需要重新考虑这些视图之间的相互作用,以及您想要实现的目标。

顺便说一句,您的“image.png”实际上是 jpg。虽然这些在模拟器上渲染得很好,但它会在设备上中断(出现编译器错误)。

【讨论】:

很好的答案,非常感谢。你能给我一个你认为最干净的方法的高级想法吗?我也尝试在 mainScrollview 后面设置 imageScroller,但我仍然遇到同样的问题。 @user,如果不了解您在实际项目中想要实现的目标,就很难描述一种干净的方法——这超出了这个问题的范围。但是我想知道为什么要将水平滚动的 imageScroller 嵌入到另一个垂直滚动的 mainView 中,因为 imageScroller 应该相对于那个 mainView 保持静止。我原以为您可以将其视为同一个 superView 中的同级,以实现相同的结果,而无需进行所有这些偏移计算和重新定位。 想要的效果是让文本内容滚动到图像滚动条的顶部。这是一个生产应用程序,有几个工程师正在开发它,我想在对当前实现进行尽可能少的更改的情况下实现这种效果(因此其他开发人员在进行新更改时不必重新考虑这个控制器) . 您是否自己将imageScroller 放在(屏幕大小)mainScrollview 后面并有一个相同高度的透明窗口传递触摸事件,或者您是否将imageScroller 直接放在上方mainScrollview 并在用户向上滚动时增加主 scrollView 的 origin-&gt;y(因为默认行为会将滚动文本内容保持在 mainScrollview 的边界内并低于 mainScrollview.origin.y)? @user,我有一个有趣的替代方法,我可以为您推荐 - 但我认为您应该提出一个新问题,因为答案会偏离您原来的太多。无论如何,明天我会给你一个体面的大纲(已经很晚了;-j)【参考方案2】:

我会在这里发布我的更新。

一位回答者提到了- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView,虽然这不适用于此处,但类似的scrollViewDidEndDecelerating: 确实适用。在昨天的挣扎中,我实施了

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

    if (self.mainScrollView.contentOffset.y < 10)
    
        [UIView animateWithDuration:0.2 animations:^
            self.imageScroller.frame = self.initialImageScrollerFrame;

        ];
    

这在某种程度上是一种解决方法,但它很丑陋,而且绝对不适合生产。如果 contentOffset 足够接近 0,它只是将帧动画返回到其原始位置。这是朝着正确方向迈出的一步,但恐怕它不值得作为解决方案。

----

也添加了

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
                     withVelocity:(CGPoint)velocity
              targetContentOffset:(inout CGPoint *)targetContentOffset

    CGPoint point = *targetContentOffset;
    CGFloat offset = point.y;

    if (0 <= offset && offset < 10)
    
        [UIView animateWithDuration:0.2 animations:^
            self.imageScroller.frame = self.initialImageScrollerFrame;
        ];
    

这让我更接近预期的效果,但仍然无法用于生产应用程序。

【讨论】:

【参考方案3】:

同样的问题,但要添加另一个功能,左/右列表 UIScrollView 的按钮,在快速点击它后,滚动视图打破了我的大脑;(

我认为没有动画的滚动效果更好,它可以防止 UIScrollView 内容出现故障。可能不是答案,但是...

【讨论】:

【参考方案4】:

嘿,伙计,使用下面的方法可能会对你有所帮助,它是 UIScrollView 的委托方法:

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView

在此方法中,您可以将滚动视图框架从 x 和 y 更改为 0,0

【讨论】:

我已经使用过这个委托方法(尽管这并不意味着这里没有潜在的解决方案)。但我不希望每次滚动停止时都将滚动视图设置为零。 滚动视图在 setContentOffset:animated: 和 scrollRectToVisible:animated: 方法的实现结束时调用此方法,但前提是请求动画。我没有使用这些方法,但我确实尝试了scrollViewDidEndDecelerating:,但这并没有帮助。随意克隆我的示例并尝试您的解决方案。

以上是关于快速滚动后,粘性滚动视图子视图的帧不正确。可编译的例子的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Swiftui 的主视图底部的滚动视图中放置一个非粘性按钮? Spacer 在滚动视图中似乎没有效果

viewDidLayoutSubviews 中 UIScrollview 的帧不正确

创建UICollectionViewCell时子视图帧不正确

影响滚动视图的几何阅读器

当 tableview 使用自动布局滚动时避免粘性标题视图

UICollectionView 的标题视图在滚动时不断重复 - 如何创建固定(非粘性)的单个集合视图标题?