mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-05 17:35:36 +00:00
example/tabs: animated switching
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
+37
-2
@@ -5,7 +5,9 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
"image/color"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
|
|
||||||
"gioui.org/app"
|
"gioui.org/app"
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
@@ -47,6 +49,7 @@ func loop(w *app.Window) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tabs Tabs
|
var tabs Tabs
|
||||||
|
var slider Slider
|
||||||
|
|
||||||
type Tabs struct {
|
type Tabs struct {
|
||||||
list layout.List
|
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) {
|
tabs.list.Layout(gtx, len(tabs.tabs), func(tabIdx int) {
|
||||||
t := &tabs.tabs[tabIdx]
|
t := &tabs.tabs[tabIdx]
|
||||||
if t.btn.Clicked(gtx) {
|
if t.btn.Clicked(gtx) {
|
||||||
|
if tabs.selected < tabIdx {
|
||||||
|
slider.PushLeft(gtx)
|
||||||
|
} else if tabs.selected > tabIdx {
|
||||||
|
slider.PushRight(gtx)
|
||||||
|
}
|
||||||
tabs.selected = tabIdx
|
tabs.selected = tabIdx
|
||||||
}
|
}
|
||||||
var tabWidth int
|
var tabWidth int
|
||||||
@@ -105,9 +113,36 @@ func drawTabs(gtx *layout.Context, th *material.Theme) {
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
layout.Flexed(1, func() {
|
layout.Flexed(1, func() {
|
||||||
layout.Center.Layout(gtx, func() {
|
slider.Layout(gtx, func() {
|
||||||
material.H1(th, fmt.Sprintf("Tab content #%d", tabs.selected)).Layout(gtx)
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user