使用go-gio库示例的定时器详解
包含的控件和功能
1、进度条的显示和控制
2、滑块的显示和控制
3、垂直布局的设计
4、ui界面的强制刷新
package main
import (
"context"
"fmt"
"log"
"os"
"sync"
"time"
"gioui.org/app" // app contains Window handling.
"gioui.org/font/gofont" // gofont is used for loading the default font.
"gioui.org/io/key" // key is used for keyboard events.
"gioui.org/io/system" // system is used for system events (e.g. closing the window).
"gioui.org/layout" // layout is used for layouting widgets.
"gioui.org/op" // op is used for recording different operations.
"gioui.org/unit" // unit is used to define pixel-independent sizes
"gioui.org/widget" // widget contains state handling for widgets.
"gioui.org/widget/material" // material contains material design widgets.
)
// Timer 计时器实现
type Timer struct {
// 更改通知通道
Updated chan struct{}
mu sync.Mutex
start time.Time // 计时器的启动时间
now time.Time // 计时器的现在时间
duration time.Duration // 时间差
}
// NewTimer 创建一个计时器
func NewTimer(initialDuration time.Duration) *Timer {
return &Timer{
Updated: make(chan struct{}),
duration: initialDuration,
}
}
// Start 开启一个计时器、并返回他的取消方法
func (t *Timer) Start() context.CancelFunc {
// 初始化定时器状态
now := time.Now()
t.now = now
t.start = now
// 结束通道
done := make(chan struct{})
go t.run(done)
return func() { close(done) }
}
// 计时器主循环
func (t *Timer) run(done chan struct{}) {
// 50毫秒的计时器
tick := time.NewTicker(50 * time.Millisecond)
defer tick.Stop()
for {
select {
case now := <-tick.C: // 每五十毫秒更新当前计时
t.update(now)
case <-done:
return
}
}
}
// 向ui发送更新ui界面的通知
func (t *Timer) invalidate() {
select {
case t.Updated <- struct{}{}:
default:
}
}
// 更新时间
func (t *Timer) update(now time.Time) {
t.mu.Lock()
defer t.mu.Unlock()
// 上一次刷新的时间
previousNow := t.now
// 现在刷新的时间
t.now = now
// 当现在的时间与开始时间的时间差小于设置差值时,更新界面
progressAfter := t.now.Sub(t.start)
if progressAfter <= t.duration {
// 通知更新ui界面
t.invalidate()
return
}
// 当时间差等于差值时更新ui界面、使进度条走到头
progressBefore := previousNow.Sub(t.start)
if progressBefore <= t.duration {
t.invalidate()
return
}
}
// Reset 将开始时间设置为上次的时间
func (t *Timer) Reset() {
t.mu.Lock()
defer t.mu.Unlock()
t.start = t.now
t.invalidate()
}
// SetDuration 更改计时器的持续时间
func (t *Timer) SetDuration(duration time.Duration) {
t.mu.Lock()
defer t.mu.Unlock()
if t.duration == duration {
return
}
t.duration = duration
t.invalidate()
}
// Info 获取当前计时器的信息
func (t *Timer) Info() (info Info) {
t.mu.Lock()
defer t.mu.Unlock()
info.Progress = t.now.Sub(t.start)
info.Duration = t.duration
if info.Progress > info.Duration {
info.Progress = info.Duration
}
return info
}
// Info 计时器信息
type Info struct {
Progress time.Duration
Duration time.Duration
}
// ProgressString 返回进度条的信息
func (info *Info) ProgressString() string {
return fmt.Sprintf("%.1fs", info.Progress.Seconds())
}
func main() {
ui := NewUI()
go func() {
w := app.NewWindow(
app.Title("Timer"),
app.Size(unit.Dp(360), unit.Dp(360)),
)
if err := ui.Run(w); err != nil {
log.Println(err)
os.Exit(1)
}
os.Exit(0)
}()
app.Main()
}
var defaultMargin = unit.Dp(10)
// UI ui界面
type UI struct {
Theme *material.Theme
Timer *Timer // 计时器
duration widget.Float // 滑动框
reset widget.Clickable // 可点击对象
}
// NewUI 创建计时器实例
func NewUI() *UI {
ui := &UI{}
ui.Theme = material.NewTheme(gofont.Collection())
// 设置五秒的初始值
ui.Timer = NewTimer(5 * time.Second)
ui.duration.Value = 5
return ui
}
// Run 运行界面
func (ui *UI) Run(w *app.Window) error {
// 开启一个计时器
closeTimer := ui.Timer.Start()
// 退出时关闭
defer closeTimer()
var ops op.Ops
for {
select {
// 主动刷新ui窗口
case <-ui.Timer.Updated:
// 是窗口无效、刷新frameEvent
w.Invalidate()
case e := <-w.Events():
switch e := e.(type) {
case system.FrameEvent:
gtx := layout.NewContext(&ops, e)
ui.Layout(gtx)
e.Frame(gtx.Ops)
case key.Event:
switch e.Name {
case key.NameEscape:
return nil
}
case system.DestroyEvent:
return e.Err
}
}
}
}
// Layout ui界面显示
func (ui *UI) Layout(gtx layout.Context) layout.Dimensions {
// 检查重置按钮是否点击了
if ui.reset.Clicked() {
// 重置时间
ui.Timer.Reset()
}
// 检查滑块值是否已更改
if ui.duration.Changed() {
// 重新设置时间差
ui.Timer.SetDuration(secondsToDuration(float64(ui.duration.Value)))
}
// 获取当前计时器的信息
info := ui.Timer.Info()
progress := float32(0)
// 当时间差为0时,进度条显示完成
if info.Duration == 0 {
progress = 1
} else {
// 百分比显示
progress = float32(info.Progress.Seconds() / info.Duration.Seconds())
}
// inset is used to add padding around the window border.
inset := layout.UniformInset(defaultMargin)
return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
// 创建一个垂直布局
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(material.Body1(ui.Theme, "Elapsed Time").Layout),
// 进度条
layout.Rigid(material.ProgressBar(ui.Theme, progress).Layout),
// 进度信息
layout.Rigid(material.Body1(ui.Theme, info.ProgressString()).Layout),
// 占位组件
layout.Rigid(layout.Spacer{Height: ui.Theme.TextSize}.Layout),
// 文本显示区
layout.Rigid(material.Body1(ui.Theme, "Duration").Layout),
// 滑块
layout.Rigid(material.Slider(ui.Theme, &ui.duration, 0, 15).Layout),
// 占位组件
layout.Rigid(layout.Spacer{Height: ui.Theme.TextSize}.Layout),
// 重置按钮
layout.Rigid(material.Button(ui.Theme, &ui.reset, "Reset").Layout),
)
})
}
// 将秒数转换成时间间隔
func secondsToDuration(s float64) time.Duration {
return time.Duration(s * float64(time.Second))
}