具有匹配宽度 DocumentView 的 NSScrollView

NSScrollView with a matching width DocumentView

首先,我想说的是,我已经查看了大量其他资源来尝试实现此目的,但我所查看的内容似乎都无济于事。

例如:Automatically grow document view of NSScrollView using auto layout?

我有一个 NSScrollView,它是我在 Interface Builder 中创建的,并设置了约束以填充它所在的 window:滚动视图的顶部、左侧、右侧和底部设置为等于顶部、左侧、父视图的右侧和底部(这是 Xamarin 中 viewcontroller 使用的 xib 视图)。

我想要的文档视图是一个简单的 NSView,它在我的 viewcontroller 的 ViewDidLoad:

中动态填充了 NSImageViews
EventListScrollView.TranslatesAutoresizingMaskIntoConstraints = false;

var documentView = new NSView(EventListScrollView.Bounds);
//documentView.AutoresizingMask = NSViewResizingMask.WidthSizable | NSViewResizingMask.HeightSizable;
//documentView.TranslatesAutoresizingMaskIntoConstraints = false;

NSView lastHeader = null;
foreach(var project in _projects)
{
    var imageView = new NSImageView();
    imageView.TranslatesAutoresizingMaskIntoConstraints = false;
    imageView.SetContentCompressionResistancePriority(1, NSLayoutConstraintOrientation.Horizontal);
    imageView.SetContentCompressionResistancePriority(1, NSLayoutConstraintOrientation.Vertical);
    imageView.ImageScaling = NSImageScale.ProportionallyUpOrDown;

    var xPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Left, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Left, 1, 0);
    NSLayoutConstraint yPosConstraint = null;
    if(lastHeader!=null)
    {
        yPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, lastHeader, NSLayoutAttribute.Bottom, 1, 0);
    }
    else
    {
        yPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Top, 1, 0);
    }

    var widthConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Width, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Width, 1, 0);
    var heightConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, imageView, NSLayoutAttribute.Width, (nfloat)0.325, 0);

    documentView.AddSubview(imageView);
    documentView.AddConstraints(new[] { xPosConstraint, yPosConstraint, widthConstraint, heightConstraint });

    lastHeader = imageView;
    ImageService.Instance.LoadUrl($"https:{project.ProjectLogo}").Into(imageView);
}

EventListScrollView.DocumentView = documentView;

因为我不一定知道 _projects 中有多少项目,而且我不知道将加载到每个 imageView 中的图像的高度,尽管我知道预期比例,直到运行时我才知道 documentView 的高度。

我希望每次包含 window 滚动视图时 documentView 的宽度都调整为与 NSClipView 相同(因此也是滚动视图)调整大小。至于 documentView 的高度,我希望它由我在 documentView 中填充的 imageView 的大小决定,这就是为什么高度约束是相对于宽度。

我玩过 documentViewAutoresizingMaskNSClipView、滚动视图等。我尝试将 TranslatesAutoresizingMaskIntoConstraints 设置为 false 的所有内容并添加约束以将 documentView 的左、右和上设置为等于 NSClipView 的左、右和上。当我这样做时,视图似乎甚至没有出现。我似乎无法弄清楚这一点。

我也试过在不使用 NSImageViews 的情况下这样做:

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

    EventListScrollView.TranslatesAutoresizingMaskIntoConstraints = false;
    var clipView = new NSClipView
    {
        TranslatesAutoresizingMaskIntoConstraints = false
    };

    EventListScrollView.ContentView = clipView;
    EventListScrollView.AddConstraint(NSLayoutConstraint.Create(clipView, NSLayoutAttribute.Left, NSLayoutRelation.Equal, EventListScrollView, NSLayoutAttribute.Left, 1, 0));
    EventListScrollView.AddConstraint(NSLayoutConstraint.Create(clipView, NSLayoutAttribute.Right, NSLayoutRelation.Equal, EventListScrollView, NSLayoutAttribute.Right, 1, 0));
    EventListScrollView.AddConstraint(NSLayoutConstraint.Create(clipView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, EventListScrollView, NSLayoutAttribute.Top, 1, 0));
    EventListScrollView.AddConstraint(NSLayoutConstraint.Create(clipView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, EventListScrollView, NSLayoutAttribute.Bottom, 1, 0));

    var documentView = new NSView();
    documentView.WantsLayer = true;
    documentView.Layer.BackgroundColor = NSColor.Black.CGColor;
    documentView.TranslatesAutoresizingMaskIntoConstraints = false;
    EventListScrollView.DocumentView = documentView;
    clipView.AddConstraint(NSLayoutConstraint.Create(documentView, NSLayoutAttribute.Left, NSLayoutRelation.Equal, clipView, NSLayoutAttribute.Left, 1, 0));
    clipView.AddConstraint(NSLayoutConstraint.Create(documentView, NSLayoutAttribute.Right, NSLayoutRelation.Equal, clipView, NSLayoutAttribute.Right, 1, 0));
    clipView.AddConstraint(NSLayoutConstraint.Create(documentView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, clipView, NSLayoutAttribute.Top, 1, 0));

    NSView lastHeader = null;
    foreach(var project in _projects)
    {
        var random = new Random();

        var imageView = new NSView();
        imageView.TranslatesAutoresizingMaskIntoConstraints = false;
        imageView.WantsLayer = true;
        imageView.Layer.BackgroundColor = NSColor.FromRgb(random.Next(0, 255), random.Next(0, 255), random.Next(0, 255)).CGColor;

        var xPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Left, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Left, 1, 0);
        NSLayoutConstraint yPosConstraint = null;
        if(lastHeader!=null)
        {
            yPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, lastHeader, NSLayoutAttribute.Bottom, 1, 0);
        }
        else
        {
            yPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Top, 1, 0);
        }

        var widthConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Width, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Width, 1, 0);
        var heightConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, imageView, NSLayoutAttribute.Width, (nfloat)0.325, 0);

        documentView.AddSubview(imageView);
        documentView.AddConstraints(new[] { xPosConstraint, yPosConstraint, widthConstraint, heightConstraint });

        lastHeader = imageView;
    }
}

我明白了。所以,这样做的诀窍是 documentView 不会知道自己有多大,除非你把它的底部固定到你的最后一个子视图上。您可以使用视觉格式来执行此操作,但如果您直到运行时才真正知道您的视图是什么,那就太过分了。这是对我有用的代码:

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

    var clipView = EventListScrollView.ContentView;

    var documentView = new FlippedView();
    documentView.TranslatesAutoresizingMaskIntoConstraints = false;
    EventListScrollView.DocumentView = documentView;
    clipView.AddConstraint(NSLayoutConstraint.Create(documentView, NSLayoutAttribute.Left, NSLayoutRelation.Equal, clipView, NSLayoutAttribute.Left, 1, 0));
    clipView.AddConstraint(NSLayoutConstraint.Create(documentView, NSLayoutAttribute.Right, NSLayoutRelation.Equal, clipView, NSLayoutAttribute.Right, 1, 0));
    clipView.AddConstraint(NSLayoutConstraint.Create(documentView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, clipView, NSLayoutAttribute.Top, 1, 0));

    NSView lastHeader = null;
    foreach (var project in _projects)
    {
        var imageView = new NSImageView();
        imageView.TranslatesAutoresizingMaskIntoConstraints = false;
        imageView.SetContentCompressionResistancePriority(1, NSLayoutConstraintOrientation.Horizontal);
        imageView.SetContentCompressionResistancePriority(1, NSLayoutConstraintOrientation.Vertical);
        imageView.ImageScaling = NSImageScale.ProportionallyUpOrDown;

        var xPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Left, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Left, 1, 0);
        NSLayoutConstraint yPosConstraint = null;
        if (lastHeader != null)
        {
            yPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, lastHeader, NSLayoutAttribute.Bottom, 1, 0);
        }
        else
        {
            yPosConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Top, 1, 0);
        }

        var widthConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Width, NSLayoutRelation.Equal, documentView, NSLayoutAttribute.Width, 1, 0);
        var heightConstraint = NSLayoutConstraint.Create(imageView, NSLayoutAttribute.Height, NSLayoutRelation.LessThanOrEqual, imageView, NSLayoutAttribute.Width, (nfloat)0.360, 0);

        documentView.AddSubview(imageView);
        documentView.AddConstraint(xPosConstraint);
        documentView.AddConstraint(yPosConstraint);
        documentView.AddConstraint(widthConstraint);
        documentView.AddConstraint(heightConstraint);

        lastHeader = imageView;
        ImageService.Instance.LoadUrl($"https:{project.ProjectLogo}").Into(imageView);
    }

    if(lastHeader!=null)
    {
        var bottomPinConstraint = NSLayoutConstraint.Create(documentView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, lastHeader, NSLayoutAttribute.Bottom, 1, 0);
        documentView.AddConstraint(bottomPinConstraint);
    }
}

关键是最后一个约束——bottomPinConstraint。这允许 documentView 扩展其高度以适应所有子视图。

此外,即使我手动设置 NSClipViewdocumentView 之间的约束,我仍然需要使用带有 IsFlipped = true; 的视图,否则它会忽略约束并将我的视图固定到 NSClipView 的底部。我的 EventListScrollView 是在 Interface Builder 中创建的,并设置了约束以使其填满它所在的整个 window。

最终结果是一个 NSScrollView 和一个 DocumentView,它填充了 ScrollView 的宽度,但也垂直扩展以适应我以编程方式添加到它的子视图。

我这样做是为了让我的自定义视图根据滚动视图宽度调整大小。

    scrollView.documentView = customView
    customView.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        customView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
        customView.widthAnchor.constraint(greaterThanOrEqualTo: scrollView.widthAnchor),
    ])

自定义视图在 X 轴上提供 intrinsicContentSize

eonil 的回答几乎是完美的,但是如果系统设置为始终显示滚动条,偏移量就会不正确。水平滚动条以一个小的偏移结束。要解决此问题,请改用剪辑视图的锚点:

  NSLayoutConstraint.activate([
     view.leadingAnchor.constraint(equalTo: scrollView.contentView.leadingAnchor),
     view.widthAnchor.constraint(equalTo: scrollView.contentView.widthAnchor),
    ])