YogaKit中 position 的使用方法

3,526 阅读6分钟

这里有一篇关于YogaKit使用方法的翻译文章,其中介绍的比较全面,基本可以使用FlexBox布局,但是仅仅看这里的介绍还是难以解决一些布局问题,本篇文章不再介绍 FlexBoxYogaKit 的相关知识和概念,直奔主题实践,旨在通过案例帮助更好的解决问题。

在学习FlexBox布局的时候,为了加深理解建议学习一下CSS,理解盒子模型、外边距、内边距、定位、定位上下文及相对定位和绝对定位的区别,能够帮助我们更好的理解FlexBox,更好的使用YogaKit。

案例一:

如下图,这种布局很常见,实现的方法也有很多,使用YogaKit实现也有很多方法,但是优雅程度不同。

首先,使用 preservingOrigin 可以勉强实现这一布局,但是代码显得不规整,作为一种布局方法,我直接摒弃。

let blueView = UIView(frame: .zero)
blueView.configureLayout { (layout) in
	layout.isEnabled = true
	layout.flexGrow = 1
	layout.marginTop = 60
}
view.addSubview(blueView)

let imageView = UIImageView(frame: .zero)
imageView.configureLayout { (layout) in
	layout.isEnabled = true
	layout.height = 100
	layout.marginTop = 10
}
view.addSubview(imageView)

view.yoga.applyLayout(preservingOrigin: true)
imageView.yoga.applyLayout(preservingOrigin: false)

其次,使用 margin 属性,将其赋负值也可以轻易实现此效果,然而这些都不是重点

let blueView = UIView(frame: .zero)
blueView.configureLayout { (layout) in
	layout.isEnabled = true
	layout.flexGrow = 1
	layout.marginTop = 60
}
view.addSubview(blueView)

let imageView = UIImageView(frame: .zero)
imageView.configureLayout { (layout) in
    layout.isEnabled = true
    layout.height = 100
    layout.aspectRatio = 1                  
    layout.marginTop = -50
    layout.alignSelf = .center                   
}
blueView.addSubview(imageView)
view.yoga.applyLayout(preservingOrigin: true)

问题1:YogaKit是布局流式布局的利器,在使用的时候会发现整个布局都是根据内容依次堆叠,在如图的布局样式中,如果要把 imageView 放到蓝色背景的底部,此时就无法使用 marginBottom 来完成。

解决这个问题首先想到的是通过调整父视图的内容布局属性来完成。

let blueView = UIView(frame: .zero)
blueView.backgroundColor = .blue
blueView.configureLayout { (layout) in
    layout.isEnabled = true
    layout.flexGrow = 1
    layout.marginTop = 60
    layout.justifyContent = .flexEnd
}
view.addSubview(blueView)

问题2:如果仅仅通过修改 layout.justifyContent = .flexEnd 来解决此问题就会引起新的问题,就是说会影响蓝色View中的其他内容,所有内容都以底部为起点开始布局,故此方案有部分影响,根据情况采纳。

其次的解决方法可以是用定位来处理,只能说不要太完美。

let blueView = UIView(frame: .zero)
blueView.backgroundColor = .blue
blueView.configureLayout { (layout) in
    layout.isEnabled = true
    layout.flexGrow = 1
    layout.marginTop = 60
    layout.position = .relative
}
view.addSubview(blueView)

let image = UIImageView(frame: .zero)
image.backgroundColor = .yellow
image.configureLayout { (layout) in
    layout.isEnabled = true
    layout.height = 100
    layout.bottom = 0
    layout.aspectRatio = 1
    layout.alignSelf = .center
    layout.position = .absolute
}
blueView.addSubview(image)
view.yoga.applyLayout(preservingOrigin: true)

案例二:

在项目中可能会遇到这种布局,使用YogaKit中的FlexWrap能更方便的解决item(图中圆角方块)换行问题,但是也存在一些问题,如图所示,为了便于说明做以下命名,图中深色圆角方块为 item,图中图片为 image,图中包裹 item 的边框为 wrapper,整体为 cell

在此中样式的 cell 中,我们希望 image 按比例位于 cell 的最右边,所以 wrapperflexGrow 设为 1,可以将 image 挤到最右边;wrapper 中的 item 要换行,所以 wrapperflexWrap 设为 wrap,核心代码如下:

cell.configureLayout { (layout) in
    layout.isEnabled = true
    layout.flexDirection = .row
}

let wrapper = UIView(frame: .zero)
wrapper.configureLayout { (layout) in
    layout.isEnabled = true
    layout.flexGrow = 1 
    layout.flexDirection = .row                       
}
cell.addSubview()

let image = UIImageView(frame: .zero)
image.configureLayout { (layout) in
  layout.isEnabled = true
  layout.width = 140
  layout.aspectRatio = 1
}
cell.addSubview(image)

for _ in 0..<8 {
    let item = UILabel(frame: .zero)
    item.configureLayout { (layout) in
  	    layout.isEnabled = true
  	    layout.marginHorizontal = 8
  	    layout.marginVertical = 5
  	    layout.width = 60
  	    layout.height = 30
  }
  wrapper.addSubview(item)
}

问题1:仅仅如此是不够的,运行会看到 image 被挤出屏幕外边,而且 item 们也没有折行,显然 wrapper 被内容撑开,未达到预期样式。

子视图会影响俯视图的大小,使用 flexWrap 属性可以让子视图折行,但是前提是要给父视图一个明确的宽度。

let maxWidth = YGValue(view.bounds.size.width - 140)
wrapper.configureLayout { (layout) in
    layout.isEnabled = true
    layout.flexGrow = 1 
    layout.flexDirection = .row   
    layout.width = maxWidth
}

问题2:在给定 wrapper 一个宽度后,貌似可以完美解决问题,但是前提是需要计算宽度,不够优雅,在一个宽度不固定的容器内显然不能使用此方法。

要解决此问题就要用到 position 属性,position 有两个值:.relative 相对定位 和 .absolute 绝对定位,相对定位可以作为绝对定位的定位上下文,决定绝对定位的参照物,如果没有定位上下文默认参照物为根视图。使用绝对定位使得该视图脱离布局流,位置相对于父视图,不会影响父视图大小。在 wrapper 中再添加一个 rapWrapper 来承载 item

let wrapper = UIView(frame: .zero)
wrapper.configureLayout { (layout) in
	layout.isEnabled = true
	layout.flexGrow = 1
	layout.position = .relative
}
cell.addSubview(wrapper)

let rapWrapper = UIView(frame: .zero)
rapWrapper.backgroundColor = .gray
rapWrapper.configureLayout { (layout) in
	layout.isEnabled = true
	layout.flexWrap = .wrap
	layout.flexDirection = .row
	layout.position = .absolute
}
wrapper.addSubview(rapWrapper)
for _ in 0..<8 {
    let item = UILabel(frame: .zero)
    item.configureLayout { (layout) in
        layout.isEnabled = true
        layout.marginHorizontal = 8
        layout.marginVertical = 5
        layout.width = 60
        layout.height = 30
  }
  rapWrapper.addSubview(item)
}

案例三:

同样是简单的布局,使用YogaKit也很简单,然而简单是要付出代价的,正确的使用才能避免不必要的尴尬😓

看上去如此简单的布局,用YogaKit的时候会感受到它的"魔性",前提是要正确的使用。

首先想到的布局方法是 scrollView 使用 flexGrow 充满,将 button 挤到底部,而 scrollViewcontentSize 也不用计算了,在内部加一个 contentView 负责填充内容,撑开 scrollView 即可,简单到堪称完美。

let scrollView = UIScrollView(frame: .zero)
scrollView.backgroundColor = .blue
scrollView.configureLayout { (layout) in
    layout.isEnabled = true
    layout.flexGrow = 1
}
view.addSubview(scrollView)

let contentView = UIView(frame: .zero)
contentView.configureLayout { (layout) in
    layout.isEnabled = true
    layout.height = 300
}
scrollView.addSubview(contentView)

let button = UIView(frame: .zero)
button.backgroundColor = .yellow
button.configureLayout { (layout) in
    layout.isEnabled = true
    layout.height = 50
}
view.addSubview(button)
view.yoga.applyLayout(preservingOrigin: true)
scrollView.contentSize.height = contentView.bounds.size.height

问题1:看似很简单的布局,也为以后出错埋下了伏笔,当 contentView 的高度小时还看不出问题,但是当 contentView 的高度大于屏幕高度时,问题出现了,scrollView 不能滑动,button 也被挤到了屏幕外面。

contentView.configureLayout { (layout) in
    layout.isEnabled = true
    layout.height = 900
}

如此,解决办法也就显而易见了,没错,使用定位解决问题。

let scrollView = UIScrollView(frame: .zero)
scrollView.backgroundColor = .blue
scrollView.configureLayout { (layout) in
    layout.isEnabled = true
    layout.flexGrow = 1
    layout.position = .relative
}
view.addSubview(scrollView)

let contentView = UIView(frame: .zero)
contentView.configureLayout { (layout) in
    layout.isEnabled = true
    layout.height = 900
    layout.position = .absolute
}
scrollView.addSubview(contentView)

let button = UIView(frame: .zero)
button.backgroundColor = .yellow
button.configureLayout { (layout) in
    layout.isEnabled = true
    layout.height = 50
}
view.addSubview(button)
view.yoga.applyLayout(preservingOrigin: true)
scrollView.contentSize.height = contentView.bounds.size.height

更新:

感谢 yun1467723561418 这位好心朋友的指点,案例二和案例三中避免子视图撑大父视图用 flexShrink 同样可以解决,如下:

案例二代码修改:

let wrapper = UIView(frame: .zero)
wrapper.configureLayout { (layout) in
	layout.isEnabled = true
	layout.flexGrow = 1
	layout.flexShrink = 1
}
cell.addSubview(wrapper)
for _ in 0..<8 {
    let item = UILabel(frame: .zero)
    item.configureLayout { (layout) in
        layout.isEnabled = true
        layout.marginHorizontal = 8
        layout.marginVertical = 5
        layout.width = 60
        layout.height = 30
  }
  wrapper.addSubview(item)
}

案例三代码修改:

let scrollView = UIScrollView(frame: .zero)
scrollView.backgroundColor = .blue
scrollView.configureLayout { (layout) in
    layout.isEnabled = true
    layout.flexGrow = 1
    layout.flexShrink = 1
}
view.addSubview(scrollView)

let contentView = UIView(frame: .zero)
contentView.configureLayout { (layout) in
    layout.isEnabled = true
    layout.height = 900
}
scrollView.addSubview(contentView)

let button = UIView(frame: .zero)
button.backgroundColor = .yellow
button.configureLayout { (layout) in
    layout.isEnabled = true
    layout.height = 50
}
view.addSubview(button)
view.yoga.applyLayout(preservingOrigin: true)
scrollView.contentSize.height = contentView.bounds.size.height

关于 flexShrink

flexShrink 允许内容超出时缩小,标记内容超出时谁先缩小,优先级从根视图依次往下,如:A包含B,B包含C,在没有限制A的大小时A可能被内容撑开,设置B或者C的 flexShrink 是不起作用的,但设置A的 flexShrink可以保证B或C不会超出A的大小

末尾:

不知道使用YogaKit的同学有多少,也没有看到有介绍 position 的文章,我也是通过阅读了「CSS权威指南」后才尝试使用 position 的,同时对于 overflow 的使用还是不得而知,希望有懂的大神告诉我一下!有写的不对的地方,还请慷慨指出!