UIScrollView 混合布局方法的问题

Trouble with UIScrollView mixed layouts approach

我正在尝试关注 UIScrollView And Autolayout Mixed approach

我看过其他类似的问题but with no accepted answer

我不确定这是我需要的。

请理解我既不使用故事板也不使用 XIB。 我的所有视图都是在 Xamarin.iOS 中使用 c# 以编程方式创建的。这与 objective C 代码非常相似。

像许多其他人一样,我找不到让我的滚动视图真正滚动的方法。

所以在我的主视图控制器中,我在 ViewDidLoad() 中有以下内容:

   public override void ViewDidLoad()
    {
        base.ViewDidLoad();

        _scrollView = new UIScrollView
        {
            ShowsHorizontalScrollIndicator = false,
            TranslatesAutoresizingMaskIntoConstraints = false,
            AlwaysBounceVertical = true,
            Bounces = true
        };

        _refreshControl = new UIRefreshControl { TranslatesAutoresizingMaskIntoConstraints = false };
        _refreshControl.Enabled = true;
        _refreshControl.ValueChanged -= RefreshControl_ValueChanged;
        _refreshControl.ValueChanged += RefreshControl_ValueChanged;
        if (UIDevice.CurrentDevice.CheckSystemVersion(10, 0))
        {
            _scrollView.RefreshControl = _refreshControl;
        }
        else
        {
            _scrollView.AddSubview(_refreshControl);
        }

        this.View.AddSubview(_scrollView);

        _scrollViewContainer = new UIView { TranslatesAutoresizingMaskIntoConstraints = true };
        _scrollView.AddSubview(_scrollViewContainer);

        _scrollView.Anchor(top: this.View.SaferAreaLayoutGuide().TopAnchor, leading: this.View.LeadingAnchor, bottom: this.View.SaferAreaLayoutGuide().BottomAnchor, trailing: this.View.TrailingAnchor);
    }

没什么特别的,我创建了我的 UIScrollView,将它添加为主视图的子视图。创建一个 UIRefreshControl 并将其添加到滚动视图。创建一个普通的 UIView 并将其添加到 UIScrollView 以放置我的所有子视图。

因为我使用的是 AutoLayout,所以 ScrollView 锚定在主视图的约束范围内。

现在稍后在 ViewWillAppear() 中,我正在创建添加到普通 UIView 的卡片视图。这些卡片视图使用自动布局相互堆叠,第一张卡片锚定到容器视图的顶部、前导和尾随锚点。

这些卡片中的所有内容都使用自动布局,基本上可以堆叠所有内容。

        public UIView BuildQOLCard()
        {
            CardView qolCard = new CardView();
            _scrollViewContainer.AddSubview(qolCard);
            qolCard.TranslatesAutoresizingMaskIntoConstraints = false;
            qolCard.CornerRadius = 5f;
            qolCard.ShadowOffsetHeight = 0;
            qolCard.ShadowOffsetWidth = 0;
            qolCard.BackgroundColor = UIColor.White;
...
           qolCard.Anchor(leading: _scrollViewContainer.LeadingAnchor, trailing: _scrollViewContainer.TrailingAnchor, top: _scrollViewContainer.TopAnchor, padding: new UIEdgeInsets(10f, 10f, 10f, 10f));

            return qolCard;
        }

当然,容器视图不会神奇地知道如何调整自身的大小。所以我必须给它一个尺寸,否则我什么都看不到。

所以我像这样找了一个帮手:

public void SizeScrollViewContentSize()
{
    nfloat scrollViewHeight = 0.0f;
    foreach (UIView view in this._scrollViewContainer.Subviews)
    {
        scrollViewHeight += view.Frame.Size.Height;
    }

    this._scrollViewContainer.Frame = new CGRect(0, 0, this._scrollView.Frame.Width, scrollViewHeight);
}

但我似乎无法弄清楚何时调用它,因为子视图在这里但它们的大小未知。我尝试从 ViewDidLayoutSubviews() 调用它。

感谢任何帮助。


编辑:应用已接受答案的建议,导致启用滚动,但滚动视图总是弹回顶部。

现在是代码:

public override void ViewDidLoad()
{
    base.ViewDidLoad();

    _scrollView = new UIScrollView
    {
        ShowsHorizontalScrollIndicator = false,
        TranslatesAutoresizingMaskIntoConstraints = false
    };

    if (!UIDevice.CurrentDevice.CheckSystemVersion(10, 0))
    {
        _scrollView.AlwaysBounceVertical = true;
        _scrollView.Bounces = true;
    }

    _refreshControl = new UIRefreshControl { TranslatesAutoresizingMaskIntoConstraints = false };
    _refreshControl.Enabled = true;
    _refreshControl.ValueChanged -= RefreshControl_ValueChanged;
    _refreshControl.ValueChanged += RefreshControl_ValueChanged;
    if (UIDevice.CurrentDevice.CheckSystemVersion(10, 0))
    {
        _scrollView.RefreshControl = _refreshControl;
    }
    else
    {
        _scrollView.AddSubview(_refreshControl);
    }

    this.View.AddSubview(_scrollView);

    _scrollViewContainer = new UIView { TranslatesAutoresizingMaskIntoConstraints = false };
    _scrollView.AddSubview(_scrollViewContainer);

    _scrollView.Anchor(top: this.View.SaferAreaLayoutGuide().TopAnchor, leading: this.View.LeadingAnchor, bottom: this.View.SaferAreaLayoutGuide().BottomAnchor, trailing: this.View.TrailingAnchor);

    // Only define the width of the container
    _scrollViewContainer.TopAnchor.ConstraintEqualTo(_scrollView.TopAnchor).Active = true;
    _scrollViewContainer.LeadingAnchor.ConstraintEqualTo(_scrollView.LeadingAnchor).Active = true;
    _scrollViewContainer.WidthAnchor.ConstraintEqualTo(_scrollView.WidthAnchor).Active = true;
}

卡片一:

            CardView qolCard = new CardView();
            _scrollViewContainer.AddSubview(qolCard);
            qolCard.TranslatesAutoresizingMaskIntoConstraints = false;
            qolCard.CornerRadius = 5f;
            qolCard.ShadowOffsetHeight = 0;
            qolCard.ShadowOffsetWidth = 0;
            qolCard.BackgroundColor = UIColor.White;
...
           qolCard.Anchor(leading: _scrollViewContainer.LeadingAnchor, trailing: _scrollViewContainer.TrailingAnchor, top: _scrollViewContainer.TopAnchor, padding: new UIEdgeInsets(10f, 10f, 10f, 10f));

卡片 2:

               CardView goalsCard = new CardView();
                _scrollViewContainer.AddSubview(goalsCard);
                goalsCard.TranslatesAutoresizingMaskIntoConstraints = false;
                goalsCard.CornerRadius = 5f;
                goalsCard.ShadowOffsetHeight = 0;
                goalsCard.ShadowOffsetWidth = 0;
                goalsCard.BackgroundColor = UIColor.White;
...
               // Top should be constrained to the previous CardView's Bottom
                goalsCard.Anchor(leading: _scrollViewContainer.LeadingAnchor, trailing: _scrollViewContainer.TrailingAnchor, top: qolCard.BottomAnchor,padding: new UIEdgeInsets(10f, 10f, 10f, 10f));

最终锚点:

           // constraint the last card bottom to the bottom of the scrollview container
            goalsCard.Anchor(bottom: context._scrollViewContainer.BottomAnchor);

锚定助手:

   internal static void Anchor(this UIView uIView, NSLayoutYAxisAnchor top = null, NSLayoutXAxisAnchor leading = null, NSLayoutYAxisAnchor bottom = null, NSLayoutXAxisAnchor trailing = null, UIEdgeInsets padding = default, CGSize size = default)
    {
        uIView.TranslatesAutoresizingMaskIntoConstraints = false;
        if (top != null)
        {
            uIView.TopAnchor.ConstraintEqualTo(top, padding.Top).Active = true;
        }

        if (leading != null)
        {
            uIView.LeadingAnchor.ConstraintEqualTo(leading, padding.Left).Active = true;
        }

        if (bottom != null)
        {
            uIView.BottomAnchor.ConstraintEqualTo(bottom, -padding.Bottom).Active = true;
        }

        if (trailing != null)
        {
            uIView.TrailingAnchor.ConstraintEqualTo(trailing, -padding.Right).Active = true;
        }

        if (size.Width != 0)
        {
            uIView.WidthAnchor.ConstraintEqualTo(size.Width).Active = true;
        }

        if (size.Height != 0)
        {
            uIView.HeightAnchor.ConstraintEqualTo(size.Height).Active = true;
        }
    }

滚动视图需要定义其框架它需要定义其内容。如果内容约束得当,滚动是自动的。

我将逐步完成此操作 - 使用 Storyboard 只是为了让我们了解发生了什么。当我们通过代码完成时,绝对没有什么不同。

从一个视图控制器开始,添加一个滚动视图,并将其约束到所有四个边(我给它一个绿色背景以便我们可以看到它):

现在我们添加一个 "container" 视图(青色背景),并将所有 4 个边约束到滚动视图:

如果我们运行这个,我们看到:

那是因为 "container" 没有宽度或高度。它的 Leading / Trailing / Top / Bottom 约束定义了滚动视图的 content.

所以,让我们将它的等宽和等高约束到滚动视图:

运行它,我们看到这个:

很好,除了...它永远不会滚动,因为它与滚动视图的高度相同。

因此,我删除了等高 - 因为我希望容器的 内容 确定其高度。

我添加了一个 "CardView" 并将其顶部/前导/尾随约束设置为 "container" 的顶部/前导/尾随,具有 10 点 "padding" 和 100 的高度。我还给它一个底部约束到 "container" (+10) 的底部 - 这将 "pull the bottom of the container" 在 CardView 底部以下最多 10 点:

导致:

现在我添加第二个 CardView,将 Leading/Trailing 约束约束到容器的 Leading/Trailing,具有 10 点 "padding" 和 100 的高度。我将其顶部约束设置为 CardView 的底部-1(+10),我把它的底部约束在"container"(+10)的底部:

不幸的是,我们现在有一个来自 CardView-1 底部的 10 磅约束 有一个来自底部的 10 磅约束CardView-2 - 这当然行不通。

那么,让我们从 CardView-1

中删除 "Bottom to Superview" 约束

我们得到这个:

让我们对更多的卡片视图重复这些步骤,每次将前导/尾部限制为 "container",高度为 100,从上到下为 previous CardView。 .. 然后仅将 last CardView 的底部约束到 "container":

的底部

耶!我们可以滚动:

通过代码执行此操作只需记住每个 CardView 的顶部应限制在 previousCardView 的底部——除了对于 first 卡,您将其限制在 "container" 的顶部,而 last 卡,您将其限制在 "container".

全部通过自动布局完成。无需执行任何 "calculate height and set frame" 代码。


附带说明一下,您可以使用 StackView 并摆脱其中的大部分:

  • 忘了"container"
  • 向滚动视图添加堆栈视图
    • 轴:垂直,间距:10
  • 将堆栈视图的所有 4 个边限制为具有 10 磅填充的滚动视图
  • 给堆栈视图一个等于滚动视图宽度 -20 的宽度约束(对于每一侧的 10 磅填充)

然后,只需将您的 CardView 添加为堆栈视图的 arrangedSubview。

然后....完成!