Go Gio 實(shí)戰(zhàn):煮蛋計(jì)時(shí)器的實(shí)現(xiàn) 04— 布局
上篇文章介紹了按鈕,但整個(gè)按鈕是占據(jù)屏幕的,這顯然不合適。本文就解決該問題。
01 目標(biāo)
要解決按鈕的顯示問題,我們引入布局的概念。本文使用 Flexbox[1] 布局。

關(guān)于 Flex 布局的基本概念請(qǐng)參考 mozilla[2]。
02 布局的整體代碼結(jié)構(gòu)
先忽略細(xì)節(jié),看看布局整體結(jié)構(gòu)的代碼:
case?system.FrameEvent:
????layout.Flex{
????//?...
????}.Layout(?//?...
????????//?插入兩個(gè)?rigid?元素:
????????//?第一個(gè)放按鈕
????????layout.Rigid(),
????????//?這一個(gè)放一個(gè)空的?spacer
????????layout.Rigid(),
????}
解釋說明
解釋下這段代碼的結(jié)構(gòu)。
首先我們通過結(jié)構(gòu)體 layout.Flex{ }定義一個(gè) ?Flexbox。然后我們向它增加一個(gè)要放置的子項(xiàng)列表 Layout(gtx, ...)。圖形上下文 gtx 包含子項(xiàng)必須遵守的約束,并且任何數(shù)量的子項(xiàng)都要遵循。
我們列出的子項(xiàng)都是由 layout.Rigid( ) 創(chuàng)建的:第一個(gè)是按鈕的占位符,另一個(gè)占位符,用于包含按鈕下方的空白區(qū)域。
什么是 Rigid[3]?很簡(jiǎn)單 - 它的工作是填充給定的空間。Rigid 的子項(xiàng)首先占據(jù)它的部分,而 Flexed[4] 子項(xiàng)占據(jù)剩下的。除此之外,子項(xiàng)按照定義的順序排列。
約束和尺寸(Constraints 和 Dimensions)
在這一點(diǎn)上,我們可以退后一步,看看將所有這些結(jié)合在一起的概念,即 Constraints 和 Dimensions。
Constraints[5] 表示 widget 的最大和最小大小,即 widget 能多大或多小。 Dimensions[6] 表示 widget 的實(shí)際大小,即 widget 的實(shí)際多大或多小。
父級(jí)設(shè)置 Constraints,子級(jí)響應(yīng) Dimensions。父級(jí)創(chuàng)建一個(gè)小部件并調(diào)用Layout(),小部件用它自己的尺寸響應(yīng),有效地布置自己。好比真實(shí)世界中,并非所有孩子都表現(xiàn)得很好,而且孩子們會(huì)認(rèn)為媽媽或爸爸的一些限制是不公平的 —— 因此需要一些細(xì)微差別和協(xié)商。但在大多數(shù)情況下,就是這樣。約束和尺寸將它們綁定在一起。
正如我們?cè)谏厦婵吹降模季植僮魇沁f歸的。一個(gè)子項(xiàng)本身還可以有子項(xiàng)。布局本身可以包含布局。如此下去,你可以從簡(jiǎn)單的組件構(gòu)建復(fù)雜的結(jié)構(gòu)。
03 詳細(xì)代碼
上面從高層次介紹了整個(gè)代碼框架,現(xiàn)在深入細(xì)節(jié),看看 ?system.FrameEvent 部分的代碼:
case?system.FrameEvent:
????gtx?:=?layout.NewContext(&ops,?e)
??//?flexbox?布局概念
????layout.Flex{
???????//?從上到下,垂直對(duì)齊
????????Axis:?layout.Vertical,
???????//?開始時(shí)(即頂部)留有空白
????????Spacing:?layout.SpaceStart,
????}.Layout(gtx,
????????//?我們插入兩個(gè) rigid 元素:
????????//?首先是?Button
????????layout.Rigid(
????????????func(gtx?layout.Context)?layout.Dimensions?{
????????????????btn?:=?material.Button(th,?&startButton,?"Start")
????????????????return?btn.Layout(gtx)
????????????},
????????),
????????//?然后是一個(gè)空?spacer
????????layout.Rigid(
????????????//?spacer?的高度為?25?個(gè)設(shè)備獨(dú)立像素
????????????layout.Spacer{Height:?unit.Dp(25)}.Layout,
????????),
????)
????e.Frame(gtx.Ops)
代碼注解
在 layout.Flex{} 里面,我們定義了兩個(gè)屬性:
Axis(軸):垂直對(duì)齊意味各項(xiàng)豎著排列。 Spacing(間距):多出來的空間在頂部(上方),注意,這個(gè)不是 spacer。
進(jìn)一步看 layout.Flex 結(jié)構(gòu)體的定義,可以根據(jù) Mozilla 上的文檔對(duì)應(yīng)著學(xué)習(xí)。
//?Flex?lays?out?child?elements?along?an?axis,
//?according?to?alignment?and?weights.
type?Flex?struct?{
?//?Axis?is?the?main?axis,?either?Horizontal?or?Vertical.
?Axis?Axis
?//?Spacing?controls?the?distribution?of?space?left?after
?//?layout.
?Spacing?Spacing
?//?Alignment?is?the?alignment?in?the?cross?axis.
?Alignment?Alignment
?//?WeightSum?is?the?sum?of?weights?used?for?the?weighted
?//?size?of?Flexed?children.?If?WeightSum?is?zero,?the?sum
?//?of?all?Flexed?weights?is?used.
?WeightSum?float32
}
然后是調(diào)用 Flex 的 Layout 方法。該方法的簽名如下:
//?Layout?a?list?of?children.?The?position?of?the?children?are
//?determined?by?the?specified?order,?but?Rigid?children?are?laid?out
//?before?Flexed?children.
func?(f?Flex)?Layout(gtx?Context,?children?...FlexChild)?Dimensions
接收 0 到多個(gè) FlexChild。如果獲得 FlexChild 實(shí)例呢?這就是 layout.Rigid 函數(shù):
//?Rigid?returns?a?Flex?child?with?a?maximal?constraint?of?the?remaining?space.
func?Rigid(widget?Widget)?FlexChild
本例子中,我們傳遞了兩個(gè) FlexChild。
那 Dimensions 又是怎么定義的呢?它是一個(gè)結(jié)構(gòu)體:
//?Dimensions?are?the?resolved?size?and?baseline?for?a?widget.
//
//?Baseline?is?the?distance?from?the?bottom?of?a?widget?to?the?baseline?of
//?any?text?it?contains?(or?0).?The?purpose?is?to?be?able?to?align?text
//?that?span multiple?widgets.
type?Dimensions?struct?{
?Size?????image.Point
?Baseline?int
}
上文已經(jīng)介紹了 Dimensions 的作用,即它負(fù)責(zé)解析小部件的大小和基線。基線是小部件底部到其包含的任何文本基線的距離(或 0)。其目的是能夠?qū)R跨多個(gè)小部件的文本。
現(xiàn)在就看看對(duì) layout.Rigid( ) 的兩個(gè)調(diào)用:
Rigid 接受一個(gè)Widget[7],即小部件 小部件只是返回它自己的 Dimensions 信息 如何得到小部件并不重要。這里使用了兩種截然不同的方式:在第一個(gè) Rigid 中,我們傳入一個(gè) func(),它返回btn.Layout(),即layout.Dimensions。在第二個(gè) Rigid 中,我們創(chuàng)建了一個(gè)Spacer{}結(jié)構(gòu)體,調(diào)用它的Layout方法,進(jìn)而得到 layout.Dimensions從父組件的角度來看,這并不重要。只要子項(xiàng)返回 layout.Dimensions 即可。

這是布局小部件。但是小部件(widget)到底是什么?
顧名思義, material.Button就是一個(gè)基于材料設(shè)計(jì)的 Button[8],我們?cè)谏弦徽略敿?xì)介紹過。Spacer[9] 添加空白空間,這里由 Height 定義的。由于我們已將整體布局定義為垂直布局,多余的空間應(yīng)位于頂部,因此它會(huì)落到底部并且按鈕位于其頂部。這讓按鈕底部有空白。
從源碼角度,Widget 的定義如下:
//?Widget?is?a?function?scope?for?drawing,?processing?events?and
//?computing?dimensions?for?a?user?interface?element.
type?Widget?func(gtx?Context)?Dimensions
即 Widget 是用于繪圖(drawing)、處理事件和計(jì)算用戶界面元素尺寸的函數(shù)。
因此,我們可以推斷,layout.Spacer 的 Layout 方法簽名符合 Widget 類型:
func?(s?Spacer)?Layout(gtx?Context)?Dimensions
實(shí)際上,各個(gè)組件的 Layout 方法都是一個(gè) Widget。
04 小結(jié)
要掌握本章的內(nèi)容,必須先熟悉 Flex。Web 前端開發(fā)對(duì)此會(huì)很熟悉。
為了方便,附上完整代碼:
package?main
import?(
?"gioui.org/app"
?"gioui.org/font/gofont"
?"gioui.org/io/system"
?"gioui.org/layout"
?"gioui.org/op"
?"gioui.org/unit"
?"gioui.org/widget"
?"gioui.org/widget/material"
)
func?main()?{
?go?func()?{
??//?創(chuàng)建一個(gè)新窗口
??w?:=?app.NewWindow(
???app.Title("煮蛋計(jì)時(shí)器"),
???app.Size(unit.Dp(400),?unit.Dp(600)),
??)
??//?ops?表示?UI?上的操作
??var?ops?op.Ops
??//?startButton?時(shí)候一個(gè)可點(diǎn)擊的小部件
??var?startButton?widget.Clickable
??//?th?定義?material?design(材料設(shè)計(jì))的風(fēng)格
??th?:=?material.NewTheme(gofont.Collection())
??//?循環(huán)監(jiān)聽窗口上的事件
??for?e?:=?range?w.Events()?{
???//?監(jiān)聽事件的類型
???switch?e?:=?e.(type)?{
???//?當(dāng)應(yīng)用程序需要重新渲染是發(fā)送該事件
???case?system.FrameEvent:
????gtx?:=?layout.NewContext(&ops,?e)
????//?flexbox?布局概念
????layout.Flex{
?????//?從上到下,垂直對(duì)齊
?????Axis:?layout.Vertical,
?????//?開始時(shí)(即頂部)留有空白
?????Spacing:?layout.SpaceStart,
????}.Layout(gtx,
?????//?我們插入兩個(gè) rigid 元素:
?????//?首先是?Button
?????layout.Rigid(
??????func(gtx?layout.Context)?layout.Dimensions?{
???????btn?:=?material.Button(th,?&startButton,?"Start")
???????return?btn.Layout(gtx)
??????},
?????),
?????//?然后是一個(gè)空?spacer
?????layout.Rigid(
??????//?spacer?的高度為?25?個(gè)設(shè)備獨(dú)立像素
??????layout.Spacer{Height:?unit.Dp(25)}.Layout,
?????),
????)
????e.Frame(gtx.Ops)
???}
??}
?}()
?app.Main()
}
參考資料
Flexbox: https://pkg.go.dev/gioui.org/layout#Flex
[2]mozilla: https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox
[3]Rigid: https://pkg.go.dev/gioui.org/layout#Rigid
[4]Flexed: https://pkg.go.dev/gioui.org/layout#Flexed
[5]Constraints: https://pkg.go.dev/gioui.org/layout#Constraints
[6]Dimensions: https://pkg.go.dev/gioui.org/layout#Dimensions
[7]Widget: https://pkg.go.dev/gioui.org/layout#Widget
[8]Button: https://pkg.go.dev/gioui.org/widget/material#Button
[9]Spacer: https://pkg.go.dev/[email protected]/layout#Spacer
推薦閱讀
