滚动犹豫与使用自动布局浮动视图的UIScrollView [英] Scroll hesitation with floating view in UIScrollView using auto layout

查看:209
本文介绍了滚动犹豫与使用自动布局浮动视图的UIScrollView的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

问:的结果
我会简化问题,但保持我原来的参考...

QUESTION:
I'll simplify the question but keep my original for reference...

我修改的现有约束的优先级,但结果只改变UIScrollView的子视图之一的地位。其余所有子视图保持其原有的大小和位置,但它看起来像我招致下面一个我在修改的约束所有子视图的布局传递。那么,为什么ViewWillLayoutSubviews和UpdateViewConstraints被称为上的东西并没有改变?

I am modifying the priority of existing constraints, but the result only changes the position of one of the UIScrollView subviews. All remaining subviews maintain their original size and position, yet it looks like I am incurring a layout pass on all subviews below the one I am modifying constraints on. So, why is ViewWillLayoutSubviews and UpdateViewConstraints being called on things that haven't changed?

【原题】
请参阅下面的详细资料。什么是附带的截屏看到滚动犹豫的原因以及如何解决这个问题?

[Original question] See the details below. What is the cause of the scrolling hesitation seen in the included screencasts and how can I fix it?

背景:的结果
我已经建立承载的几个孩子UIViewControllers的意见,每一个标题视图配对,使用户可以切换其内容视图知名度手风琴样式列表控制。我创建了使用自动布局一个UIScrollView这个列表控件。我已经变得非常熟悉汽车的布局与一个UIScrollView的复杂性,但承认我一般pretty新的自动布局。我已经很大程度上依赖于苹果公司的文件和相关的博客文章社区:结果

BACKGROUND:
I’ve built an accordion style list control that hosts the views of several child UIViewControllers, each paired with a header view to enable the user to toggle visibility of its content view. I’ve created this list control using a UIScrollView with auto layout. I’ve become very familiar with the intricacies of auto layout with a UIScrollView but admit that I am pretty new to auto layout in general. I’ve relied heavily on Apple’s documentation and related blog posts from the community:

我实现这个控制,使头视图可以高于其他的UIScrollView内容浮动。非常像一个分组的UITableView的部分意见,他们会坚持的UIScrollView的顶部,用户向下滚动查看更多的内容。顺便说一句,我本来用这一个UITableView,但它的方式管理造成滚动自身性能问题,可见细胞构建的。结果

I’ve implemented this control so that the header views can be floated above other UIScrollView content. Very much like the section views of a grouped UITableView, they will stick to the top of the UIScrollView as the user scrolls down to view more content. Incidentally, I originally built this using a UITableView, but the way it manages visible cells caused scrolling performance issues of its own.

问题:的结果
滚动内容时,我有一些性能问题。我做了一些故障排除,而且我发现,当浮头功能被禁用,滚动性能是pretty好(虽然仍有扩大/折叠一款有些犹豫可能具有相同的原因我的滚动性能问题)。但是,启用此功能时,滚动犹豫,因为每个标题视图是浮动的。我将在我的iPod Touch 5,结果运行我的原型的截屏

PROBLEM:
I’m having some performance issues when scrolling content. I’ve done some troubleshooting, and I’ve found that when the "floating header" feature is disabled, scrolling performance is pretty good (although there is still some hesitation on expanding/collapsing a section which may have the same cause as my scrolling performance issue). But when this feature is enabled, scrolling hesitates as each header view is floated. I’ve included a screencast of my prototype running on my iPod Touch 5.

iPod上运行的触摸5 原型截屏

这是一个非常小的犹豫,但这个原型有显著不太复杂的内容的看法。最后一个项目显示大约一秒钟最多的犹豫。结果

It’s a very minor hesitation, but this prototype has significantly less complex content views. The final project shows hesitation of up to about a second.

联系方式:的结果
该样机已经使用Xamarin建的,但我在Objective-C熟练,如果这就是你想要的答案。下面是我设置了我的约束来支持此功能。我在修改的UIScrollView子视图重新加载()方法做到了这一点。

DETAILS:
The prototype has been built using Xamarin, but I'm proficient in Objective-C if that's how you want to answer. Here’s how I’ve set up my constraints to support this feature. I’ve done this in a Reload() method that modifies the UIScrollView subviews.


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,所以我已经做到了。正如你所看到的,我已经添加了浮动头的限制,即使在执行时没有标题应该浮动。我已经给了他们一个较低的优先级,使他们不会默认应用。我已经做了与倒塌的部分内容高度约束相同。我这样做,这样我就不必添加/删除约束浮动页眉或折叠部分,我只需要修改的约束优先级。我不知道这是很好的做法,但我想这可能有助于避免不必要的布局通行证。

All content in a UIScrollView needs leading, top, trailing and bottom edge constraints, so that the UIScrollView can determine its ContentSize, so I have done that. As you can see, I’ve added the floating header constraints, even though at execution time no headers should float. I’ve given them a lower priority so that they’re not applied by default. I’ve done the same with a content height constraint for a collapsed section. I’ve done this so that I don’t have to add/remove constraints to float a header or collapse a section, I just need to modify constraint priorities. I don't know if that's good practice but I thought it might help avoid unnecessary layout passes.

我跟踪了适用于内联和浮动头的约束。当它确定一个标题应飘来,我降低相关联头部约束DefaultLow的优先级,并增加相关的浮动头约束DefaultHigh的优先级。我这样做,在为UIScrollView的滚动事件的事件处理。我确定哪些部分在ContentOffset被占领的空间和漂浮它的头。我跟踪一个已经飘来,只是为了避免内联的东西,并不需要被内联标题的最后一个索引。

I’m keeping track of the constraints that apply to both inline and floating headers. When it’s determined that a header should be floated, I lower the priority of the relevant inline header constraints to DefaultLow and increase the priority of the relevant floating header constraints to DefaultHigh. I do that in an event handler for the UIScrollView’s Scrolled event. I determine which section is occupying the space at ContentOffset and float its header. I’m keeping track of the last index of the header that’s been floated, just to avoid inlining something that doesn’t need to be inlined.



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

我已经通过添加记录做了一些其他故障排除孩子UIViewControllers的ViewWillLayoutSubviews和UpdateViewConstraints,我可以看到,当一个头是浮动的,一个布局传递在previous内容视图,所有的意见做在其下方。我相信这是犹豫的原因。我不认为这是一个巧合,布局传递包括previous内容。浮动头,我必须deprioritize约束钉扎其上边缘到previous内容视图的底部,并提高优先的约束钉扎其上边缘到的UIScrollView的顶部边缘。

I’ve done some additional troubleshooting by added logging to the ViewWillLayoutSubviews and UpdateViewConstraints of the child UIViewControllers, and I can see that when a header is floated, a layout pass is done on the previous content view and all views below it. I believe this is the cause of the hesitation. I don’t think its a coincidence that the layout pass includes the previous content. To float the header, I have to deprioritize the constraint pinning its top edge to the bottom of the previous content view and increase the priority on the constraint pinning its top edge to the top edge of the UIScrollView.

但是,由于的的UIScrollView里面的内容意见不改变大小和位置,我不认为我应该承担上的任何东西的布局传递。而且,我发现,有时候我不知道。例如,如果我轻弹以快速滚动至底部,标题是浮动一前一后如预期,但不会发生布局通行证 - 至少直到滚动速度减慢。我已经包含在模拟器上运行我的原型的截屏,用控制台输出。

But since the size and position of the content views inside the UIScrollView don’t change, I don’t think I should be incurring a layout pass on anything. And, I’ve found that sometimes I don’t. For example, if I flick to quickly scroll to the bottom, the headers are floated one after the other as expected, but no layout passes occur — at least not until the scroll velocity slows. I’ve included a screencast of my prototype running in the simulator, with console output.

与控制台输出模拟器运行的原型截屏

我还包含一个链接到源。

I’ve also included a link to the source.

<一个href=\"https://www.dropbox.com/s/yxkppulxpkxeey2/ExpandableListViewTooManyCallsToViewLayoutSubviews.zip?dl=0\"相对=nofollow>来源存档

推荐答案

当我想你可能通过的UITableView 得到更好的服务满足您提到的性能问题,而不是重新发明的UITableView ,还有这里肯定有些地方看起来可疑。你应该先通过仪器运行code,看看真正的问题。想不花一些时间分析,以优化通常是鹅追逐。

While I think you'd probably be better served addressing your mentioned performance problems via UITableView rather than reinventing UITableView, there are definitely some places here that look suspicious. You should first run your code through Instruments to see where the real problems are. Trying to optimize without spending some time profiling is usually a goose-chase.

但尽管如此,让我们看看你的循环的某些部分。循环往往哪里都是问题。

But still, let's look at some parts of your loop. Loops are often where are problems are.

        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);

这是重复调用功能很多,你不应该需要。 NumberOfSections 只应调用一次。 GetHeightForHeader 最好是很便宜的,否则你应该在阵列缓存其结果。同样 GetViewForSection 。如果这不是一个简单的数组查找,你应该把它变成之一。你还生成 scrollContent 每部分,但它总是一样的。

This is calling a lot of functions repeatedly that you shouldn't need to. NumberOfSections should only be called once. GetHeightForHeader had better be very cheap, or else you should cache its results in an array. Similarly GetViewForSection. If that isn't a simple array lookup, you should turn it into one. You're also generating scrollContent for every section, but it's always the same.

最后,我想给在 floatHeader 强大的外观和 inlineHeader 。请确保这些已经知道他们的确切值,并没有计算很多东西。你的循环应该做什么,但查找视图有一个范围重叠当前Y坐标y坐标(你并不需要一个完整的 IntersectsWith ,只是Y坐标)然后调整1或2个视图的Y坐标(当前浮动视图或previous浮动观和新的)。你不应该需要什么怎么回事。

Finally, I would give a strong look at floatHeader and inlineHeader. Make sure that these already know their exact values and don't have to calculate a lot of stuff. Your loop should do nothing but find what view has a range of Y coordinates that overlap the current Y coordinate (you don't need a full IntersectsWith, just the Y coordinate), and then adjust either 1 or 2 view's Y coordinate (the current floating view, or the previous floating view and the new one). You shouldn't need anything else going on here.

不过,第一步是通过仪器来运行它,看看跳出。

But step one is to run it through Instruments and see what jumps out.

这篇关于滚动犹豫与使用自动布局浮动视图的UIScrollView的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆