I have designed several animated Panel
s in C# that all inherit from one base class that performs the actual animations on the Panel.Children
. Up until recently, they've all worked perfectly... but recently, I was using one with a collection that had a large number of items in it and I started getting some strange visual errors.
When the application first loads the items, the first x number of items display correctly and then the remainder are all displayed in the first position (on top of each other). If I resize the window or select an item, they all rearrange themselves to display correctly. (Using the example code below, you will have to resize the window to see them correct themselves.)
After a lot of head scratching and debugging, I discovered that the correct values were still getting through to the animation code for each item, but the last x items were simply not being animated. Each item definitely passes through the animation code, but the last x items stay in the position set in the animated Panel.ArrangeOverride
method.
In order to try to locate a problem in my code, I created a new UserControl
and animated Panel
class with the minimum code required to recreate the problem. Having done so, I was surprised that the problem was still reproducible. With 141 items in my example code, there was no display problem, but with any more, the problem appears.
Please excuse the long lines of code, but there is so much to display.
AnimatedStackPanel.cs class
public class AnimatedStackPanel : Panel
{
protected override Size MeasureOverride(Size availableSize)
{
double x = 0, y = 0;
foreach (UIElement child in Children)
{
child.Measure(availableSize);
x = Math.Max(x, availableSize.Width == double.PositiveInfinity ? child.DesiredSize.Width : availableSize.Width);
y += child.DesiredSize.Height;
}
return new Size(availableSize.Width == double.PositiveInfinity ? x : availableSize.Width, availableSize.Height == double.PositiveInfinity ? y : availableSize.Height);
}
protected override Size ArrangeOverride(Size finalSize)
{
if (this.Children == null || this.Children.Count == 0) return finalSize;
Size previousChildSize = new Size();
Point endPosition = new Point(0, 0);
foreach (UIElement child in Children)
{
endPosition.Y += previousChildSize.Height;
double childWidth = finalSize.Width;
child.Arrange(new Rect(0, 0, childWidth, child.DesiredSize.Height));
Point startPosition = new Point(endPosition.X, endPosition.Y + finalSize.Height);
AnimatePosition(child, startPosition, endPosition, new TimeSpan(0, 0, 1));
previousChildSize = child.DesiredSize;
}
return finalSize;
}
private void AnimatePosition(UIElement child, Point startPosition, Point endPosition, TimeSpan animationDuration)
{
DoubleAnimation xAnimation = new DoubleAnimation(startPosition.X, endPosition.X, animationDuration);
DoubleAnimation yAnimation = new DoubleAnimation(startPosition.Y, endPosition.Y, animationDuration);
TransformGroup transformGroup = child.RenderTransform as TransformGroup;
if (transformGroup == null)
{
TranslateTransform translatationTransform = new TranslateTransform();
transformGroup = new TransformGroup();
transformGroup.Children.Add(translatationTransform);
child.RenderTransform = transformGroup;
child.RenderTransformOrigin = new Point(0, 0);
}
TranslateTransform translateTransform = (TranslateTransform)transformGroup.Children[0];
translateTransform.BeginAnimation(TranslateTransform.XProperty, xAnimation, HandoffBehavior.Compose);
translateTransform.BeginAnimation(TranslateTransform.YProperty, yAnimation, HandoffBehavior.Compose);
//child.Arrange(new Rect(endPosition.X, endPosition.Y, child.DesiredSize.Width, child.DesiredSize.Height));
}
}
AnimatedListBox.xaml UserControl
<UserControl x:Class="WpfTest.Views.AnimatedListBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Controls="clr-namespace:WpfTest.Views.Controls">
<UserControl.Resources>
<ItemsPanelTemplate x:Key="AnimatedStackPanel">
<Controls:AnimatedStackPanel />
</ItemsPanelTemplate>
</UserControl.Resources>
<Grid>
<ListBox ItemsSource="{Binding Data}" ItemsPanel="{StaticResource AnimatedStackPanel}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" />
</Grid>
</UserControl>
AnimatedListBox.xaml.cs UserControl
code behind
public partial class AnimatedListBox : UserControl
{
public AnimatedListBox()
{
InitializeComponent();
Data = new ObservableCollection<int>();
// change this 150 below to anything less than 142 and the problem disappears!!
for (int count = 1; count <= 150; count++) Data.Add(count);
DataContext = this;
}
public static DependencyProperty DataProperty = DependencyProperty.Register("Data", typeof(ObservableCollection<int>), typeof(AnimatedListBox));
public ObservableCollection<int> Data
{
get { return (ObservableCollection<int>)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
}
While experimenting, I found something else that seems strange. If I change the position of the item in the Panel.ArrangeOverride
method to the following, there are more unexpected results.
child.Arrange(new Rect(endPosition.X, endPosition.Y, childWidth, child.DesiredSize.Height));
Somehow, this changes the actual (animation) end positions, ie. where the items end up being positioned. It introduces some kind of double spacing of the items. How can arranging the item in its end position before it starts its animation change its final end position? Am I missing something obvious?
Also, if you uncomment the commented line in the AnimatedStackPanel.AnimatePosition
method and comment out the two lines above (ie. cut out the animation and move the items directly to the same end positions that the animations would have moved them to) then you will see the problem disappear again... this applies no matter how many items are in the collection. This is what led me to think that the problem was animation related.
One last thing that I found out is that the problem exists with or without using DataTemplate
s for the item data type... I'm pretty sure that it's animation related. If anyone can see what I'm doing wrong, or find a solution for this, I'd be very grateful because this one has had me baffled. Can anyone even reproduce this problem with the example code? Many thanks in advance.
Sheridan, i played around with your code sample a bit and what i found out is that it apparently matters when you apply the RenderTransform. Instead of assigning it in
AnimatePosition
, i.e. during the first run of ArrangeOverride, i moved the code toMeasureOverride
:Now the animations behave as expected even on the first invocation of ArrangeOverride. Seems like the last few RenderTransforms were not yet attached to the control while already beeing animated.
By the way, the maximum number was 144 for me on two different systems (dual core and quad core) running 32-bit Windows 7.