Callback function when value equals specified number
I'm quite new in WPF-Animations so apologize if it's too easy, but I can't find any answer (nor my question). So: I have very simple animation - some ca开发者_StackOverflow中文版nvas is rotating from angle -45 degrees to 45 degrees. All animation is made in XAML (got some issues with code-behind animation). I would like to bind function when value equals 0 (e.g. make some noise then). How can I approach this? Thank you for all hints.
I have two options to solve this problem. One is intrusive but gives you more control over the actual value, another is not intrusive but gives you only indirect control over the value. I'll give the sample code with both options at the end of the answer.
Non intrusive solution
Subscribe to the CurrentTimeInvalidated event on your DoubleAnimation
object. If you know the animation function and its duration you can approximately say when the animation value is close to your event. For say, animation duration is 500 ms, and the animation function is linear. Then you can say, that at 250ms you are halfway through.
Intrusive solution
Remember: DoubleAnimation
(like any other animation) is just a class and you are welcome to inherit it and override any virtual member. In case of DoubleAnimation
of particular interest is GetCurrentValueCore()
method. And of course you can define any events or dependency properties on this new class. Now you see where it's all going. Inherit DoubleAnimation
, override GetCurrentValueCore()
, define ValueChanged
event, and fire it on every call to GetCurrentValueCore()
.
Code example
MainWindow.xaml
<Window x:Class="WpfPlayground.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
xmlns:l="clr-namespace:WpfPlayground">
<Grid>
<Grid.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard Duration="00:00:00.500" Storyboard.TargetName="rectangle" RepeatBehavior="Forever">
<l:DoubleAnimationWithCallback From="0"
To="180" Duration="00:00:00.500"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).(RotateTransform.Angle)"
Callback="{Binding AnimationCallback, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type l:MainWindow}}}"
CurrentTimeInvalidated="OnCurrentTimeInvalidated" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
<!--We animate this rectangle-->
<Rectangle x:Name="rectangle" Width="50" Height="50" Fill="Green">
<Rectangle.LayoutTransform>
<RotateTransform />
</Rectangle.LayoutTransform>
</Rectangle>
<!--Debug information-->
<TextBlock x:Name="tbTime" HorizontalAlignment="Center" VerticalAlignment="Top"/>
<TextBlock x:Name="tbAngle" HorizontalAlignment="Center" VerticalAlignment="Bottom"/>
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Animation;
using System.Globalization;
namespace WpfPlayground
{
public partial class MainWindow : Window
{
public Func<double, double> AnimationCallback { get { return AnimationCallbackImpl; } }
public MainWindow()
{
InitializeComponent();
}
private double AnimationCallbackImpl(double value)
{
tbAngle.Text = value.ToString(CultureInfo.CurrentCulture);
return value;
}
private void OnCurrentTimeInvalidated(object sender, EventArgs e)
{
tbTime.Text = ((AnimationClock)sender).CurrentTime.ToString();
}
}
}
DoubleAnimationWithCallback.cs
using System;
using System.Windows;
using System.Windows.Media.Animation;
namespace WpfPlayground
{
public class DoubleAnimationWithCallback : DoubleAnimation
{
// Cache Callback DP, to avoid performance hit.
private Func<double, double> _callback;
// reference to frozen instance. See comments below for explanation.
private DoubleAnimationWithCallback _coreInstance;
public Func<double, double> Callback
{
get { return (Func<double, double>)GetValue(CallbackProperty); }
set { SetValue(CallbackProperty, value); }
}
public static readonly DependencyProperty CallbackProperty =
DependencyProperty.Register("Callback", typeof(Func<double, double>), typeof(DoubleAnimationWithCallback), new PropertyMetadata(null, OnCallbackChanged));
private static void OnCallbackChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var dawc = o as DoubleAnimationWithCallback;
if (dawc != null)
{
dawc.UpdateCallback(e.NewValue as Func<double, double>);
}
}
private void UpdateCallback(Func<double, double> callback)
{
_callback = callback;
if (_coreInstance != null)
{
_coreInstance._callback = _callback;
}
}
protected override Freezable CreateInstanceCore()
{
if (_coreInstance == null)
{
// When callback changes we update corresponding callback on
// the frozen object too.
_coreInstance = new DoubleAnimationWithCallback()
{
Callback = Callback
};
}
return _coreInstance;
}
protected override double GetCurrentValueCore(double defaultOriginValue, double defaultDestinationValue, AnimationClock animationClock)
{
var value = base.GetCurrentValueCore(defaultOriginValue, defaultDestinationValue, animationClock);
if (_callback != null)
{
return _callback(value);
}
return value;
}
}
}
There is one caveat though: animation pipeline works with Freezable
objects, so you'll have to override CreateInstanceCore()
method and return proper instance. Furthermore, if you change Callback
dependency property on the real object you'll have to also update the frozen one. It is not quite welcome practice and that's why I call it intrusive. Be very careful with this code and test it throughly. It just shows a possible direction and is not the final destination.
Hope this helps
精彩评论