From f29964fee15aeffd136fd9bb8716c124124ed5b2 Mon Sep 17 00:00:00 2001 From: Egon Elbre Date: Wed, 13 May 2020 11:37:11 +0300 Subject: [PATCH] example/tabs: animated switching Signed-off-by: Egon Elbre --- example/tabs/slider.go | 130 +++++++++++++++++++++++++++++++++++++++++ example/tabs/tabs.go | 39 ++++++++++++- 2 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 example/tabs/slider.go diff --git a/example/tabs/slider.go b/example/tabs/slider.go new file mode 100644 index 00000000..08f21c90 --- /dev/null +++ b/example/tabs/slider.go @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "time" + + "gioui.org/f32" + "gioui.org/layout" + "gioui.org/op" +) + +const defaultDuration = 300 * time.Millisecond + +// Slider implements sliding between old/new widget values. +type Slider struct { + Duration time.Duration + + push int + + last *op.Ops + next *op.Ops + + t0 time.Time + offset float32 +} + +// PushLeft pushes the existing widget to the left. +func (s *Slider) PushLeft(gtx *layout.Context) { s.push = 1 } + +// PushRight pushes the existing widget to the right. +func (s *Slider) PushRight(gtx *layout.Context) { s.push = -1 } + +// Layout lays out widget that can be pushed. +func (s *Slider) Layout(gtx *layout.Context, w layout.Widget) { + if s.push != 0 { + s.last, s.next = s.next, new(op.Ops) + s.offset = float32(s.push) + s.t0 = gtx.Now() + s.push = 0 + } + + var delta time.Duration + if !s.t0.IsZero() { + now := gtx.Now() + delta = now.Sub(s.t0) + s.t0 = now + } + + if s.offset != 0 { + duration := s.Duration + if duration == 0 { + duration = defaultDuration + } + movement := float32(delta.Seconds()) / float32(duration.Seconds()) + if s.offset < 0 { + s.offset += movement + if s.offset >= 0 { + s.offset = 0 + } + } else { + s.offset -= movement + if s.offset <= 0 { + s.offset = 0 + } + } + + op.InvalidateOp{}.Add(gtx.Ops) + } + + { + prev := gtx.Ops + if s.next == nil { + s.next = new(op.Ops) + } + s.next.Reset() + gtx.Ops = s.next + w() + gtx.Ops = prev + } + + if s.offset == 0 { + op.CallOp{Ops: s.next}.Add(gtx.Ops) + return + } + + var stack op.StackOp + stack.Push(gtx.Ops) + defer stack.Pop() + + offset := smooth(s.offset) + + if s.offset > 0 { + op.TransformOp{}.Offset(f32.Point{ + X: float32(gtx.Dimensions.Size.X) * (offset - 1), + }).Add(gtx.Ops) + op.CallOp{Ops: s.last}.Add(gtx.Ops) + + op.TransformOp{}.Offset(f32.Point{ + X: float32(gtx.Dimensions.Size.X), + }).Add(gtx.Ops) + op.CallOp{Ops: s.next}.Add(gtx.Ops) + } else { + op.TransformOp{}.Offset(f32.Point{ + X: float32(gtx.Dimensions.Size.X) * (offset + 1), + }).Add(gtx.Ops) + op.CallOp{Ops: s.last}.Add(gtx.Ops) + + op.TransformOp{}.Offset(f32.Point{ + X: float32(-gtx.Dimensions.Size.X), + }).Add(gtx.Ops) + op.CallOp{Ops: s.next}.Add(gtx.Ops) + } +} + +// smooth handles -1 to 1 with ease-in-out cubic easing func. +func smooth(t float32) float32 { + if t < 0 { + return -easeInOutCubic(-t) + } + return easeInOutCubic(t) +} + +// easeInOutCubic maps a linear value to a ease-in-out-cubic easing function. +func easeInOutCubic(t float32) float32 { + if t < 0.5 { + return 4 * t * t * t + } + return (t-1)*(2*t-2)*(2*t-2) + 1 +} diff --git a/example/tabs/tabs.go b/example/tabs/tabs.go index 783f17ce..d20c771d 100644 --- a/example/tabs/tabs.go +++ b/example/tabs/tabs.go @@ -5,7 +5,9 @@ package main import ( "fmt" "image" + "image/color" "log" + "math" "gioui.org/app" "gioui.org/f32" @@ -47,6 +49,7 @@ func loop(w *app.Window) error { } var tabs Tabs +var slider Slider type Tabs struct { list layout.List @@ -73,6 +76,11 @@ func drawTabs(gtx *layout.Context, th *material.Theme) { tabs.list.Layout(gtx, len(tabs.tabs), func(tabIdx int) { t := &tabs.tabs[tabIdx] if t.btn.Clicked(gtx) { + if tabs.selected < tabIdx { + slider.PushLeft(gtx) + } else if tabs.selected > tabIdx { + slider.PushRight(gtx) + } tabs.selected = tabIdx } var tabWidth int @@ -105,9 +113,36 @@ func drawTabs(gtx *layout.Context, th *material.Theme) { }) }), layout.Flexed(1, func() { - layout.Center.Layout(gtx, func() { - material.H1(th, fmt.Sprintf("Tab content #%d", tabs.selected)).Layout(gtx) + slider.Layout(gtx, func() { + fill(gtx, dynamicColor(tabs.selected)) + layout.Center.Layout(gtx, func() { + material.H1(th, fmt.Sprintf("Tab content #%d", tabs.selected+1)).Layout(gtx) + }) }) }), ) } + +func bounds(gtx *layout.Context) f32.Rectangle { + cs := gtx.Constraints + d := image.Point{X: cs.Width.Min, Y: cs.Height.Min} + return f32.Rectangle{ + Max: f32.Point{X: float32(d.X), Y: float32(d.Y)}, + } +} + +func fill(gtx *layout.Context, col color.RGBA) { + dr := bounds(gtx) + paint.ColorOp{Color: col}.Add(gtx.Ops) + paint.PaintOp{Rect: dr}.Add(gtx.Ops) +} + +func dynamicColor(i int) color.RGBA { + sn, cs := math.Sincos(float64(i) * math.Phi) + return color.RGBA{ + R: 0xA0 + byte(0x30*sn), + G: 0xA0 + byte(0x30*cs), + B: 0xD0, + A: 0xFF, + } +}