I am using Microsoft Interactivity and Microsoft Interactions to rotate an object based on a Property in my code-behind. To make the rotation more smooth I added in an easing function. It does the animation perfectly fine but when it reaches the end of the animation for 1 split frame the rotation resets to the value it was before the animation and then switches back to the value after the rotation, causing it to 'twitch' back and forth. This only happens on EaseOut.
<i:Interaction.Triggers>
<ie:PropertyChangedTrigger Binding="{Binding Rotation}">
<ie:ChangePropertyAction TargetName="RotateTransformer" PropertyName="Angle" Value="{Binding Rotation}" Duration="0:0:2">
<ie:ChangePropertyAction.Ease>
<BackEase EasingMode="EaseOut" Amplitude="1.2" />
</ie:ChangePropertyAction.Ease>
</ie:ChangePropertyAction>
</ie:PropertyChangedTrigger>
</i:Interaction.Triggers>
<Path Stroke="Black" Fill="Gray">
<Path.RenderTransform>
<RotateTransform x:Name="RotateTransformer" CenterX="64" CenterY="105" />
</Path.RenderTransform>
<Path.Data>
<PathGeometry>
<PathFigureCollection>
<PathFigure StartPoint="64,0" >
<LineSegment Point="39,110" />
<LineSegment Point="64, 70" />
<LineSegment Point="39,180" />
<LineSegment Point="89, 180" />
<LineSegment Point="64,70"/>
<LineSegment Point="89,110" />
<LineSegment Point="64,0" />
</PathFigure>
</PathFigureCollection>
</PathGeometry>
</Path.Data>
</Path>
As this seems to be a bug in the implementation of the ChangePropertyAction class, I figured the best way to get to the bottom of this is to throw the assembly into your favorite reflector style app, and look at the guts of the implementation.
Here's an excerpt (there's a lot left out, but the relevant bit is in there though):
public class ChangePropertyAction : TargetedTriggerAction<object>
{
/* some dependency properties here, like DurationProperty, ValueProperty, etc... */
protected override void Invoke(object parameter)
{
/* a lot of validation here, but skimming over that mostly. Valid input results in a call to AnimatePropertyChange() */
}
private void AnimatePropertyChange(PropertyInfo propertyInfo, object fromValue, object newValue)
{
Storyboard storyboard = new Storyboard();
Timeline timeline = !typeof (double).IsAssignableFrom(propertyInfo.PropertyType)
? (!typeof (Color).IsAssignableFrom(propertyInfo.PropertyType)
? (!typeof (Point).IsAssignableFrom(propertyInfo.PropertyType)
? this.CreateKeyFrameAnimation(fromValue, newValue)
: this.CreatePointAnimation((Point) fromValue, (Point) newValue))
: this.CreateColorAnimation((Color) fromValue, (Color) newValue))
: this.CreateDoubleAnimation((double) fromValue, (double) newValue);
timeline.Duration = this.Duration;
storyboard.Children.Add(timeline);
Storyboard.SetTarget((Timeline) storyboard, (DependencyObject) this.Target);
Storyboard.SetTargetProperty((Timeline) storyboard, new PropertyPath(propertyInfo.Name, new object[0]));
storyboard.Completed += (EventHandler) ((o, e) => propertyInfo.SetValue(this.Target, newValue, new object[0]));
storyboard.FillBehavior = FillBehavior.Stop;
storyboard.Begin();
}
private static object GetCurrentPropertyValue(object target, PropertyInfo propertyInfo)
{
FrameworkElement frameworkElement = target as FrameworkElement;
target.GetType();
object obj = propertyInfo.GetValue(target, (object[]) null);
if (frameworkElement != null && (propertyInfo.Name == "Width" || propertyInfo.Name == "Height") && double.IsNaN((double) obj))
obj = !(propertyInfo.Name == "Width") ? (object) frameworkElement.ActualHeight : (object) frameworkElement.ActualWidth;
return obj;
}
private Timeline CreateDoubleAnimation(double fromValue, double newValue)
{
return (Timeline) new DoubleAnimation()
{
From = new double?(fromValue),
To = new double?(newValue),
EasingFunction = this.Ease
};
}
}
If you want to look at the full code, run it through DotPeek or ILSpy yourself, both are free :-)
So in the end, all it's doing is validating inputs, looking at the type of the value and create a storyboard with a transition animation appropriate to the property type. The 'flicker' effect is actually the value briefly returning to its original value (that which is actually bound) when the animation is done, after which the binding is updated to reflect the new value. The reason for this behavior is down to one single property setting on the storyboard:
storyboard.FillBehavior = FillBehavior.Stop;
This FillBehavior determines what happens when the Timeline (the storyboard in this case) reaches its end. MSDN has this to say:
HoldEnd: After it reaches the end of its active period, the timeline holds its progress until the end of its parent's active and hold periods.
Stop: The timeline stops if it is outside its active period while its parent is inside its active period.
If we simply change this property to be set to FillBehavior.HoldEnd, the flicker is gone. The downside is that you'll have to re-implement this TriggerAction, but you can probably leave out a lot if you just want it to work for a double animation.
Hope this helps anyone!
One thing I noticed, WRONG!CenterX
and CenterY
should be a decimal and less than 1, e.g. CenterY="0.45" CenterX="0.4"
UPDATE
After spending some time playing with ChangePropertyAction
, I found the animation will always have that flicker no matter what easing function you choose.
I think it is a bug...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With