【问题标题】:UIStackView proportional layout with only intrinsicContentSizeUIStackView 比例布局只有intrinsicContentSize
【发布时间】:2018-02-25 10:52:53
【问题描述】:

我在 UIStackView 中排列子视图的布局时遇到问题,想知道是否有人可以帮助我了解发生了什么。

所以我有一些间距的 UIStackView(例如 1,但这没关系)和 .fillProportionally 分布。我正在添加仅具有 1x1 的 intrinsicContentSize 的排列子视图(可以是任何东西,只是方形视图),我需要它们在 stackView 中按比例拉伸。

问题是,如果我添加没有实际框架的视图,只有固有尺寸,那么我会得到这个错误的布局

否则,如果我添加具有相同大小框架的视图,一切都会按预期工作,

但我真的不喜欢设置视图的框架。

我很确定这完全是关于拥抱和抗压优先,但无法弄清楚正确答案是什么。

这是一个 Playground 示例:

import UIKit
import PlaygroundSupport

class LView: UIView {

    // If comment this and leave only intrinsicContentSize - result is wrong
    convenience init() {
        self.init(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
    }

    // If comment this and leave only convenience init(), then everything works as expected
    public override var intrinsicContentSize: CGSize {
        return CGSize(width: 1, height: 1)
    }
}

let container = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
container.backgroundColor = UIColor.white

let sv = UIStackView()
container.addSubview(sv)


sv.leftAnchor.constraint(equalTo: container.leftAnchor).isActive = true
sv.rightAnchor.constraint(equalTo: container.rightAnchor).isActive = true
sv.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
sv.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
sv.translatesAutoresizingMaskIntoConstraints = false
sv.spacing = 1
sv.distribution = .fillProportionally

// Adding arranged subviews to stackView, 24 elements with intrinsic size 1x1
for i in 0..<24 {
    let a = LView()
    a.backgroundColor = (i%2 == 0 ? UIColor.red : UIColor.blue)
    sv.addArrangedSubview(a)
}
sv.layoutIfNeeded()

PlaygroundPage.current.liveView = container

【问题讨论】:

  • 如果我运行您的确切代码,但将.fillProportionally 更改为.fillEqually,我会得到您的第二张图片...这就是您想要的吗? (顺便说一下,你的 for 循环中不需要 sv.layoutIfNeeded()
  • .fillEqually 实际上并不是我想要的,因为我需要控制元素的宽度(在这个特定的水平示例中),并且 .fillEqually 将给出等宽的列。我的意思是,我确实需要一些列比其他列大两倍,例如,很少有其他列应该比两倍大的列大三倍。关于 sv.layoutIfNeeded(),你说得对,是我的错,在这个例子中我会把它移到循环之外。
  • 如果你给循环内的每个视图添加一个宽度锚,会解决你的问题吗?
  • 经过一些测试...当.spacing 不为零时,.fillProportionally 似乎最终会得到奇怪的结果(更改为sv.spacing = 0,您将获得第二张图像,只是视图之间没有空格)。我不知道它是否会被视为“错误” - 或者只是一个“怪癖”。就好像自动布局正在对 spaces 应用按比例调整大小 --- 然后用绝对值渲染它们。
  • 但是width 约束应该由UIStackView 自动设置,不是吗?当您将视图添加为arrangedSubview 时,width 约束是根据添加视图的intrisicContentSize 以及UIStackView 中现有视图的数量和大小计算的,并应用于视图。为什么我需要再手动添加一次width 约束?顺便说一句,如果我检查 View Debugging,最后一个宽度最大的元素将禁用 width 约束。这在我看来很奇怪。

标签: ios swift autolayout uistackview


【解决方案1】:

这显然是UIStackView的实现中的一个bug(即系统bug)。

DonMag 在他的评论中已经给出了指向正确方向的提示:

当您将堆栈视图的 spacing 设置为 0 时,一切都按预期工作。但是,当您将其设置为任何其他值时,布局就会中断。


原因如下:

ℹ️ 为了简单起见,我将假设堆栈视图具有

  • 一个横轴
  • 10 个排列的子视图

使用.fillProportionally 分布UIStackView 创建系统约束如下:

  • 对于每个排列的子视图,它添加一个 等宽 约束 (UISV-fill-proportionally),该约束与堆栈视图本身相关,并带有 乘数

    arrangedSubview[i].width = multiplier[i] * stackView.width
    

    如果您在堆栈视图中有 n 个排列的子视图,您将获得 n 个这些约束。我们称它们为proportionalConstraint[i](其中i 表示各个视图在arrangedSubviews 数组中的位置)。

  • 这些约束不是必需的(即它们的优先级不是 1000)。相反,arrangedSubviews 数组中第一个元素的约束被分配了 999 的优先级,第二个被分配了 998 的优先级,依此类推:

    proportionalConstraint[0].priority = 999 
    proportionalConstraint[1].priority = 998 
    proportionalConstraint[2].priority = 997 
    proportionalConstraint[3].priority = 996
    ...         
    proportionalConstraint[n–1].priority = 1000 – n
    

    这意味着必需的约束总是会战胜这些比例约束

  • 为了连接排列好的子视图(可能有间距),系统还创建了 n–1 个约束,称为 UISV-spacing

    arrangedSubview[i].trailing + spacing = arrangedSubview[i+1].leading
    

    这些约束是必需的(即优先级 = 1000)。

  • (系统还将创建一些其他约束(例如,对于垂直轴以及将第一个和最后一个排列的子视图固定到堆栈视图的边缘)但我不会在这里详细介绍,因为它们'与了解发生了什么问题无关。)

Apple's documentation 上的.fillProportionally 分布状态:

堆栈视图调整其排列视图的大小以填充堆栈视图轴上的可用空间的布局。视图会根据其内在内容大小沿堆栈视图的轴按比例调整大小。

因此,根据此proportionalConstraints 的multiplier 应按如下方式计算对于spacing = 0

  • totalIntrinsicWidth = ∑iintrinsicWidth[i]
  • multiplier[i] = intrinsicWidth[i] / totalIntrinsicWidth

如果我们排列的 10 个子视图都具有相同的固有宽度,这将按预期工作:

multiplier[i] = 0.1

所有proportionalConstraints。但是,一旦我们将间距更改为非零值,multiplier 的计算就会变得更加复杂,因为必须考虑间距的宽度。我做了数学计算,multiplier[i] 的公式是:


示例:

对于如下配置的堆栈视图:

  • stackView.width = 400
  • stackView.spacing = 2

上述等式将产生:

multiplier[i] = 0.0955

你可以通过相加来证明这是正确的:

(10 * width) + (9 * spacing)
    = (10 * multiplier * stackViewWidth) + (9 * spacing)
    = (10 * 0.0955 * 400) + (9 * 2)
    = (0.955 * 400) + 18
    = 382 + 18
    = 400
    = stackViewWidth

但是,系统分配了不同的值:

multiplier[i] = 0.0917431

总宽度为

(10 * width) + (9 * spacing)
    = (10 * 0.0917431 * 400) + (9 * 2)
    = 384,97
    < stackViewWidth

很明显,这个值是错误的。

因此,系统必须打破约束。当然,它打破了具有最低优先级的约束,即最后排列的子视图项的proportionalConstraint

这就是你的屏幕截图中最后排列的子视图被拉伸的原因。

如果您尝试不同的间距和堆栈视图宽度,您最终会得到各种看起来怪异的布局。但他们都有一个共同点: 间距始终优先。(如果将间距设置为更大的值,例如 30 或 40,您将只能看到前两个或三个排列的子视图,因为其余空间已被所需的间距。)


总结一下:

.fillProportionally 分发版仅适用于 spacing = 0

对于其他间距,系统会使用不正确的乘数创建约束。

这会破坏布局

  • 如果乘数小于应有的值,则必须拉伸已排列的子视图(最后一个)之一
  • 如果乘数大于应有的值,则必须压缩多个排列的子视图。

解决这个问题的唯一方法是“滥用”普通的UIViews,并要求固定宽度约束作为视图之间的间距。 (通常,UILayoutGuides 是为此目的而引入的,但您甚至不能使用它们,因为您无法将布局指南添加到堆栈视图。)

恐怕由于这个错误,没有干净的解决方案可以做到这一点。

【讨论】:

    【解决方案2】:

    至少从 Xcode 9.2 开始,如果初始化器和内在内容大小 被注释掉,则 Playground 提供的工作正常。在这种情况下,即使间距 > 0

    ,比例填充也会按预期工作

    这是一个间距 = 5 的示例

    这似乎是有道理的,因为排列的子视图没有固有的内容大小,并且 StackView 确定它们的宽度以按比例填充指定的轴。

    如果仅启用了初始化程序(而不是内在内容大小覆盖),那么我会得到这个,它与操场上的评论不匹配,所以我猜这个行为在问题发布后一定已经改变了:

    我不理解这种行为,因为在我看来,在使用自动布局时手动设置框架应该被忽略。

    如果仅启用了内在内容大小覆盖(而不是初始化程序),那么我会得到源自这篇文章的有问题的图像(此处间距 = 5):

    本质上,现在的设计是冲突的并且无法实现,因为视图希望是 1 磅宽,由于指定的内在内容大小。这里的总空间应该是

    24 views * 1 point/view + 23 spaces * 5 points/space = 139 < 300 = sv.bounds.width
    

    由于 Mischa 指出的最低优先级,最后排列的视图的约束被打破。

    在逐像素检查时,上面的前 23 个视图虽然比 1 个像素宽,但实际上是 2 或 3 个像素,除了最后一个,因此数学不太匹配,我不知道为什么,可能是小数的四舍五入?

    作为参考,这是在固有内容大小为宽度 5 的情况下的样子,但仍然无法满足约束。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-08-02
      • 2017-02-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多