One of the fun things I get to do regularly is build animations in to Xojo desktop components. While incredibly rewarding when you get it right, it can be a long road.
Historical Methods
The Tight Loop
A long time ago, to implement animation in Xojo, one had to resort to tight loops if the animation needed to be self-contained (in a custom class, for example). This resulted in code that looked a lot like this:
while me.Height > 0
me.Height = me.Height - 10
me.Refresh()
App.DoEvents(10)
Wend
Now, however, this is bad for a number of reasons. The most obvious being the use of a tight loop for UI updates. This can result in all kinds of funkiness that’s hard to workaround, and often the code above would’ve been much longer to account for other issues in the UI depending upon your implementation.
Second, the use of Refresh(). Refresh was required because the changes were happening in a tight loop, but it comes with its own set of drawbacks, and there are no shortage of forum posts that detail why to avoid it.
Third, App.DoEvents. Don’t. Use. DoEvents. I can’t stress this enough. Unless building a console app, DoEvents should almost always be avoided. As Norman points out in this post, if you think you need DoEvents in a desktop app, you probably actually need a thread.
Threads
Using threads for animation can be a great way to implement it depending on the scenario, but one must remember that threads can no longer access the UI. At one time, however, they could. This gave us the ability to take that tight loop above and shove it in to a thread to avoid locking up the UI and using DoEvents. A typical thread animation Run() event might’ve looked like this:
Sub Run() Handles Run
while me.Height > 0
control1.Height = control1.Height - 10
control1.Refresh()
me.Sleep(10)
Wend
End Sub
While this was an improvement as it moved the tight loop out of the primary event loop, the old implementation of threads that allowed for directly accessing the UI came with its own set of problems and was unsafe.
Today
Nowadays, the best method is to use a timer. If we want a self-contained animation, then this requires a bit of setup. I’m creating a new class called AnimatedCanvas that shrinks or expands its width when clicked. Its superclass is Canvas.
The properties
Once we have that, we’re going to add some properties to it:
Private Property expandedWidth as Integer
Private Property animStep as Integer
Private Property animTimer as Timer
Private Property expanded as Boolean = True
The expandedWidth property is where we’re going to store the width we want when the control is expanded fully.
animStep is where we’ll store the current animation’s step value, or the amount of change to the width at each stage of the animation.
animTimer is our Timer instance that will be doing the heavy-lifting.
The expanded boolean property helps us determine what operation is currently in progress. We set it to True because we’re going to use the default width of the control as our expandedWidth property later on.
The Event Handlers
The first event handler we want to add is the Open event. In this simple example we’re just setting our expandedWidth property to the width of the component, and setting up our animTimer property for use later.
Sub Open() Handles Open
'// In this example, I'm just setting the expandedWidth to the initial width.
' A real-world case would obviously be different.
expandedWidth = me.Width
'// Initialize our animation timer. I'm using a period of 20 here,
' but this should be adjusted for best performance and smoothness
' based on what you're animating.
animTimer = new Timer
animTimer.Period = 20
animTimer.Mode = 0
AddHandler animTimer.Action, WeakAddressOf animTimer_Action
End Sub
Next we’re just going to do something simple in the Paint event so that we can see what’s going on
Sub Paint(g As Graphics, areas() As REALbasic.Rect) Handles Paint
'// Just fill it red so that we can see it.
g.ForeColor = &cff0000
g.FillRect( 0, 0, g.Width, g.Height )
End Sub
MouseDown is also simple. We want our animation to occur only if the control is enabled, so we can just return the value of Enabled. If Enabled, then MouseUp will fire next where we start our animation.
Function MouseDown(X As Integer, Y As Integer) Handles MouseDown as Boolean
Return me.Enabled
End Function
MouseUp is a little more complex as we’re doing some simple math for animStep and then starting the timer.
'// Just in case we hit some strange scenario where
' the timer is disposed of. Should, theoretically,
' never happen in this case, but better safe than sorry.
if IsNull( animTimer ) then Return
'// Here we setup our animation step to take 1000ms.
' We multiple by 20, which is our timer interval.
' The minimum built-in timer Interval for macOS
' is 10ms and for Windows is 16ms, if I remember
' correctly. 20 gets us where we want to be, but
' could just as easily use a #IF target or the
' value of 16.
animStep = (expandedWidth / 1000) * 20
'// If our control is currently expanded, then we'll
' use a negative value to shrink it.
if expanded then animStep = -animStep
'// Finally, enable the timer to perform the animation.
animTimer.Mode = 2
Timer Action
Now that we have all of that setup, the last thing we need to do is add our timer’s Action handler method. This is going to do the bulk of the work by calculating the new width, checking the new width for validity, then either applying the new value or ending the animation.
Private Sub animTimer_Action(sender as Timer)
'// We want to calculate the new value ahead of time.
' This gives us the opportunity to bail a little
' earlier and not have to apply a corrected value later.
dim newWidth as Integer = me.Width + animStep
' If expanded = True then we're shrinking.
if expanded then
'// If our newWidth is going to be less than or equal to
' our minimum width, then we're done with our animation.
' I'm using a hardcoded width of 10 here, but you could
' create a property for this.
if newWidth <= 10 then
'// Make sure the width is correct for the new state,
' disable the timer, then set our Expanded property.
me.Width = 10
sender.Mode = 0
expanded = False
Return
end if
else
'// If our newWidth is going to be greater than or equal
' to our expandedWidth, we bail.
if newWidth >= expandedWidth then
'// Make sure the width is correct for the new state,
' disable the timer, then set our Expanded property.
me.Width = expandedWidth
sender.Mode = 0
expanded = True
Return
end if
end if
'// If we get here, then we're still somewhere in the middle
' of the animation. Apply the new width value.
me.Width = newWidth
End Sub
Final Notes
As you can see, animating in Xojo is a fairly simple concept at its core. The real fun starts when you want to animate multiple virtual elements inside picture objects with varying types of animation, and we won’t even get in to easing.
Bear in mind that this is an intentionally simplistic example that’s meant to illustrate the basics of timer-based animation. A full implementation that expects multiple different moving parts together or independently would be much more complex, and likely use something along the lines of an Array or Dictionary to keep track of the animation queue and the expected changes at each step in the process.
I’ve built all of this in to an example project, which you can download here.