当同事问到我这个问题时,我脑子中直接冒出了一个词“弹性盒子”。
问题:
有一个 Cell 中有 4 个并排排列的控件,布局如下图所示:
假设:
1、 这些控件高度和y坐标固定。
2、 蓝色控件x位置固定,但右端对齐于黑色控件。
3、 黑色、红色、绿色控件宽度固定,右端对齐于右侧的控件(绿色控件右对齐于cell 的右边)。
要求:
1、 当黑色、红色、绿色控件中的任意一个控件隐藏时,其余两个控件自动右移占据隐藏控件的控件,蓝色控件则自动布满剩下的宽度。以下是分别隐藏其中一个控件的效果:
2、 依次类推,当隐藏其中任意2个控件和3个控件全都隐藏的效果如下图所示:
如果是 HTML5,这个问题用“弹性盒子”来解决是再合适不过了。但是“弹性盒子”是 CSS 3.0中新增的内容,iOS 并不支持弹性盒子,我们只能自己来解决这个问题。
幸好 iOS 有自动布局,我们可以用自动布局来解决这个问题(当然还需要一点点代码)。
一、 UI 设计
打开故事板,向viewcontrollerz中拖入4个UIView,和3个按钮,如下图所示:
这个4个 UIView 和 3个 UIButton 分别是干什么的,相信你已经能一目了然了。按钮先不管,先看看4个View。
蓝色view的自动布局约束是这样的:
top:24,leading:16,height:24,trailing:10
黑色、红色、绿色 view 的布局约束都是一样的:
width:37,height:24,top:24,trailing:10
四个UIView 分别连接至如下 IBOutlet:
蓝色 v1
黑色 v2
红色 v3
绿色 v4
三个按钮的点击事件则分别连接到三个IBAction:
@IBActionfunc hideGray(sender: AnyObject) {
hide(v2)
}
@IBAction func hideRed(sender: AnyObject) {
hide(v3)
}
@IBAction func hideGreen(sender: AnyObject) {
hide(v4)
}
hide()方法待会介绍。
一、 弹性盒子设计
当黑色、红色、绿色view隐藏时(即hidden 属性为true),自动释放其占据的空间,我们需要让它们的布局约束根据hidden属性进行改变。
从上面我们可以得知,它们的自动布局约束主要是如下几个:
width、height、leading、trailing。
这几个布局跟View所占据的空间有密切关系。其中,height我们不用管,因为它们当width=0 时它们的占据的空间就已经释放了,height值是多少就无关紧要了。
那么也就是说,当view隐藏时,我们让view的width、leading、trailing同时为0,就释放了view所占据的空间。
因此,我们需要在运行时获取width、leading、trailing这三个约束,并根据hidden属性修改它们。那么我们能够在运行时获得View的指定约束吗?答案是肯定的。
我们知道,UIView有一个 constraints()方法,返回一个NSLayoutConstraints数组,包含了其所有的width、height是属于view的constrains,而leading、trailing则是属于superview的。我们可以通过遍历这两个数组来找到我们想要的约束。
我们用一个UIView的扩展来实现这个目的:
extension UIView{
func widthConstraint()->NSLayoutConstraint?{
for constraint in self.constraints() {
let firstItem = constraint.firstItem as? UIView
if firstItem == self && constraint.firstAttribute ==NSLayoutAttribute.Width{
println("I gotit:\(constraint)")
return constraint as?NSLayoutConstraint
}
}
return nil
}
func leadingConstraint()->NSLayoutConstraint?{
if self.superview == nil {
return nil
}
for constraint in self.superview!.constraints() {// 这个约束是在superview 中了
let firstItem = constraint.firstItem as? UIView
let secondItem = constraint.secondItem as? UIView
if firstItem == self && constraint.firstAttribute ==NSLayoutAttribute.Leading{
println("I gotit:\(constraint)")
return constraint as?NSLayoutConstraint
}
}
return nil
}
func trailingConstraint()->NSLayoutConstraint?{
if self.superview == nil {
return nil
}
for constraint in self.superview!.constraints() {// 这个约束是在superview 中了
let firstItem = constraint.firstItem as? UIView
if firstItem == self && constraint.firstAttribute ==NSLayoutAttribute.Trailing{
println("I gotit:\(constraint)")
return constraint as?NSLayoutConstraint
}
}
return nil
}
}
然后我们来设计一个弹性盒子,用来管理这三个View。弹性盒子类的主要目的,是将这些View的三个约束的值保存到一个地方(比如说字典中),然后当某个View的hidden属性设为false时,将约束恢复至原来的值并显示出来。
class FlexibleBox:NSObject{
structViewSpace:Printable{
var widthConstant:CGFloat = 0
var leadConstant:CGFloat = 0
var trailConstant:CGFloat = 0
var description: String {
return "width-\(widthConstant)\nleading -
\(leadConstant)\ntrailing- \(trailConstant)"
}
}
var cachedConstraints = [UIView:ViewSpace]()
func addViews(views:[UIView]){
for view in views {
addView(view)
}
}
func addView(v:UIView){
var space = ViewSpace()
if let constraint = v.trailingConstraint() {
space.trailConstant = constraint.constant
}
if let constraint = v.leadingConstraint() {
space.leadConstant = constraint.constant
}
if let constraint = v.widthConstraint() {
space.widthConstant = constraint.constant
}
cachedConstraints[v]=space
println("\(space)")
}
func freeViewSpace(v:UIView){
v.widthConstraint()?.constant = 0
v.leadingConstraint()?.constant = 0
v.trailingConstraint()?.constant = 0
}
func resumeViewSpace(v:UIView){
let space = cachedConstraints[v] ?? ViewSpace()
v.trailingConstraint()?.constant = space.trailConstant
v.leadingConstraint()?.constant = space.leadConstant
v.widthConstraint()?.constant = space.widthConstant
}
deinit{
cachedConstraints.removeAll(keepCapacity: false)
}
}
二、 使用弹性盒子
在View Controller 中声明一个弹性盒子:
let flexBox = FlexibleBox()
然后在viewDidLoad方法中:
flexBox.addViews([v2,v3,v4])
然后但点击按钮时,调用如下方法隐藏(或取消隐藏)一个View:
func toggleViewHiddenStatus(v:UIView){
if v.hidden == false {
flexBox.freeViewSpace(v)
}else{
flexBox.resumeViewSpace(v)
}
v.hidden = !v.hidden
self.view.setNeedsLayout()
}
最后一句self.view.setNeedsLayout()将导致所有自动布局约束被重新计算。