Go Gio 实战:煮蛋计时器的实现 04— 布局
上篇文章介绍了按钮,但整个按钮是占据屏幕的,这显然不合适。本文就解决该问题。
01 目标
要解决按钮的显示问题,我们引入布局的概念。本文使用 Flexbox[1] 布局。
关于 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 即可。
这是布局小部件。但是小部件(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()
}
参考资料
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
往期推荐
我是 polarisxu,北大硕士毕业,曾在 360 等知名互联网公司工作,10多年技术研发与架构经验!2012 年接触 Go 语言并创建了 Go 语言中文网!著有《Go语言编程之旅》、开源图书《Go语言标准库》等。
坚持输出技术(包括 Go、Rust 等技术)、职场心得和创业感悟!欢迎关注「polarisxu」一起成长!也欢迎加我微信好友交流:gopherstudio