This article is compatible with the latest version of Silverlight.
Overview
In this article we will present an animated image slider that we made for SilverlightShow.net. We will use it in our highlight control, which will be ready soon and will replace the highlights on our front page. Our goal is to have a slider which loads images from Uri location and shows a limited number of them. It also should use transition animation to switch between hidden and visible images.
Download source code
Principle
The basic things we need are two buttons to slide left and right and a canvas, which contains the visible images. But we also want an image to be selectable so we’ll have a rectangle to indicate the selected image. And this is enough to contain and present our images.
When it comes to implementation of the sliding animation we thought of two approaches.
- We load all images and position them one after the other. Then we show only a number of them and we move all images so some of them begin to appear and some of them begin to disappear from the control.
- How we implemented the control – we load only the images which are showed and every time we slide we accomplish a visual illusion.
In short we have: transition animation (1-2), reloading new images (2-3), restoring original position (3-4). The loading and the restoring of their original positions is done very quick so (3) can’t be seen and the user sees no change. (The visible area for the user is between the two orange lines.)
1.
2.
3.
4.
User sees 1, 2, 3, 4.
This implementation resembles what we would normally do if we wanted to scroll through images without animation – just have a number of Imageobjects and change their Source property. Actually, we are doing just that but immediately after the animation.
Implementation
For each image that we animate we use AnimationImage class. It extends Control and contains one Image and two Storyboard objects. Using Storyboard we do transformations of different objects in time. A Storyboard contains animations, one animation for each property that we want to animate. This means we have one Storyboardfor animating left movement and one for right.
<Canvas.Resources>
<Storyboardx:Name="left">
<DoubleAnimationStoryboard.TargetName="image"Duration="0:0:0.2" Storyboard.TargetProperty="Canvas.RenderTransform.TranslateTransform.X"
By="0"
x:Name="translateLeft"/>
Storyboard>
<Storyboardx:Name="right">
<DoubleAnimationStoryboard.TargetName="image"Duration="0:0:0.2"
Storyboard.TargetProperty="Canvas.RenderTransform.TranslateTransform.X"
By="0"
x:Name="translateRight"/>
Storyboard>
Canvas.Resources>
And want to can change By so we get C# object in the constructor of the control:
. . .
leftAnimation = this.parentCanvas.FindName( "left" ) as Storyboard;
translateLeft = leftAnimation.Children.FindName( "translateLeft" ) asDoubleAnimation;
. . .
And we make this property changeable in C#:
public double AnimateBy
{
get { return (double)translateLeft.By; }
set
{
translateLeft.By = - value;
translateRight.By = value;
}
}
We also want to know when the animation is over and when an image is clicked so we hook these three events:
image.MouseLeftButtonUp
+= new MouseEventHandler(ImageMouseLeftButtonUp);
leftAnimation.Completed += new EventHandler( AnimationLeftCompleted );
rightAnimation.Completed += new EventHandler( AnimationRightCompleted );
Now we are ready to use this class in our ImageSlide. Besides moving images we need buttons for left and right, and frame to indicate the user’s selection. To implement buttons we wrote ActionButton, which has disabled and enabled state and image for each of them.
public bool IsDisabled
{ set
{
isDisabled = value;
if( value )
{
disabledImage.Visibility = Visibility.Visible;
enabledImage.Visibility = Visibility.Collapsed;
}
else
{
enabledImage.Visibility = Visibility.Visible;
disabledImage.Visibility = Visibility.Collapsed;
}
}
}
Now let’s talk more on the slide itself. We want the images to slide from outside the visible area. This means we need to have two more moveable images than the number of images we want to display. On the following picture we have additional image on the right and no additional on the left. Now imagine we slide this to left and to right. (Remember after the slide we bring the images to their original position.)
This leads to the problem that first and last images can’t be seen. The problem is easily solved by adding two additional images in the Uri collection from which we are loading images.
privateCollection<Uri> locationsList;
These additional images are never shown in the visible are of the user.
The animations we are using are kept in a collection too:
privateCollection<AnimationImage> animationsList;
Since we are loading images each time we slide we need to load them fast from the memory, so we use a collection of Image objects to cache the image content:
privateCollection<Image> imagesList;
Each time we slide, a new image is loaded in the cache if it isn’t there yet.
private void ReloadVisibleImages()
{
while( offset + animationsList.Count > imagesList.Count )
{
imagesList[ imagesList.Count - 1 ].Source
= locationsList[ imagesList.Count - 1 ];
imagesList.Add( new Image() );
}
imagesList[ imagesList.Count - 1 ].Source
= locationsList[ imagesList.Count - 1 ];
for( int i = 0 ; i < animationsList.Count ; i++ )
{
animationsList[ i ].ImageSource = imagesList[ i + offset ].Source;
}
}
Now let’s see how we are adding new images to the list.
public void AddImage( Uri fileUrl )
{
//we change the last image to the new
locationsList[ locationsList.Count - 1 ] = fileUrl;
//add new empty image to "push" the last image into the visible area
//this image is never seen
locationsList.Add( newUri( "images/empty.png", UriKind.Relative ));
//there are alway 2 images outside the visible area, so
//if we have more than 3 in the list we need to enable the right
//buttonfor pushing
if( locationsList.Count > 3 )
RightButton.IsDisabled = false;
//This we will explain next
if ( visibleImagesTargetCount >= locationsList.Count )
{
animatedImagesCount++;
Width = ( animatedImagesCount - 2 ) * ( imageWidth + ( 2 * Margin ) ) + LeftButton.Width + RightButton.Width;
parentCanvas.Width = Width - LeftButton.Width - RightButton.Width;
dataCanvas.Width = Width - LeftButton.Width - RightButton.Width;
clippingRect.Width = Width - LeftButton.Width - RightButton.Width;
RefreshSize();
RefreshVisibleImagesCount();
clippingGeometry.Rect = clippingRect;
}
//Sets the last animation on the right the correct image
animationsList[ animationsList.Count - 1 ].ImageSource = locationsList[ animationsList.Count - 1 ];
}
Here we see the int visibleImagesTargetCount. Each time the user of the control adds new image we add new animated image. This is true until we reach the desired number of animated images we want (the user) to be shown. visibleImagesTargetCount is this desired number.What we have in the last if block is adding animated image and resizing the control.
Here is how we keep track of how many animations are desired
public int VisibleImages
{
get { return animatedImagesCount - 2; }
set
{
animatedImagesCount = value + 2;
visibleImagesTargetCount = value + 2;
if ( animatedImagesCount > locationsList.Count )
animatedImagesCount = locationsList.Count;
RefreshSize();
RefreshVisibleImagesCount();
}
}
In case we don’t have enough locations of images we cut the animated images to the number we have and later we add animated images later in the code above the last.
After we have added images and the control is initialized we may start sliding. In case the user clicks on an image we simply change the selection
void ImageClicked( object sender, MouseEventArgs e )
{
SelectedVisibleImageIndex = ( int )( e.GetPosition( dataCanvas ).X / ( imageWidth + 2 * Margin ) );
selectionFrame.Reset();
selectionFrame.SetValue<double>( Canvas.LeftProperty,
( int )( value * ( imageWidth + 2 * Margin ) ) );
. . .
And send message to the consumer of the control
private int SelectedVisibleImageIndex
{
set
{
selectedVisibleImageIndex = value;
if( this.SelectionChange != null )
this.SelectionChange( this,
new SelectedItemChanged( offset + selectedVisibleImageIndex ) ); }
It is more interesting to see what happens when the user clicks on the left/right button. It is clear that the animation is done here but since we are depending on the animation to complete we need to lock the buttons until all animations are done, and this way all the AnimatedImage object have loaded new images.
private void MoveToRight( object sender, MouseEventArgs e )
{
// current animanition is not finished
if( readyAnimationsCount != animationsList.Count )
return;
Next we check if we have reached the last image the user is supposed to see. I we haven’t we start the animation.
if( offset + animatedImagesCount < locationsList.Count )
{
offset++;
SelectedImage -= 1; // change the selection
// slide the selection frame if it is visible
if( SelectedImage >= -1 && SelectedImage < animatedImagesCount - 1 )
selectionFrame.AnimateLeft();
readyAnimationsCount = 0;
isReloading = true;
foreach( AnimationImage i in animationsList )
i.AnimateLeft(););// slide the images
LeftButton.IsDisabled = false;
}
If we have reached the end and we have selected the last image we need to disable the button.
if( offset + SelectedVisibleImageIndex + 1 == locationsList.Count - 2)
RightButton.IsDisabled = true;
}
Now we have started the animation. For each animation we receive a message on its completion. When the last animation is completed readyAnimationsCount is equal to animationsList.Count and so the button is unlocked.
private void ImageAnimationCompleted( object sender, AnimationImageEventArgs e )
{
readyAnimationsCount++;
if( isReloading )
{
isReloading = false;
ReloadVisibleImages();
}
}
We need to reload the images immediately after the animations completion but only once so we change isReloading to false.
Summary
At first sight a slider isn’t something complicated but adding animation to it makes things a little different. The design of our solution is probably not the best but the control is not breaking the illusion even when the CPU is under heavy load. It would be better if the loading is done out of the visible area of the control. Still we didn’t catch any problems but if you see any, don’t hesitate to notify us or leave your comments.