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

解釋說明

解釋下這段代碼的結構。

  1. 首先我們通過結構體 layout.Flex{ } 定義一個  Flexbox

  2. 然後我們向它增加一個要放置_的子項列表_ Layout(gtx, ...)。圖形上下文 _gtx_ 包含子項必須遵守的約束,並且任何數量的子項都要遵循。

我們列出的子項都是由 layout.Rigid( ) 創建的:第一個是按鈕的佔位符,另一個佔位符,用於包含按鈕下方的空白區域。

什麼是 Rigid[3]?很簡單 - 它的工作是填充給定的空間。Rigid 的子項首先佔據它的部分,而 Flexed[4] 子項佔據剩下的。除此之外,子項按照定義的順序排列。

約束和尺寸(Constraints 和 Dimensions)

在這一點上,我們可以退後一步,看看將所有這些結合在一起的概念,即 ConstraintsDimensions

父級設置 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( ) 的兩個調用:

圖片

Button above spacer

這是佈局小部件。但是小部件(widget)到底是什麼?

從源碼角度,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