Introduction
This article introduces how to create a usercontrol and enable
animation to do a random movement within the parent control area. I am
still new to WPF, so I am not sure if I am doing the right thing, so if
not, please let me know. Thanks
Background
There are major two ways to implement an animation in WPF:
- StoryBoard with using DoubleAnimation, VectorAnimation, or anything similar
- DispatcherTimer
I have read some online blogs say using StoryBoard is better than
using the timer. It probably is true, but in this article, I am using the
timer because it's easier to code... well, maybe I should say it uses
less lines of code to get what I want. Believe it or not, at the
beginning, I tried to use DoubleAnimation, but apparently I used it in
a wrong way. Please let me briefly introduce some basic:
When using an XXXAnimation object, we need to specify From, To, and Duration.
- From: is the animation start's point
- To: is the animation end's point
- Duration: is the length of time for the animation to play
So, if we want a 2D linear animation, we need at
least two animation objects, one for horizontal movement, another for
vertical movement.
Suppose we specify X.From(100) and Y.From(50), X.To(200) and Y.To(200), and
duration to 2 seconds for both of them. It means we want to object to
move from (100, 50) to (200, 200) in 2 seconds.
However, allow me tell you what mistake I made, so you won't do it:
Previously, I tried to set a random
maximum distance that the control can move. It's really hard because when the control hits the boundary,
it must bounce back. So you can see, given a control location,
I need to generate a random angle first, then calculate the maximum
movable distance, calculate the time by a given pixel per millisecond
speed. Things got really messy.
The correct way should be (at least I think):
- Randomly generate two small movement amount for X and Y
- Define a very small duration
Suppose the control original location is at (0,0), and I got a
random (2,3), the duration is 2 millisecond. It means in this 2
millisecond, the control will slightly move from (0,0) to (2,3).
Furthermore, in the
Completed event handler, I should check if the control reaches
the parent boundary yet, if not, start the animation again, otherwise,
generate a different X and Y values to move to another direction.
I think that should work much better than what I am doing now.
In fact, I am using the similar technique but with DispatcherTimer.
One thing I really hate in the timer is, the interval value depends on
how hard the animation is, so I can't make sure the speed is constant
for all moving controls. In MSDN, it explains the interval as
follows:
Timers are not guaranteed to execute exactly when the time interval occurs, but
they are guaranteed to not execute before the time interval occurs.
See that? I didn't try the StoryBoard way, but I believe that will be
better, because a storyboard can make sure the animation finish within the
specified duration.
Using the code
If you have read my previous entry, in order to use a UserControl in an XAML file, we have to create a base class, such as
public class MovableUserControl : System.Windows.Controls.UserControl
In the constructor, we have
public MovableUserControl()
{
TransformGroup tg = new TransformGroup();
tg.Children.Add(Translate);
this.RenderTransform = tg;
timer.Tick += new EventHandler(timer_Tick);
}
private TranslateTransform _translate = new TranslateTransform();
public TranslateTransform Translate
{
get { return _translate; }
}
The constructor adds a TranslateTransform class so it can be used to do the 2D linear transformation.
First, we need to find a way to fingure out the parent control's area:
protected virtual void TryLoadParentSize()
{
if (Parent is UIElement)
{
parentWidth = (Parent as UIElement).RenderSize.Width;
parentHeight = (Parent as UIElement).RenderSize.Height;
}
else
{
throw new Exception("The Parent is not a UIElement, I am not sure what to do....");
}
}
Also, we want to initialize some values before we can start the animation:
protected virtual void initializeMovement()
{
if (!hasStartedOnce)
{
originalPoint = VisualTreeHelper.GetOffset(this);
currentX = originalPoint.X;
currentY = originalPoint.Y;
}
timer.Interval = TimeSpan.FromMilliseconds(10);
restart = true;
}
restart is a variable to indicate if we need to restart the animation.
hasStartedOnce is a trick, the default is false, once the Start is
called at the first time, it will be set to true. See reason blow.
In the Start and Stop methods, we have:
public virtual void Start()
{
TryLoadParentSize();
initializeMovement();
run();
}
public virtual void Stop()
{
this.timer.Stop();
hasStartedOnce = true;
}
Now you see why I need to have that hasStartedOnce variable.
Here comes tothe most important part, the interval handler:
protected virtual void timer_Tick(object sender, EventArgs e)
{
RecalculateMovement();
UpdateMovement();
if (currentX + this.ActualWidth >= parentWidth + RightOffset ||
currentY + this.ActualHeight >= parentHeight + BottomOffset ||
currentX <= LeftOffset ||
currentY <= TopOffset)
{
restart = true;
}
}
First, I calculate the random movement, then I will update it, finally,
I check if the control has hit the boundary, if yes, I need to tell the
"calculator" to restart.
protected virtual void RecalculateMovement()
{
if (restart)
{
movementX = rand.Next(0, 5);
movementY = rand.Next(0, 5);
movementX = (currentX > halfWidth) ? -movementX : movementX;
movementY = (currentY > halfHeight) ? -movementY : movementY;
if (movementX == 0 && movementY == 0)
{
movementX = (currentX > halfWidth) ? -1 : 1;
movementY = (currentY > halfHeight) ? -1 : 1;
}
if (movementX < 0 && movementY < 0)
Direction = MovingDirection.Upper_Left;
else if (movementX < 0 && movementY == 0)
Direction = MovingDirection.Left;
else if (movementX < 0 && movementY > 0)
Direction = MovingDirection.Down_Left;
else if (movementX == 0 && movementY < 0)
Direction = MovingDirection.Upper;
else if (movementX == 0 && movementY > 0)
Direction = MovingDirection.Downward;
else if (movementX > 0 && movementY < 0)
Direction = MovingDirection.Upper_Right;
else if (movementX > 0 && movementY > 0)
Direction = MovingDirection.Down_Right;
else if (movementX > 0 && movementY == 0)
Direction = MovingDirection.Right;
if (MovementCalculated != null) MovementCalculated();
restart = false;
}
}
In this RecalculateMovement method, I always check restart value
first, because the default is false, so when the first time Start is
called, it will fall to the if statement. I also have a piece of code
to determine the moving direction. And there is a small trick here, I
expose an Event, which will delegate the inherited class to do anything
interesting once it knows which direction is going. It will be useful
and you will see in the future post.
The UpdateMovement method is straightforward.
protected virtual void UpdateMovement()
{
currentX += movementX;
currentY += movementY;
Translate.X += movementX;
Translate.Y += movementY;
}
Points of Interest
I uploaded the source code of this file in
here, and you can take a look. Next
time I will show you how to inherit from this base class and do
something interesting.