使用自动布局在UIScrollView中使用浮动视图滚动犹豫

题:
我会简化问题,但保留我的原始参考…

我正在修改现有约束的优先级,但结果只会改变其中一个UIScrollView子视图的位置。 所有剩下的子视图保持原来的大小和位置,但是看起来像是我在修改约束条件下的所有子视图上发生布局传递。 那么,为什么ViewWillLayoutSubviews和UpdateViewConstraints被调用的东西没有改变?

[原始问题]请看下面的细节。 在包含的屏幕录像中看到的滚动犹豫的原因是什么?我该如何解决?

背景:
我已经构build了一个手风琴风格的列表控件,它承载了几个子UIViewControllers的视图,每个UIViewControllers都与一个标题视图配对,以使用户能够切换其内容视图的可见性。 我已经使用自动布局的UIScrollView创build了这个列表控件。 我已经非常熟悉用UIScrollView进行自动布局的复杂性,但是承认我对于自动布局来说是相当新颖的。 我严重依赖苹果的文档和社区的相关博客文章:

  • 苹果的文档
  • 相关的StackOverflow问题
  • 在许多其他许多人之中。

我已经实现了这个控件,使得标题视图可以浮动在其他UIScrollView内容之上。 非常像分组的UITableView的部分视图,当用户向下滚动以查看更多内容时,他们将坚持到UIScrollView的顶部。 顺便说一下,我最初使用UITableView来构build它,但是它pipe理可见单元格的方式导致了它自己的滚动性能问题。

问题:
滚动内容时遇到一些性能问题。 我已经做了一些故障排除,我发现,当“浮动标题”function被禁用时,滚动性能是相当不错的(虽然仍然有一些犹豫,扩大/折叠可能与我的滚动相同的原因部分性能问题)。 但是,当启用此function时,每个标题视图浮动时,滚动犹豫不决。 我已经在我的iPod Touch 5上添加了我的原型屏幕录像。

在iPod Touch 5上运行原型的Screencast

这是一个非常小的犹豫,但这个原型具有明显不太复杂的内容视图。 最后的项目显示了犹豫不决,大约一秒钟。

细节:
原型是使用Xamarin构build的,但是我精通Objective-C,如果这就是你想要回答的问题。 以下是我如何设置我的约束来支持此function。 我已经在修改UIScrollView子视图的Reload()方法中做了这个。

UIView previousContent = null; for (var sectionIdx = 0; sectionIdx < this.Source.NumberOfSections (this); sectionIdx++) { var vwHeader = this.Source.GetViewForHeader (this, sectionIdx); var vwContent = this.Source.GetViewForSection (this, sectionIdx); this.scrollView.AddSubview (vwHeader); this.scrollView.AddSubview (vwContent); this.scrollView.BringSubviewToFront (vwHeader); var headerHeight = this.Source.GetHeightForHeader (this, sectionIdx); var isSectionCollapsed = this.Source.GetIsSectionCollapsed (this, sectionIdx); // This will never change, so set constraint priority to Required (1000) var headerHeightConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Height, NSLayoutRelation.Equal, null, NSLayoutAttribute.Height, 1.0f, headerHeight); headerHeightConstraint.Priority = (float)UILayoutPriority.Required; this.AddConstraint (headerHeightConstraint); // This constraint is used to handle visibility of a section. // This is updated in UpdateConstraints.  var contentZeroHeightConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Height, NSLayoutRelation.Equal, null, NSLayoutAttribute.Height, 1.0f, 0.0f); if (isSectionCollapsed) contentZeroHeightConstraint.Priority = (float)UILayoutPriority.Required - 1.0f; else contentZeroHeightConstraint.Priority = (float)UILayoutPriority.DefaultLow;  this.AddConstraint (contentZeroHeightConstraint);  // Set initial state of dictionary that keeps track of all inline and floating header constraints if (!this.inlineConstraints.ContainsKey (sectionIdx)) this.inlineConstraints.Add (sectionIdx, new List<NSLayoutConstraint> ()); this.inlineConstraints [sectionIdx].Clear (); if (!this.floatConstraints.ContainsKey (sectionIdx)) this.floatConstraints.Add (sectionIdx, new List<NSLayoutConstraint> ()); this.floatConstraints [sectionIdx].Clear ();  // If this is the first section, pin top edges to the scrollview, not the previous sibling.  if (previousContent == null) { // Pin the top edge of the header view to the top edge of the scrollview. var headerTopToScrollViewTopConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Top, 1.0f, 0.0f); headerTopToScrollViewTopConstraint.Priority = (float)UILayoutPriority.DefaultHigh; // Add this constraint to the dictionary that tracks inline constraints, because we will need to change it when this header view needs to float. this.inlineConstraints [sectionIdx].Add (headerTopToScrollViewTopConstraint); this.AddConstraint (headerTopToScrollViewTopConstraint); // Also pin the top edge of the content view to the top edge of the scrollview, with a padding of header height. // This is done to minimize constraints that need to be modified when a header is floated.  // May be safely changed to pin to the bottom edge of the header view. var contentTopToScrollViewTopConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Top, 1.0f, headerHeight); contentTopToScrollViewTopConstraint.Priority = (float)UILayoutPriority.DefaultHigh; this.AddConstraint (contentTopToScrollViewTopConstraint); } else { // Pin the top edge of the header view to the bottom edge of the previous content view. var previousContentBottomToHeaderTopConstraint = NSLayoutConstraint.Create (previousContent, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, vwHeader, NSLayoutAttribute.Top, 1.0f, 0.0f); previousContentBottomToHeaderTopConstraint.Priority = (float)UILayoutPriority.DefaultHigh;  // Add this constraint to the dictionary that tracks inline constraints, because we will need to change it when this header view needs to float.  this.inlineConstraints [sectionIdx].Add (previousContentBottomToHeaderTopConstraint);  this.AddConstraint (previousContentBottomToHeaderTopConstraint); // Also pin the top edge of the content view to the bottom edge of the previous content view. // This is done to minimize constraints that need to be modified when a header is floated. // May be safely changed to pin to the bottom edge of the header view.  var previousContentBottomToContentTopConstraint = NSLayoutConstraint.Create (previousContent, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, vwContent, NSLayoutAttribute.Top, 1.0f, -headerHeight);  previousContentBottomToContentTopConstraint.Priority = (float)UILayoutPriority.DefaultHigh; this.AddConstraint (previousContentBottomToContentTopConstraint); } // If this is the last section, pin the bottom edge of the content view to the bottom edge of the scrollview. if (sectionIdx == this.Source.NumberOfSections (this) - 1) { var contentBottomToScrollViewBottomConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Bottom, 1.0f, 0.0f); contentBottomToScrollViewBottomConstraint.Priority = (float)UILayoutPriority.DefaultHigh; this.AddConstraint (contentBottomToScrollViewBottomConstraint); } // Pin the leading edge of the header view to the leading edge of the scrollview. var headerLeadingToScrollViewLeadingConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Leading, 1.0f, 0.0f); headerLeadingToScrollViewLeadingConstraint.Priority = (float)UILayoutPriority.DefaultHigh; // Add this constraint to the dictionary that tracks inline constraints, because we will need to change it when this header view needs to float. this.inlineConstraints [sectionIdx].Add (headerLeadingToScrollViewLeadingConstraint); this.AddConstraint (headerLeadingToScrollViewLeadingConstraint); // Pin the leading edge of the content view to the leading edge of the scrollview. var contentLeadingToScrollViewLeadingConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Leading, 1.0f, 0.0f); contentLeadingToScrollViewLeadingConstraint.Priority = (float)UILayoutPriority.DefaultHigh; this.AddConstraint (contentLeadingToScrollViewLeadingConstraint); // Pin the trailing edge of the header view to the trailing edge of the scrollview. var headerTrailingToScrollViewTrailingConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Trailing, 1.0f, 0.0f); headerTrailingToScrollViewTrailingConstraint.Priority = (float)UILayoutPriority.DefaultHigh; // Add this constraint to the dictionary that tracks inline constraints, because we will need to change it when this header view needs to float. this.inlineConstraints [sectionIdx].Add (headerTrailingToScrollViewTrailingConstraint); this.AddConstraint (headerTrailingToScrollViewTrailingConstraint); // Pin the trailing edge of the content view to the trailing edge of the scrollview. var contentTrailingToScrollViewTrailingConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Trailing, 1.0f, 0.0f); contentTrailingToScrollViewTrailingConstraint.Priority = (float)UILayoutPriority.DefaultHigh; this.AddConstraint (contentTrailingToScrollViewTrailingConstraint); // Add a width constraint to set header width to scrollview width. var headerWidthConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Width, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Width, 1.0f, 0.0f); headerWidthConstraint.Priority = (float)UILayoutPriority.Required; this.AddConstraint (headerWidthConstraint); // Add a width constraint to set content width to scrollview width. var contentWidthConstraint = NSLayoutConstraint.Create (vwContent, NSLayoutAttribute.Width, NSLayoutRelation.Equal, this.scrollView, NSLayoutAttribute.Width, 1.0f, 0.0f); contentWidthConstraint.Priority = (float)UILayoutPriority.Required; this.AddConstraint (contentWidthConstraint); // Add a lower priority constraint to pin the leading edge of the header view to the leading edge of the parent of the scrollview. var floatHeaderLeadingEdgeConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, this, NSLayoutAttribute.Leading, 1.0f, 0.0f); floatHeaderLeadingEdgeConstraint.Priority = (float)UILayoutPriority.DefaultLow; // Add this constraint to the dictionary that tracks floating constraints, because we will need to change it when this header view needs to be inline. this.floatConstraints [sectionIdx].Add (floatHeaderLeadingEdgeConstraint); this.AddConstraint (floatHeaderLeadingEdgeConstraint); // Add a lower priority constraint to pin the top edge of the header view to the top edge of the parent of the scrollview. var floatHeaderTopEdgeConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this, NSLayoutAttribute.Top, 1.0f, 0.0f); floatHeaderTopEdgeConstraint.Priority = (float)UILayoutPriority.DefaultLow; // Add this constraint to the dictionary that tracks floating constraints, because we will need to change it when this header view needs to be inline. this.floatConstraints [sectionIdx].Add (floatHeaderTopEdgeConstraint); this.AddConstraint (floatHeaderTopEdgeConstraint); // Add a lower priority constraint to pin the trailing edge of the header view to the trailing edge of the parent of the scrollview. var floatHeaderTrailingEdgeConstraint = NSLayoutConstraint.Create (vwHeader, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, this, NSLayoutAttribute.Trailing, 1.0f, 0.0f); floatHeaderTrailingEdgeConstraint.Priority = (float)UILayoutPriority.DefaultLow; // Add this constraint to the dictionary that tracks floating constraints, because we will need to change it when this header view needs to be inline. this.floatConstraints [sectionIdx].Add (floatHeaderTrailingEdgeConstraint); this.AddConstraint (floatHeaderTrailingEdgeConstraint); previousContent = vwContent; } 

UIScrollView中的所有内容都需要引导,顶部,尾部和底部的边界约束,以便UIScrollView可以确定它的ContentSize,所以我已经完成了。 正如你所看到的,我已经添加了浮动头部约束,即使在执行时没有头部应该浮动。 我已经给他们一个较低的优先级,以便它们不被默认应用。 我已经完成了与折叠节的内容高度约束。 我已经这样做了,所以我不必添加/删除约束来浮动标题或折叠节,我只需要修改约束优先级。 我不知道这是不是好的做法,但我认为这可能有助于避免不必要的布局。

我跟踪适用于内联和浮动标头的约束。 当确定一个头文件应该被浮动时,我将相关的内联头文件约束的优先级降低到DefaultLow,并将相关的浮动头约束的优先级提高到DefaultHigh。 我在UIScrollView的Scrolled事件的事件处理程序中这样做。 我确定哪个部分占据ContentOffset的空间并浮动它的头部。 我正在跟踪浮动头的最后一个索引,只是为了避免内联内联的东西。

 private int lastFloatHeaderIdx = -1; private void scrolled (object sender, EventArgs e) { // Restore the code below to see the scroll hesitation from what I think are unnecessary calls to ViewWillLayoutSubviews and UpdateViewConstraints // How can I achieve this behavior without incurring the unnecessary expense? if (this.Source != null) { for (var idx = 0; idx < this.Source.NumberOfSections (this); idx++) { var headerHeight = this.Source.GetHeightForHeader (this, idx); var vwContent = this.Source.GetViewForSection (this, idx); var sectionFrame = new CGRect (new CGPoint(vwContent.Frame.X, vwContent.Frame.Y - headerHeight), new CGSize(vwContent.Frame.Width, headerHeight + vwContent.Frame.Height)); var scrollContent = new CGRect (this.scrollView.ContentOffset.X, this.scrollView.ContentOffset.Y, this.scrollView.Frame.Width, 1.0f); if (sectionFrame.IntersectsWith (scrollContent)) { this.floatHeader (idx); } else if (idx > this.lastFloatHeaderIdx) { // This is an unnecessary optimization. Appears to have no effect. var inlines = this.inlineConstraints [idx]; if (inlines.Count > 0 && inlines [0].Priority < (float)UILayoutPriority.DefaultHigh) { // This is also an unnecessary optimization. Appears to have no effect. this.inlineHeader (idx); } } } } } 

我已经通过添加日志logging到子UIViewControllers的ViewWillLayoutSubviews和UpdateViewConstraints做了一些额外的故障排除,我可以看到,当一个标题被浮动时,在先前的内容视图和它下面的所有视图上进行布局传递。 我相信这是犹豫的原因。 我不认为这是一个巧合,布局通过包括以前的内容。 为了浮动头部,我必须将约束其顶部边界的约束放在先前内容视图的底部,并增加约束将其顶部边缘固定到UIScrollView顶部边缘的优先级。

但是由于UIScrollView中的内容视图的大小和位置不会改变,所以我不认为我应该对任何东西进行布局传递。 而且,我发现有时候我没有。 例如,如果我轻弹以快速滚动到底部,那么标题将按照预期一个接一个地浮动,但不会发生布局传递 – 至less在滚动速度减慢之前不会发生布局传递。 我已经在模拟器中包含了我的原型屏幕录像,并带有控制台输出。

在控制台输出模拟器中运行的原型Screencast

我还包括一个链接到源代码。

档案来源

虽然我认为你可能会更好地通过UITableView解决你提到的性能问题,而不是重新创buildUITableView ,但是在这里肯定有一些看起来可疑的地方。 你应该首先通过仪器来运行你的代码,看看真正的问题在哪里。 试图优化,而不花费一些时间分析通常是一个鹅的追逐。

但是,我们来看看你的循环的一些部分。 循环往往是问题的地方。

  for (var idx = 0; idx < this.Source.NumberOfSections (this); idx++) { var headerHeight = this.Source.GetHeightForHeader (this, idx); var vwContent = this.Source.GetViewForSection (this, idx); var sectionFrame = new CGRect (new CGPoint(vwContent.Frame.X, vwContent.Frame.Y - headerHeight), new CGSize(vwContent.Frame.Width, headerHeight + vwContent.Frame.Height)); var scrollContent = new CGRect (this.scrollView.ContentOffset.X, this.scrollView.ContentOffset.Y, this.scrollView.Frame.Width, 1.0f); 

这反复调用了许多你不需要的function。 NumberOfSections只能被调用一次。 GetHeightForHeader最好是非常便宜的,否则你应该将结果caching在一个数组中。 同样的GetViewForSection 。 如果这不是一个简单的数组查找,你应该把它变成一个。 你也为每个部分生成scrollContent ,但总是一样的。

最后,我会详细介绍一下floatHeaderinlineHeader 。 确保这些已经知道他们的确切值,不必计算很多东西。 你的循环应该什么都不做,但是find哪个视图有一个与当前Y坐标重叠的Y坐标范围(你不需要一个完整的IntersectsWith ,只需要Y坐标),然后调整1或者2视图的Y坐标(当前的浮动视图,或以前的浮动视图和新的)。 你不应该在这里需要任何其他的东西。

但是,第一步是通过仪器运行,看看跳出了什么。