Go Gio 實戰:煮蛋計時器的實現 04— 佈局
大家好,我是程序員幽鬼。
上篇文章介紹了按鈕,但整個按鈕是佔據屏幕的,這顯然不合適。本文就解決該問題。
01 目標
要解決按鈕的顯示問題,我們引入佈局的概念。本文使用 Flexbox[1] 佈局。
A low button with a spacer below
關於 Flex 佈局的基本概念請參考 mozilla[2]。
02 佈局的整體代碼結構
先忽略細節,看看佈局整體結構的代碼:
case system.FrameEvent:
layout.Flex{
// ...
}.Layout( // ...
// 插入兩個 rigid 元素:
// 第一個放按鈕
layout.Rigid(),
// 這一個放一個空的 spacer
layout.Rigid(),
}
解釋說明
解釋下這段代碼的結構。
-
首先我們通過結構體
layout.Flex{ }
定義一個Flexbox
。 -
然後我們向它增加一個要放置_的子項列表_
Layout(gtx, ...)
。圖形上下文 _gtx_ 包含子項必須遵守的約束,並且任何數量的子項都要遵循。
我們列出的子項都是由 layout.Rigid( )
創建的:第一個是按鈕的佔位符,另一個佔位符,用於包含按鈕下方的空白區域。
什麼是 Rigid[3]?很簡單 - 它的工作是填充給定的空間。Rigid 的子項首先佔據它的部分,而 Flexed[4] 子項佔據剩下的。除此之外,子項按照定義的順序排列。
約束和尺寸(Constraints 和 Dimensions)
在這一點上,我們可以退後一步,看看將所有這些結合在一起的概念,即 Constraints 和 Dimensions。
-
Constraints[5] 表示 widget 的最大和最小大小,即 widget 能多大或多小。
-
Dimensions[6] 表示 widget 的實際大小,即 widget 的實際多大或多小。
父級設置 Constraints,子級響應 Dimensions。父級創建一個小部件並調用Layout()
,小部件用它自己的尺寸響應,有效地佈置自己。好比真實世界中,並非所有孩子都表現得很好,而且孩子們會認爲媽媽或爸爸的一些限制是不公平的 —— 因此需要一些細微差別和協商。但在大多數情況下,就是這樣。約束和尺寸將它們綁定在一起。
正如我們在上面看到的,佈局操作是遞歸的。一個子項本身還可以有子項。佈局本身可以包含佈局。如此下去,你可以從簡單的組件構建複雜的結構。
03 詳細代碼
上面從高層次介紹了整個代碼框架,現在深入細節,看看 system.FrameEvent
部分的代碼:
case system.FrameEvent:
gtx := layout.NewContext(&ops, e)
// flexbox 佈局概念
layout.Flex{
// 從上到下,垂直對齊
Axis: layout.Vertical,
// 開始時(即頂部)留有空白
Spacing: layout.SpaceStart,
}.Layout(gtx,
// 我們插入兩個 rigid 元素:
// 首先是 Button
layout.Rigid(
func(gtx layout.Context) layout.Dimensions {
btn := material.Button(th, &startButton, "Start")
return btn.Layout(gtx)
},
),
// 然後是一個空 spacer
layout.Rigid(
// spacer 的高度爲 25 個設備獨立像素
layout.Spacer{Height: unit.Dp(25)}.Layout,
),
)
e.Frame(gtx.Ops)
代碼註解
在 layout.Flex{}
裏面,我們定義了兩個屬性:
-
Axis(軸):垂直對齊意味各項豎着排列。
-
Spacing(間距):多出來的空間在頂部(上方),注意,這個不是 spacer。
進一步看 layout.Flex
結構體的定義,可以根據 Mozilla 上的文檔對應着學習。
// 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
}
然後是調用 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 到多個 FlexChild。如果獲得 FlexChild 實例呢?這就是 layout.Rigid
函數:
// Rigid returns a Flex child with a maximal constraint of the remaining space.
func Rigid(widget Widget) FlexChild
本例子中,我們傳遞了兩個 FlexChild。
那 Dimensions 又是怎麼定義的呢?它是一個結構體:
// 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
}
上文已經介紹了 Dimensions 的作用,即它負責解析小部件的大小和基線。基線是小部件底部到其包含的任何文本基線的距離(或 0)。其目的是能夠對齊跨多個小部件的文本。
現在就看看對 layout.Rigid( )
的兩個調用:
-
Rigid 接受一個 Widget[7],即小部件
-
小部件只是返回它自己的 Dimensions 信息
-
如何得到小部件並不重要。這裏使用了兩種截然不同的方式:在第一個 Rigid 中,我們傳入一個
func()
,它返回btn.Layout()
,即layout.Dimensions
。在第二個 Rigid 中,我們創建了一個Spacer{}
結構體,調用它的Layout
方法,進而得到 layout.Dimensions -
從父組件的角度來看,這並不重要。只要子項返回 layout.Dimensions 即可。
Button above spacer
這是佈局小部件。但是小部件(widget)到底是什麼?
-
顧名思義,
material.Button
就是一個基於材料設計的 Button[8],我們在上一章詳細介紹過。 -
Spacer[9] 添加空白空間,這裏由 Height 定義的。由於我們已將整體佈局定義爲垂直佈局,多餘的空間應位於頂部,因此它會落到底部並且按鈕位於其頂部。這讓按鈕底部有空白。
從源碼角度,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)、處理事件和計算用戶界面元素尺寸的函數。
因此,我們可以推斷,layout.Spacer 的 Layout 方法簽名符合 Widget 類型:
func (s Spacer) Layout(gtx Context) Dimensions
實際上,各個組件的 Layout 方法都是一個 Widget。
04 小結
要掌握本章的內容,必須先熟悉 Flex。Web 前端開發對此會很熟悉。
爲了方便,附上完整代碼:
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() {
// 創建一個新窗口
w := app.NewWindow(
app.Title("煮蛋計時器"),
app.Size(unit.Dp(400), unit.Dp(600)),
)
// ops 表示 UI 上的操作
var ops op.Ops
// startButton 時候一個可點擊的小部件
var startButton widget.Clickable
// th 定義 material design(材料設計)的風格
th := material.NewTheme(gofont.Collection())
// 循環監聽窗口上的事件
for e := range w.Events() {
// 監聽事件的類型
switch e := e.(type) {
// 當應用程序需要重新渲染是發送該事件
case system.FrameEvent:
gtx := layout.NewContext(&ops, e)
// flexbox 佈局概念
layout.Flex{
// 從上到下,垂直對齊
Axis: layout.Vertical,
// 開始時(即頂部)留有空白
Spacing: layout.SpaceStart,
}.Layout(gtx,
// 我們插入兩個 rigid 元素:
// 首先是 Button
layout.Rigid(
func(gtx layout.Context) layout.Dimensions {
btn := material.Button(th, &startButton, "Start")
return btn.Layout(gtx)
},
),
// 然後是一個空 spacer
layout.Rigid(
// spacer 的高度爲 25 個設備獨立像素
layout.Spacer{Height: unit.Dp(25)}.Layout,
),
)
e.Frame(gtx.Ops)
}
}
}()
app.Main()
}
參考資料
[1]
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/gioui.org@v0.0.0-20210504193539-82fff0178bed/layout#Spacer
歡迎關注「幽鬼」,像她一樣做團隊的核心。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/4IQzkFveURNdqh8w151K7Q