Go Gio 实战:煮蛋计时器的实现 04— 布局

polarisxu

共 6238字,需浏览 13分钟

 ·

2021-11-05 15:11

上篇文章介绍了按钮,但整个按钮是占据屏幕的,这显然不合适。本文就解决该问题。

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(),
    }

解释说明

解释下这段代码的结构。

  1. 首先我们通过结构体 layout.Flex{ } 定义一个  Flexbox
  2. 然后我们向它增加一个要放置的子项列表 Layout(gtx, ...)。图形上下文 gtx 包含子项必须遵守的约束,并且任何数量的子项都要遵循。

我们列出的子项都是由 layout.Rigid( ) 创建的:第一个是按钮的占位符,另一个占位符,用于包含按钮下方的空白区域。

什么是 Rigid[3]?很简单 - 它的工作是填充给定的空间。Rigid 的子项首先占据它的部分,而 Flexed[4] 子项占据剩下的。除此之外,子项按照定义的顺序排列。

约束和尺寸(Constraints 和 Dimensions)

在这一点上,我们可以退后一步,看看将所有这些结合在一起的概念,即 ConstraintsDimensions

  • 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{} 里面,我们定义了两个属性:

  1. Axis(轴):垂直对齐意味各项竖着排列。
  2. 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




往期推荐


我是 polarisxu,北大硕士毕业,曾在 360 等知名互联网公司工作,10多年技术研发与架构经验!2012 年接触 Go 语言并创建了 Go 语言中文网!著有《Go语言编程之旅》、开源图书《Go语言标准库》等。


坚持输出技术(包括 Go、Rust 等技术)、职场心得和创业感悟!欢迎关注「polarisxu」一起成长!也欢迎加我微信好友交流:gopherstudio

浏览 55
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报