Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make starting animations idempotent and improve runner performance #4449

Closed
wants to merge 8 commits into from
44 changes: 41 additions & 3 deletions animation.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package fyne

import "time"
import (
"sync"
"time"
)

// AnimationCurve represents an animation algorithm for calculating the progress through a timeline.
// Custom animations can be provided by implementing the "func(float32) float32" definition.
Expand Down Expand Up @@ -32,6 +35,22 @@ var (
AnimationLinear = animationLinear
)

// AnimationState represents the state of an animation.
//
// Since: 2.5
type AnimationState int

const (
// AnimationStateNotStarted represents an animation that has been created but not yet started.
AnimationStateNotStarted AnimationState = iota

// AnimationStateRunning represents an animation that is running.
AnimationStateRunning

// AnimationStateStopped represents an animation that has been stopped or has finished running.
AnimationStateStopped
)

// Animation represents an animated element within a Fyne canvas.
// These animations may control individual objects or entire scenes.
//
Expand All @@ -42,6 +61,9 @@ type Animation struct {
Duration time.Duration
RepeatCount int
Tick func(float32)

mutex sync.Mutex
state AnimationState
}

// NewAnimation creates a very basic animation where the callback function will be called for every
Expand All @@ -55,12 +77,28 @@ func NewAnimation(d time.Duration, fn func(float32)) *Animation {

// Start registers the animation with the application run-loop and starts its execution.
func (a *Animation) Start() {
CurrentApp().Driver().StartAnimation(a)
a.mutex.Lock()
defer a.mutex.Unlock()
if a.state == AnimationStateRunning {
return
}
a.state = AnimationStateRunning
d := CurrentApp().Driver().(interface{ StartAnimationPrivate(*Animation) })
d.StartAnimationPrivate(a)
}

// Stop will end this animation and remove it from the run-loop.
func (a *Animation) Stop() {
CurrentApp().Driver().StopAnimation(a)
a.mutex.Lock()
defer a.mutex.Unlock()
a.state = AnimationStateStopped
}

// State returns the state of this animation.
//
// Since: 2.5
func (a *Animation) State() AnimationState {
return a.state
}

func animationEaseIn(val float32) float32 {
Expand Down
9 changes: 7 additions & 2 deletions driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ type Driver interface {
Quit()

// StartAnimation registers a new animation with this driver and requests it be started.
StartAnimation(*Animation)
//
// Deprecated: Use a.Start() instead.
StartAnimation(a *Animation)

// StopAnimation stops an animation and unregisters from this driver.
StopAnimation(*Animation)
//
// Deprecated: Use a.Stop() instead.
StopAnimation(a *Animation)
}
10 changes: 0 additions & 10 deletions internal/animation/animation.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package animation

import (
"sync/atomic"
"time"

"fyne.io/fyne/v2"
Expand All @@ -14,7 +13,6 @@ type anim struct {
reverse bool
start time.Time
total int64
stopped uint32 // atomic, 0 == false 1 == true
}

func newAnim(a *fyne.Animation) *anim {
Expand All @@ -23,11 +21,3 @@ func newAnim(a *fyne.Animation) *anim {
animate.repeatsLeft = a.RepeatCount
return animate
}

func (a *anim) setStopped() {
atomic.StoreUint32(&a.stopped, 1)
}

func (a *anim) isStopped() bool {
return atomic.LoadUint32(&a.stopped) == 1
}
16 changes: 7 additions & 9 deletions internal/animation/animation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,8 @@ func TestGLDriver_StopAnimation(t *testing.T) {
case <-time.After(time.Second):
t.Error("animation was not ticked")
}
run.Stop(a)
run.animationMutex.RLock()
assert.Zero(t, len(run.animations))
run.animationMutex.RUnlock()
a.Stop()
assert.True(t, a.State() == fyne.AnimationStateStopped, "animation was not stopped")
}

func TestGLDriver_StopAnimationImmediatelyAndInsideTick(t *testing.T) {
Expand All @@ -64,7 +62,7 @@ func TestGLDriver_StopAnimationImmediatelyAndInsideTick(t *testing.T) {
Tick: func(f float32) {},
}
run.Start(a)
run.Stop(a)
a.Stop()

// stopping animation inside tick function
for i := 0; i < 10; i++ {
Expand All @@ -73,7 +71,7 @@ func TestGLDriver_StopAnimationImmediatelyAndInsideTick(t *testing.T) {
b = &fyne.Animation{
Duration: time.Second,
Tick: func(d float32) {
run.Stop(b)
b.Stop()
wg.Done()
}}
run.Start(b)
Expand All @@ -86,12 +84,12 @@ func TestGLDriver_StopAnimationImmediatelyAndInsideTick(t *testing.T) {
Tick: func(f float32) {},
}
run.Start(c)
run.Stop(c)
c.Stop()

wg.Wait()
// animations stopped inside tick are really stopped in the next runner cycle
time.Sleep(time.Second/60 + 100*time.Millisecond)
run.animationMutex.RLock()
run.animationMutex.Lock()
assert.Zero(t, len(run.animations))
run.animationMutex.RUnlock()
run.animationMutex.Unlock()
}
76 changes: 28 additions & 48 deletions internal/animation/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,56 +9,23 @@ import (

// Runner is the main driver for animations package
type Runner struct {
animationMutex sync.RWMutex
animations []*anim
animationMutex sync.Mutex
pendingAnimations []*anim
runnerStarted bool

runnerStarted bool
animations []*anim // accessed only by runAnimations
}

// Start will register the passed application and initiate its ticking.
func (r *Runner) Start(a *fyne.Animation) {
r.animationMutex.Lock()
defer r.animationMutex.Unlock()
r.pendingAnimations = append(r.pendingAnimations, newAnim(a))
r.animationMutex.Unlock()

if !r.runnerStarted {
r.runnerStarted = true
r.animations = append(r.animations, newAnim(a))
r.runAnimations()
} else {
r.pendingAnimations = append(r.pendingAnimations, newAnim(a))
}
}

// Stop causes an animation to stop ticking (if it was still running) and removes it from the runner.
func (r *Runner) Stop(a *fyne.Animation) {
r.animationMutex.Lock()
defer r.animationMutex.Unlock()

newList := make([]*anim, 0, len(r.animations))
stopped := false
for _, item := range r.animations {
if item.a != a {
newList = append(newList, item)
} else {
item.setStopped()
stopped = true
}
}
r.animations = newList
if stopped {
return
}

newList = make([]*anim, 0, len(r.pendingAnimations))
for _, item := range r.pendingAnimations {
if item.a != a {
newList = append(newList, item)
} else {
item.setStopped()
}
}
r.pendingAnimations = newList
}

func (r *Runner) runAnimations() {
Expand All @@ -67,20 +34,33 @@ func (r *Runner) runAnimations() {
go func() {
for done := false; !done; {
<-draw.C
r.animationMutex.Lock()
oldList := r.animations
r.animationMutex.Unlock()
newList := make([]*anim, 0, len(oldList))
for _, a := range oldList {
if !a.isStopped() && r.tickAnimation(a) {
newList = append(newList, a)

// tick currently running animations
// use technique from https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
// to filter the still-running animations for the next iteration without allocating a new slice
newList := r.animations[:0]
for _, a := range r.animations {
if stopped := a.a.State() == fyne.AnimationStateStopped; !stopped && r.tickAnimation(a) {
newList = append(newList, a) // still running
} else if !stopped {
a.a.Stop() // mark as stopped (completed running)
}
}

// bring in all pending animations
r.animationMutex.Lock()
r.animations = append(newList, r.pendingAnimations...)
r.pendingAnimations = nil
done = len(r.animations) == 0
for i, a := range r.pendingAnimations {
newList = append(newList, a)
r.pendingAnimations[i] = nil
}
r.pendingAnimations = r.pendingAnimations[:0]
r.animationMutex.Unlock()

done = len(newList) == 0
for i := len(newList); i < len(r.animations); i++ {
r.animations[i] = nil // nil out extra slice capacity
}
r.animations = newList
}
r.animationMutex.Lock()
r.runnerStarted = false
Expand Down
6 changes: 5 additions & 1 deletion internal/driver/glfw/animation.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ package glfw
import "fyne.io/fyne/v2"

func (d *gLDriver) StartAnimation(a *fyne.Animation) {
a.Start()
}

func (d *gLDriver) StartAnimationPrivate(a *fyne.Animation) {
d.animation.Start(a)
}

func (d *gLDriver) StopAnimation(a *fyne.Animation) {
d.animation.Stop(a)
a.Stop()
}
6 changes: 5 additions & 1 deletion internal/driver/mobile/animation.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ package mobile
import "fyne.io/fyne/v2"

func (d *mobileDriver) StartAnimation(a *fyne.Animation) {
a.Start()
}

func (d *mobileDriver) StartAnimationPrivate(a *fyne.Animation) {
d.animation.Start(a)
}

func (d *mobileDriver) StopAnimation(a *fyne.Animation) {
d.animation.Stop(a)
a.Stop()
}
5 changes: 5 additions & 0 deletions test/testdriver.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ func (d *testDriver) StopAnimation(a *fyne.Animation) {
// currently no animations in test app, do nothing
}

func (d *testDriver) StartAnimationPrivate(a *fyne.Animation) {
/// currently no animations in test app, we just initialise it and leave
a.Tick(1.0)
}

func (d *testDriver) Quit() {
// no-op
}
Expand Down
Loading
Loading