UIScrollView内嵌UIScrollView,在两者之间滚动时如何保持平稳过渡

8
我有以下布局(见下图),在大多数情况下都可以正常工作。用户能够在蓝色的UIScrollView中滚动(实际上是一个UITableView,但本问题概括为此),当他们到达此滚动视图的末尾时,他们可以重新开始滚动(必须将手指离开并再次放上去,否则内部滚动视图会反弹),然后“超级”滚动视图开始滚动,显示其余的图像。
我不想要整个“(必须将手指离开并再次放上去,否则内部滚动视图会反弹)”这一部分。理想情况下,一旦包含的UIScrollView到达其内容的末尾,我希望父视图立即接管滚动,以便内部滚动视图不会反弹。
当用户向上滚动时,同样的道理适用;当红色滚动视图到达其内容的顶部时,我希望内部蓝色滚动视图立即开始向上滚动,而不是红色滚动视图在顶部反弹。
有什么想法吗?我能够确定滚动视图何时到达其内容的末尾,但我不确定如何应用此知识来实现我想要的效果。谢谢。
// Inner (blue) scroll view bounds checking
if (scrollView.contentOffset.y + scrollView.frame.size.height > scrollView.contentSize.height) { ... }

// Outer (red) scroll view bounds checking
if (scrollView.contentOffset.y < 0) { ... }
2个回答

20

好的,我有一个巧妙的技巧可以和你分享。

不要使用一个带有红色边框的滚动视图,而是把它作为一个填充整个屏幕的普通UIView。在该视图中,按照你的示例布置滚动视图(表视图)和图像视图。

在所有其他滚动视图和图像视图上方放置一个填充根视图(即也填充整个屏幕)的滚动视图。将此视图的内容大小设置为您想要滚动的所有视图的总内容高度。换句话说,有一个无形的滚动视图位于所有其他视图的顶部,其内容大小高度为内部滚动视图(表视图)内容大小高度+图像视图大小高度。

层次结构应该如下所示:

enter image description here

然后,在你创建的这个具有非常高内容大小的顶部滚动视图上,将其代理设置为你的视图控制器。实现scrollViewDidScroll,我们将进行一些魔法。

滚动视图基本上通过调整边界原点的奇妙公式来滚动,以产生动量等效果。因此,在我们的scrollviewDidScroll方法中,我们将简单地调整基础视图的边界:

-(void)scrollViewDidScroll:(UIScrollView *)scrollView{

    //the scroll view underneath (in the container view) will have a max content offset equal to the content height
    //but minus the bounds height
    CGFloat maxYOffsetForUnderScrollView = self.underScrollView.contentSize.height - self.underScrollView.bounds.size.height;

    CGRect scrolledBoundsForContainerView = self.view.bounds;

    if (scrollView.contentOffset.y <= maxYOffsetForUnderScrollView) {
        //in this scenario we are still within the content for the underScrollView
        //so we make sure the container view is scrolled to the top and set the offset for the contained scrollview
        self.containerView.bounds = scrolledBoundsForContainerView;
        self.underScrollview.contentOffset = scrollView.contentOffset;
        return;
    }

    //in this scenario we have scrolled throug the entirety of the contained scrollview
    //set its offset to the max and change the bounds of the container view to scroll everything else.
    self.underScrollView.contentOffset = CGPointMake(0, maxYOffsetForUnderScrollView);

    scrolledBoundsForContainerView.origin.y = scrollView.contentOffset.y - maxYOffsetForUnderScrollView;
    self.containerView.bounds = scrolledBoundsForContainerView;
}

你会发现,由于 scrollViewDidScroll 在每一帧动画中被调用,所以这种包含视图的虚拟滚动看起来非常自然。

但是等等!我听到你说。现在顶部的滚动视图截取了所有的触摸事件,而其下面的视图也需要被触摸到。我有一个有趣的解决方案。

将位于顶部的滚动视图设置为在屏幕之外(即将其框架设置为在屏幕之外,但仍然具有相同的大小)。然后在你的 viewDidLoad 方法中,将该滚动视图的 panGestureRecogniser 添加到主视图中。这意味着您将获得所有 iOS 的自然滚动动量和其他东西,而不必实际将该视图显示在屏幕上。由于它们的工作方式与 UIEvent 处理不同,因此包含的滚动视图现在可能会出现抖动,因此您需要将其移除。

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    [self.view addGestureRecognizer:self.scrollview.panGestureRecognizer];

    [self.underScrollview removeGestureRecognizer:self.underScrollView.panGestureRecognizer];

    //further code to set up content sizes and stuff
}

我很享受制作这个东西,所以这里有一个指向Github上示例项目的链接:https://github.com/joelparsons/multipleScrollers

编辑:

如果想要在顶部滚动视图离屏幕时显示滚动条,可以将scrollIndicatorInsets设置为此样式的插入物:

CGPoint scrollviewOrigin = self.scrollview.frame.origin;
self.scrollview.scrollIndicatorInsets = UIEdgeInsetsMake(-scrollviewOrigin.y,0,scrollviewOrigin.y,scrollviewOrigin.x);
需要注意的是,仍然必须具有正确的高度,但我相信你明白这个意思。

接下来,为了使工具栏绘制在可见范围之外,您必须关闭剪切边界功能。

self.scrollview.clipsToBounds = NO;

太棒了,非常感谢。有一个小问题; 当滚动视图被推出屏幕时,右侧的滚动指示器会消失。有没有办法保留滚动指示器,并像以前一样允许下面的元素接收触摸事件? - WDUK
与此同时,我已经将滚动视图的宽度缩小到8,并将其贴在屏幕右侧。这似乎是一种不太优雅的解决方法,但我现在会接受你的答案,并希望你能在未来找到更好的解决方案 :) - WDUK
1
谢谢。你的解决方案还不错,但我已经更新了我的答案,并将更新Github代码。 - jackslash
哇,这真的值得加100分!我之前卡在一个不同的问题上,但需要类似的解决方案已经三天了!你应该考虑写一篇关于这种技术的文章。 - deadalnix
1
史诗般的回答。非常感谢。 - rmvz3
嗨@jackslash,你能帮我解决我的问题吗?链接 - j.iese

0

哇,jackslash,你救了我的命。

在我的情况下,我需要使用三个深度的滚动视图。

  1. 父级滚动视图
    • 其中一个子项是滚动视图
  2. 子滚动视图:在下面的图像中显示为“Explanation Scroll View / Review Table View / Information Scroll View”
  3. 隐藏的滚动视图
    • 将自己的内容偏移分配给“父级滚动视图”和“子滚动视图”
    • 具有整个平铺内容的内容大小

视图层次结构截图

设置视图层次结构后,我只需要同步整个平铺内容大小并正确分配内容偏移量即可。

    Observable.combineLatest(
        overviewStackView.rx.observe(CGRect.self, #keyPath(UIStackView.frame)).unwrap(),
        explanationScrollView.rx.observe(CGSize.self, #keyPath(UIScrollView.contentSize)).unwrap()
      )
      .subscribe(onNext: { [weak self] overviewStackViewFrame, explanationScrollViewContentSize in
        guard let self = self else { return }
        let totalContentHeight = overviewStackViewFrame.height + self.segmentedControl.frame.height + explanationScrollViewContentSize.height
        self.hiddenScrollView.contentSize.height = totalContentHeight
      })
      .disposed(by: disposeBag)

    hiddenScrollView.rx.didScroll
      .subscribe(onNext: { [weak self] _ in
        guard let self = self else { return }
        let currentHiddenScrollViewOffsetY = self.hiddenScrollView.contentOffset.y
        let parentScrollViewMaxOffsetY = self.overviewStackView.frame.height
        let expectedChildScrollViewOffsetY = max(currentHiddenScrollViewOffsetY - parentScrollViewMaxOffsetY, 0)
        self.parentScrollView.contentOffset.y = min(parentScrollViewMaxOffsetY, currentHiddenScrollViewOffsetY)
        self.explanationScrollView.contentOffset.y = expectedChildScrollViewOffsetY
      })
      .disposed(by: disposeBag)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接