這裡有一篇關於YogaKit使用方法的翻譯文章,其中介紹的比較全面,基本可以使用FlexBox佈局,但是僅僅看這裡的介紹還是難以解決一些佈局問題,本篇文章不再介紹 FlexBox 和 YogaKit 的相關知識和概念,直奔主題實踐,旨在通過案例幫助更好的解決問題。
在學習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 = .blueblueView.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 = .blueblueView.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 = .yellowimage.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 的最右邊,所以 wrapper 的 flexGrow 設為 1,可以將 image 擠到最右邊;wrapper 中的 item 要換行,所以 wrapper 的 flexWrap 設為 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 = .grayrapWrapper.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 擠到底部,而 scrollView 的 contentSize 也不用計算了,在內部加一個 contentView 負責填充內容,撐開 scrollView 即可,簡單到堪稱完美。
let scrollView = UIScrollView(frame: .zero)scrollView.backgroundColor = .bluescrollView.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 = .yellowbutton.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 = .bluescrollView.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 = .yellowbutton.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 = .bluescrollView.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 = .yellowbutton.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 的使用還是不得而知,希望有懂的大神告訴我一下!有寫的不對的地方,還請慷慨指出!