Skip to content

Commit

Permalink
Add a new widget for activity indication
Browse files Browse the repository at this point in the history
  • Loading branch information
andydotxyz committed Sep 16, 2023
1 parent 3ee5a3e commit 998790f
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 1 deletion.
7 changes: 6 additions & 1 deletion cmd/fyne_demo/tutorials/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ var (
makeAccordionTab,
true,
},
"activity": {"Activity",
"A spinner indicating activity used in buttons etc.",
makeActivityTab,
true,
},
"button": {"Button",
"Simple widget for user tap handling.",
makeButtonTab,
Expand Down Expand Up @@ -182,6 +187,6 @@ var (
"": {"welcome", "canvas", "animations", "icons", "widgets", "collections", "containers", "dialogs", "windows", "binding", "advanced"},
"collections": {"list", "table", "tree", "gridwrap"},
"containers": {"apptabs", "border", "box", "center", "doctabs", "grid", "scroll", "split"},
"widgets": {"accordion", "button", "card", "entry", "form", "input", "progress", "text", "toolbar"},
"widgets": {"accordion", "activity", "button", "card", "entry", "form", "input", "progress", "text", "toolbar"},
}
)
47 changes: 47 additions & 0 deletions cmd/fyne_demo/tutorials/widget.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,53 @@ func makeAccordionTab(_ fyne.Window) fyne.CanvasObject {
return ac
}

func makeActivityTab(_ fyne.Window) fyne.CanvasObject {
a1 := widget.NewActivity()
a2 := widget.NewActivity()
a3 := widget.NewActivity()

prop := canvas.NewRectangle(color.Transparent)
prop.SetMinSize(fyne.NewSize(160, 80))

var button *widget.Button
start := func() {
button.Disable()
a1.Start()
a1.Show()
a2.Start()
a2.Show()
a3.Start()
a3.Show()

defer func() {
go func() {
time.Sleep(time.Second * 10)
a1.Stop()
a1.Hide()
a2.Stop()
a2.Hide()
a3.Stop()
a3.Hide()

button.Enable()
}()
}()
}

button = widget.NewButton("Animate", start)
start()

fakeDialog := container.NewStack(canvas.NewRectangle(theme.OverlayBackgroundColor()),
container.NewVBox(widget.NewLabelWithStyle("Dialog, e.g.", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
container.NewStack(prop, a3)))

return container.NewCenter(container.NewGridWithColumns(1,
container.NewCenter(container.NewVBox(
container.NewHBox(widget.NewLabel("Working..."), a1),
container.NewStack(button, a2))),
container.NewCenter(fakeDialog)))
}

func makeButtonTab(_ fyne.Window) fyne.CanvasObject {
disabled := widget.NewButton("Disabled", func() {})
disabled.Disable()
Expand Down
145 changes: 145 additions & 0 deletions widget/activity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package widget

import (
"image/color"
"time"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/internal/cache"
"fyne.io/fyne/v2/theme"
)

var _ fyne.Widget = (*Activity)(nil)

// Activity is used to indicate that something is happening that should be waited for,
// or is in the background (depending on usage).
type Activity struct {
BaseWidget
}

// NewActivity returns a widget for indicating activity
func NewActivity() *Activity {
a := &Activity{}
a.ExtendBaseWidget(a)
return a
}

func (a *Activity) MinSize() fyne.Size {
a.ExtendBaseWidget(a)

return fyne.NewSquareSize(theme.IconInlineSize())
}

// Start the activity indicator animation
func (a *Activity) Start() {
if r, ok := cache.Renderer(a.super()).(*activityRenderer); ok {
r.start()
}
}

// Stop the activity indicator animation
func (a *Activity) Stop() {
if r, ok := cache.Renderer(a.super()).(*activityRenderer); ok {
r.stop()
}
}

func (a *Activity) CreateRenderer() fyne.WidgetRenderer {
dots := make([]fyne.CanvasObject, 3)
for i := range dots {
dots[i] = canvas.NewCircle(theme.ForegroundColor())
}
r := &activityRenderer{dots: dots}
r.anim = &fyne.Animation{
// Curve: fyne.AnimationEaseOut,
Duration: time.Second * 2,
RepeatCount: fyne.AnimationRepeatForever,
Tick: r.animate}
r.updateColor()
return r
}

var _ fyne.WidgetRenderer = (*activityRenderer)(nil)

type activityRenderer struct {
anim *fyne.Animation
dots []fyne.CanvasObject

bound fyne.Size
maxCol color.NRGBA
maxRad float32
}

func (a *activityRenderer) Destroy() {
a.stop()
}

func (a *activityRenderer) Layout(size fyne.Size) {
a.maxRad = fyne.Min(size.Width, size.Height) / 2
a.bound = size
}

func (a *activityRenderer) MinSize() fyne.Size {
return fyne.NewSquareSize(theme.IconInlineSize())
}

func (a *activityRenderer) Objects() []fyne.CanvasObject {
return a.dots
}

func (a *activityRenderer) Refresh() {
a.updateColor()
}

func (a *activityRenderer) animate(done float32) {
off := done * 2
if off > 1 {
off = 2 - off
}

off1 := (done + 0.25) * 2
if done >= 0.75 {
off1 = (done - 0.75) * 2
}
if off1 > 1 {
off1 = 2 - off1
}

off2 := (done + 0.75) * 2
if done >= 0.25 {
off2 = (done - 0.25) * 2
}
if off2 > 1 {
off2 = 2 - off2
}

a.scaleDot(a.dots[0].(*canvas.Circle), off)
a.scaleDot(a.dots[1].(*canvas.Circle), off1)
a.scaleDot(a.dots[2].(*canvas.Circle), off2)
}

func (a *activityRenderer) scaleDot(dot *canvas.Circle, off float32) {
rad := a.maxRad - a.maxRad*off/1.2
mid := fyne.NewPos(a.bound.Width/2, a.bound.Height/2)

dot.Move(mid.Subtract(fyne.NewSquareOffsetPos(rad)))
dot.Resize(fyne.NewSquareSize(rad * 2))

alpha := uint8(0 + int(float32(a.maxCol.A)*off))
dot.FillColor = color.NRGBA{R: a.maxCol.R, G: a.maxCol.G, B: a.maxCol.B, A: alpha}
dot.Refresh()
}

func (a *activityRenderer) start() {
a.anim.Start()
}

func (a *activityRenderer) stop() {
a.anim.Stop()
}

func (a *activityRenderer) updateColor() {
rr, gg, bb, aa := theme.ForegroundColor().RGBA()
a.maxCol = color.NRGBA{R: uint8(rr >> 8), G: uint8(gg >> 8), B: uint8(bb >> 8), A: uint8(aa >> 8)}
}

0 comments on commit 998790f

Please sign in to comment.