重学AutoLayout (1) -- Intrinsic Content Size

重学 AutoLayout (1) -- Intrinsic Content Size

最近闲来无事, 把AutoLayout的内容整理整理.

关键的内容 - 划重点

  1. 控件的IntrinsicContentSize大小
  2. CHCR是解决冲突的钥匙
  3. Constrain约束都有Priority
  4. systemLayoutSizeFitting是约束和Frame的纽带

1. Anchor 和 Constrain

iOS 的AutoLayout 是基于cassowary算法, 在开发人员设置好各个view的约束信息以后, NSISEnginer会计算出每个view的frame. 最终布局使用的view的frame进行布局.

注意 AutoLayout 与 AutoSizeMask 不能共存, 如果需要对某个View使用AutoLayout, 需要优先使用view.translatesAutoresizingMaskIntoConstraints = false

在我们使用Anchor去进行界面的约束布局时, 常常会碰到结果现象和预期不符, 甚至约束冲突的问题.

例如, 我们的一个 UILabelUITextFiled在一条线上, 我们预期UITextFiled被拉伸, 但是结果是UILabel被拉伸了, Demo代码如下:

func setupSubViews() {
    let label = makeLabel(withText: "Name")
    let textFiled = makeTextFiled(withPlaceHolderText: "Enter name here")
    view.addSubview(label)
    view.addSubview(textFiled)
    
    NSLayoutConstraint.activate([
        // label anchor
        label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8),
        label.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8),
        label.rightAnchor.constraint(equalTo: textFiled.leftAnchor, constant: -8),
        // textFiled anchor
        textFiled.topAnchor.constraint(equalTo: label.topAnchor),
        textFiled.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -8),
    ])
}

func makeLabel(withText text: String) -> UILabel {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = text
        label.backgroundColor = .yellow
        return label
    }
    
func makeTextFiled(withPlaceHolderText text: String) -> UITextField {
        let label = UITextField()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = text
        label.backgroundColor = .lightGray
        return label
    }
复制代码

image.png

why??? 本文就是来解决这里的疑惑的!!!

2. IntrinsicContentSize

在使用AutoLayout时, 我们对 View的IntrinsicContentSize 的理解非常关键.

我们需要时刻牢记, 每个View都有自然大小, 也就这个View如果没有外部约束情况下, 它的CGSize是多少!!!

对于系统的UI控件来说他们都有自己的IntrinsicContentSize大小. 也就是说大部分的UIKit控件都能size themselves, 下面是一些常见的控件的IntrinsicContentSize 信息

  1. UISwitch - (49, 31)
  2. UIActivityIdicator - Small: (20, 20) Big:(37, 37)
  3. UIButton - 内部的Label的size + padding
  4. UILabel - The size that fits its text!!! (这是在label的宽度没有约束的情况下!!! 后面有特殊)
  5. UIImageView: - The size of the image(如果没有图像, 那么是(0, 0))
  6. UIView - has no intrinsic conent size (或者说是 (-1, -1))

注意, 从上面的概念来说 1~5 都是拥有IntrinsicContentSize的, 也就是它们可以根据自己的内容size themselves, 而原生的UIView不行的.

2.1 UIView

UIView 在默认情况下IntrinsicContentSize(-1, -1), 这表示UIView默认情况下是没有自然大小的, 或者自然大小是(UIView.noIntrinsicMetric, UIView.noIntrinsicMetric)

除了直接设置widthAnchorheightAnchor强制约束UIView的大小, 另外一种方式就是自定义继承, 重写该方法:

class BigView: UIView {
    // 拥有自然大小的 UIView
    override var intrinsicContentSize: CGSize{
        return CGSize(width: 200, height: 100)
    }
}

class MyView: UIView {
    // 默认的自然大小是 (-1, -1), 是无法参与 AutoLayout 计算的!!!
    override var intrinsicContentSize: CGSize{
        let size = super.intrinsicContentSize
        return size
    }
}
复制代码

如果后续要使用MyView, 就能像UIlabel那样能拥有自己的固有尺寸!

2.2 UILabel

class MyLabel: UILabel {
    override var intrinsicContentSize: CGSize {
        let size1 = super.intrinsicContentSize
        let size2 = sizeThatFits(UIView.layoutFittingCompressedSize)
        print("UILabel intrinsicContentSize: \(size1)")
        print("UILabel sizeThatFits: \(size2)")
        return size1
    }
}
复制代码
  1. label.text = "", 此时没有内容, 打印结果如下:
UILabel intrinsicContentSize: (0.0, 0.0)
UILabel sizeThatFits: (0.0, 0.0)
复制代码
  1. label.text = "123123123131313232332"
UILabel intrinsicContentSize: (196.0, 20.333333333333332)
UILabel sizeThatFits: (196.0, 20.333333333333332)
复制代码
  1. label.text = "123123123131313232332", 并且label.preferredMaxLayoutWidth = 100, label.numberOfLines = 0:
UILabel intrinsicContentSize: (96.0, 61.0)
UILabel sizeThatFits: (196.0, 20.333333333333332)
复制代码

在以上的基础上:

let myLabel = MyLabel()
myLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myLabel)
myLabel.text = "123123123131313232332"
myLabel.preferredMaxLayoutWidth = 100
myLabel.numberOfLines = 0

let layoutSize =  myLabel.systemLayoutSizeFitting(CGSize(width: 100, height: UIView.noIntrinsicMetric))
print("layoutSize: \(layoutSize)")
复制代码

能看到输出结果:

layoutSize: (96.0, 61.0)
UILabel intrinsicContentSize: (96.0, 61.0)
UILabel sizeThatFits: (196.0, 20.333333333333332)
复制代码

注意:

  1. preferredMaxLayoutWidth 和 numberOfLines 需要一起使用才会生效!
  2. preferredMaxLayoutWidth 可以简单理解成, 在text文本长度很长时, label的 intrinsicContent 的 intrinsic width 的估算值
  3. sizeThatFits 方法同 systemLayoutSizeFitting

2.3 ImageView

ImageView 同UIlabel类似, 在没有设置image属性时, 自然大小为(0, 0), 当设置了image以后, 自然大小为图像的size

3. CHCR与约束优先级

假设一个自定义Label的Intrinsic content size(width: 50 ,height: 20).

(50, 20) 表示 Label的自然大小!!! 但是这仅仅是Label自己的期望大小, 是 Optional的!!!, 如果有额外的约束使得无法满足的这个期望怎么办??? 这里就需要理解CHCR: CH 表示是Content Hugging; CR 表示Content Compression Resistance.

  1. Content Hugging: label.width <= 50, priority = defaultLow(250)
  2. Content Compression Resistance: label.width >= 50, priority = defaultHight(750)

简单来说, Intrinsic Size 中的 width 就是给NSISEnginer输入了两个width关联的带有指定优先级的约束条件!!! (height 类似)

而我们自己创建的Anchor Constrain默认优先级都是priority = required(1000)

因此, 只要我们常规方式手动设置一个水平方向约束给label, 那么就会打破它的CHCH的约束条件, 而常见的设置label的 width 约束有两种:

  1. label.leftAnchor + label.rightAnchor
  2. label.widthAnchor

再来看看文章开头的问题!!!

如果我们需要Label不要被拉伸, 那么需要修改label.CH 的 priority > textFiled.CH 的 priority, 因此我们只需要增加一句话即可:

// 方法1: 水平方向上: label.chp 251 > textField.chp 250, 这样水平约束上, TextFiled会被拉伸!!!
label.setContentHuggingPriority(UILayoutPriority(rawValue: 251), for: .horizontal)
// 方法2: 水平方向上: label.chp 250 > textFiled.chp 249 与上面类似
textFiled.setContentHuggingPriority(UILayoutPriority(rawValue: 249), for: .horizontal)
复制代码

因此AutoLayout小结一下:

  1. 关键概念: Intrinsic SizeOptional, 只是控件的建议, 可以被打破!!!
  2. 问题转化: 约束 anbiguous 或者 约束冲突, 要转化成约束冲突方向各个View的CHCR的Priority的问题!!!
  3. 解决方式: 修改CHCR Priority. 具体来说: CH表示 <=, 并priority = 250; CR表示>=, priority = 750; 如果自己不想被拉伸, 那么增加CHP, 如果自己不想被压缩, 增加CRP
  4. 与Frame关系: systemLayoutSizeFittingtranslatesAutoresizingMaskIntoConstraints
  5. 特殊注意: 带有preferredMaxLayoutWidth属性的控件
  1. CHP/CRP 表示 CH Priority 和 CR Priority

  2. >= 表示最小大小; <= 表示最大大小

  3. Intrinsic Width, 表示 最小大小是width, 最大大小也是 width, 只是Priority不同

4 常见的 CHCR 的配置

func makeImageView(named: String) -> UIImageView {
    let view = UIImageView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.contentMode = .scaleToFit
    view.image = UIImage(named: named)

    // 图像抗压缩/抗拉伸的经验配置 
    // 允许图像在垂直方向上适配: 被拉伸 or 被压缩
    // By making the image hug itself a little bit less and less resistant to being compressed
    // we allow the image to stretch and grow as required
    view.setContentHuggingPriority(UILayoutPriority(rawValue: 249), for: .vertical)
    view.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 749), for: .vertical)

    return view
}
复制代码

针对 UILabel:

func makeLabel(withText text: String) -> UILabel {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.text = text
    label.backgroundColor = .yellow
    // 文本一般需要更加紧凑!!!
    label.preferredMaxLayoutWidth = 100
    
    // 开发经验: 与 XIB 一样, 设置CH为251
    label.setContentHuggingPriority(UILayoutPriority(rawValue: 251), for: .horizontal)
    label.setContentHuggingPriority(UILayoutPriority(rawValue: 251), for: .vertical)
    
    return label
}
复制代码

XIB 或者 Storyboard中, UILabel的CHP是251, UIImageView的CRP是749!!!

其他apple 官方的建议:

  • When stretching a series of views to fill a space, if all the views have an identical content-hugging priority, the layout is ambiguous. Auto Layout doesn’t know which view should be stretched.

    A common example is a label and text field pair. Typically, you want the text field to stretch to fill the extra space while the label remains at its intrinsic content size. To ensure this, make sure the text field’s horizontal content-hugging priority is lower than the label’s.

    In fact, this example is so common that Interface Builder automatically handles it for you, setting the content-hugging priority for all labels to 251. If you are programmatically creating the layout, you need to modify the content-hugging priority yourself.

  • Odd and unexpected layouts often occur when views with invisible backgrounds (like buttons or labels) are accidentally stretched beyond their intrinsic content size. The actual problem may not be obvious, because the text simply appears in the wrong location. To prevent unwanted stretching, increase the content-hugging priority.

  • Baseline constraints work only with views that are at their intrinsic content height. If a view is vertically stretched or compressed, the baseline constraints no longer align properly.

  • Some views, like switches, should always be displayed at their intrinsic content size. Increase their CHCR priorities as needed to prevent stretching or compressing.

  • Avoid giving views required CHCR priorities. It’s usually better for a view to be the wrong size than for it to accidentally create a conflict. If a view should always be its intrinsic content size, consider using a very high priority (999) instead. This approach generally keeps the view from being stretched or compressed but still provides an emergency pressure valve, just in case your view is displayed in an environment that is bigger or smaller than you expected.

参考

  1. github.com/forkingdog/…
  2. developer.apple.com/library/arc…
  3. constraints.cs.washington.edu/solvers/cas…
  4. github.com/SnapKit/Sna…
  5. github.com/layoutBox/P…

猜你喜欢

转载自juejin.im/post/7079426268470444062
今日推荐