Animate Margin Change in Silverlight - silverlight

I am animating a border resize in Silverlight however I also need to gradually remove the margin around it (currently 50). Blend doesn't seem to generate a tween for margin change - it just jumps from 50 to 0 in one go. Is there a way to achieve this?

The problem is that a Margin is really of type "System.Windows.Thickness" which is NOT a dependency object, thus Left, Top, Right, and Bottom are NOT Dependency Properties and thus cannot be animated using DoubleAnimation (which allows for tweening).
What is used to animate the Margin is an ObjectAnimation which does not tween. This is why you see the margin jump from its original location to its new location. As another common example, the same happens when you try to animate the Visibility property between Visible and Collapsed.
You would either need to do timer based animation in order to animate margin or implement your own Animation type for Thickness objects.

Ben Lemmon gives an elegant solution: http://blogs.msdn.com/blemmon/archive/2009/03/18/animating-margins-in-silverlight.aspx

Here is an updated version that allows you to animate from within XAML
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace NiceCards.Animations
{
public class ThicknessAnimationX
{
public static readonly DependencyProperty ElementProperty = DependencyProperty.RegisterAttached("Element", typeof(DependencyObject), typeof(DoubleAnimation), new PropertyMetadata(new PropertyChangedCallback(OnElementPropertyChanged)));
// The time along the animation from 0-1
public static DependencyProperty TimeProperty = DependencyProperty.RegisterAttached("Time", typeof(double), typeof(DoubleAnimation), new PropertyMetadata(OnTimeChanged));
// The object being animated
public static DependencyProperty TargetProperty = DependencyProperty.RegisterAttached("Target", typeof(DependencyObject), typeof(ThicknessAnimationX), null);
public static DependencyProperty TargetPropertyProperty = DependencyProperty.RegisterAttached("TargetProperty", typeof(DependencyProperty), typeof(DependencyObject), null);
public static readonly DependencyProperty FromProperty = DependencyProperty.RegisterAttached("From", typeof(Thickness), typeof(DoubleAnimation), null);
public static readonly DependencyProperty ToProperty = DependencyProperty.RegisterAttached("To", typeof(Thickness), typeof(DoubleAnimation), null);
public static void SetElement(DependencyObject o, DependencyObject value)
{
o.SetValue(ElementProperty, value);
}
public static DependencyObject GetElement(DependencyObject o)
{
return (DependencyObject)o.GetValue(ElementProperty);
}
private static void OnElementPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != null)
{
DoubleAnimation doubleAnimation = (DoubleAnimation)d;
doubleAnimation.SetValue(TargetProperty, e.NewValue);
doubleAnimation.From = 0;
doubleAnimation.To = 1;
doubleAnimation.SetValue(TargetPropertyProperty, FrameworkElement.MarginProperty);
Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("(ThicknessAnimationX.Time)"));
Storyboard.SetTarget(doubleAnimation, doubleAnimation);
}
}
private static void OnTimeChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
DoubleAnimation animation = (DoubleAnimation)sender;
double time = GetTime(animation);
Thickness from = (Thickness)sender.GetValue(FromProperty);
Thickness to = (Thickness)sender.GetValue(ToProperty);
DependencyProperty targetProperty = (DependencyProperty)sender.GetValue(TargetPropertyProperty);
DependencyObject target = (DependencyObject)sender.GetValue(TargetProperty);
target.SetValue(targetProperty, new Thickness((to.Left - from.Left) * time + from.Left,
(to.Top - from.Top) * time + from.Top,
(to.Right - from.Right) * time + from.Right,
(to.Bottom - from.Bottom) * time + from.Bottom));
}
public static double GetTime(DoubleAnimation animation)
{
return (double)animation.GetValue(TimeProperty);
}
public static void SetTime(DoubleAnimation animation, double value)
{
animation.SetValue(TimeProperty, value);
}
public static Thickness GetFrom(DoubleAnimation animation)
{
return (Thickness)animation.GetValue(FromProperty);
}
public static void SetFrom(DoubleAnimation animation, Thickness value)
{
animation.SetValue(FromProperty, value);
}
public static Thickness GetTo(DoubleAnimation animation)
{
return (Thickness)animation.GetValue(ToProperty);
}
public static void SetTo(DoubleAnimation animation, Thickness value)
{
animation.SetValue(ToProperty, value);
}
}
}
And then you can do this in XAML
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="Positions">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:0.2"/>
</VisualStateGroup.Transitions>
<VisualState x:Name="Left">
<Storyboard>
<DoubleAnimation Duration="0:0:0.3" NiceCards:ThicknessAnimationX.To="0,0,0,0" NiceCards:ThicknessAnimationX.Element="{Binding ElementName=rectangle1}" Storyboard.TargetName="rectangle1" Storyboard.TargetProperty="Opacity"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Right">
<Storyboard>
<DoubleAnimation Duration="0:0:0.3" NiceCards:ThicknessAnimationX.To="0,200,0,0" NiceCards:ThicknessAnimationX.Element="{Binding ElementName=rectangle1}" Storyboard.TargetName="rectangle1" Storyboard.TargetProperty="Opacity"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Rectangle Height="100" HorizontalAlignment="Left" Margin="23,25,0,0" x:Name="rectangle1" Stroke="Black" StrokeThickness="1" VerticalAlignment="Top" Width="200" Fill="#FF1BAA00"/>
Note that if you don't set a Target property to a DoubleAnimation in XAML, you won't be able to display the control/page in Blend. To fix this, just add a fake target property (in the code above I added the opacity property which is a double value), And it will be overriden at runtime anyway

Related

How to disable the WPF ScrollViewer to refresh the view

I am using a stack panel for displaying a slideshow. The stack panel contains two images.
Each image is bound to the View Model containing among other properties the path to the image on the disk. In the general case there are more than 2 images in the list.
There is an endless while loop used for changing the current and next pointer to the image and in this way after the slide transition is finished the the current image becomes previous and the next becomes current.
The slide transition is made by changing the scroll viewer from 0 to 100% of scrollable extended area.
This works and even very fast. It is the easiest way to implement a slide transition for an endless list of images using MVVM pattern.
My problem is that from time to time the slide transition slips and doesn't finishes at exact 100% visually but the storyboard animation completed event is triggered.
In the ViewModel I am moving the Current and Next poitners and the image blinks. Some flickering is visible from time to time.
I need to find a way on how to temporarily disable the ScrollViewer to display any visual change when I am repositioning the scoller from 100% to 0%.
I need to invalidate the WPF ScrollViewer only temporarily until I change the scroll position from 100% to 0% but this must not be visible on screen.
I mean the values must be changed but the screen must be frozen.
Is it possible somehow ?
XAML
<UserControl.Resources>
<Storyboard x:Key="sbVerticalSlideEasingTransition"
AutoReverse="True" RepeatBehavior="Forever">
<Storyboard x:Key="sbVerticalSlideTransition"
FillBehavior="HoldEnd">
<!--SCROLL TO THE MULTIPLIER OF THE HEIGHT (BASICALLY AS THE PERCENTAGE)-->
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="Mediator"
Storyboard.TargetProperty="ScrollableHeightMultiplier">
<LinearDoubleKeyFrame KeyTime="00:0:0" Value="0" />
<LinearDoubleKeyFrame KeyTime="00:0:5" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</UserControl.Resources>
<Grid x:Name="gdContainer" Background="Black">
<ScrollViewer x:Name="Scroller" Visibility="{Binding Path=IsSlider, Mode=TwoWay, NotifyOnSourceUpdated=True, ElementName=slidePlayer,
Converter='{StaticResource BoolToVisibility}'}"
VerticalScrollBarVisibility="Hidden"
HorizontalScrollBarVisibility="Disabled">
<StackPanel x:Name="ScrollPanel">
<ctrl:MediaElementEx x:Name="slide1" Height="{Binding Path=ActualHeight, ElementName=gdContainer}" Width="{Binding Path=ActualWidth, ElementName=gdContainer}" />
<ctrl:MediaElementEx x:Name="slide2" Height="{Binding Path=ActualHeight, ElementName=gdContainer}" Width="{Binding Path=ActualWidth, ElementName=gdContainer}" />
</StackPanel>
</ScrollViewer>
<!-- Mediator that forwards the property changes -->
<cmn:ScrollViewerOffsetMediator x:Name="Mediator" ScrollViewer="{Binding ElementName=Scroller}"/>
</Grid>
ScrollViewer Mediator
This is used to bind the ScrollOffset of the scrollviewer that is not bindable directly
/// <summary>
/// Mediator that forwards Offset property changes on to a ScrollViewer
/// instance to enable the animation of Horizontal/VerticalOffset.
/// </summary>
public class ScrollViewerOffsetMediator : FrameworkElement
{
/// <summary>
/// ScrollViewer instance to forward Offset changes on to.
/// </summary>
public ScrollViewer ScrollViewer
{
get { return (ScrollViewer)GetValue(ScrollViewerProperty); }
set { SetValue(ScrollViewerProperty, value); }
}
public static readonly DependencyProperty ScrollViewerProperty =
DependencyProperty.Register(
"ScrollViewer",
typeof(ScrollViewer),
typeof(ScrollViewerOffsetMediator),
new PropertyMetadata(OnScrollViewerChanged));
private static void OnScrollViewerChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var mediator = (ScrollViewerOffsetMediator)o;
var scrollViewer = (ScrollViewer)(e.NewValue);
if (null != scrollViewer)
{
scrollViewer.ScrollToVerticalOffset(mediator.VerticalOffset);
}
}
/// <summary>
/// VerticalOffset property to forward to the ScrollViewer.
/// </summary>
public double VerticalOffset
{
get { return (double)GetValue(VerticalOffsetProperty); }
set { SetValue(VerticalOffsetProperty, value); }
}
public static readonly DependencyProperty VerticalOffsetProperty =
DependencyProperty.Register(
"VerticalOffset",
typeof(double),
typeof(ScrollViewerOffsetMediator),
new PropertyMetadata(OnVerticalOffsetChanged));
public static void OnVerticalOffsetChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var mediator = (ScrollViewerOffsetMediator)o;
if (null != mediator.ScrollViewer)
{
mediator.ScrollViewer.ScrollToVerticalOffset((double)(e.NewValue));
}
}
/// <summary>
/// HorizontalOffset property to forward to the ScrollViewer.
/// </summary>
public double HorizontalOffset
{
get { return (double)GetValue(HorizontalOffsetProperty); }
set { SetValue(VerticalOffsetProperty, value); }
}
public static readonly DependencyProperty HorizontalOffsetProperty =
DependencyProperty.Register(
"HorizontalOffset",
typeof(double),
typeof(ScrollViewerOffsetMediator),
new PropertyMetadata(OnHorizontalOffsetChanged));
public static void OnHorizontalOffsetChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var mediator = (ScrollViewerOffsetMediator)o;
if (null != mediator.ScrollViewer)
{
mediator.ScrollViewer.ScrollToHorizontalOffset((double)(e.NewValue));
}
}
/// <summary>
/// Multiplier for ScrollableHeight property to forward to the ScrollViewer.
/// </summary>
/// <remarks>
/// 0.0 means "scrolled to top"; 1.0 means "scrolled to bottom".
/// </remarks>
public double ScrollableHeightMultiplier
{
get { return (double)GetValue(ScrollableHeightMultiplierProperty); }
set { SetValue(ScrollableHeightMultiplierProperty, value); }
}
public static readonly DependencyProperty ScrollableHeightMultiplierProperty =
DependencyProperty.Register(
"ScrollableHeightMultiplier",
typeof(double),
typeof(ScrollViewerOffsetMediator),
new PropertyMetadata(OnScrollableHeightMultiplierChanged));
public static void OnScrollableHeightMultiplierChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var mediator = (ScrollViewerOffsetMediator)o;
var scrollViewer = mediator.ScrollViewer;
if (null != scrollViewer)
{
scrollViewer.ScrollToVerticalOffset((double)(e.NewValue) * scrollViewer.ScrollableHeight);
if (((double)e.NewValue == 0) && ((double)e.OldValue == 1))
{
Debug.WriteLine("[1] {0:HH:mm:ss.fff} ScrollableHeightMultiplier 1 => 0 ", DateTime.Now);
}
}
}
/// <summary>
/// Multiplier for ScrollableWidth property to forward to the ScrollViewer.
/// </summary>
/// <remarks>
/// 0.0 means "scrolled to top"; 1.0 means "scrolled to bottom".
/// </remarks>
public double ScrollableWidthMultiplier
{
get { return (double)GetValue(ScrollableWidthMultiplierProperty); }
set { SetValue(ScrollableWidthMultiplierProperty, value); }
}
public static readonly DependencyProperty ScrollableWidthMultiplierProperty =
DependencyProperty.Register(
"ScrollableWidthMultiplier",
typeof(double),
typeof(ScrollViewerOffsetMediator),
new PropertyMetadata(OnScrollableWidthMultiplierChanged));
public static void OnScrollableWidthMultiplierChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var mediator = (ScrollViewerOffsetMediator)o;
var scrollViewer = mediator.ScrollViewer;
if (null != scrollViewer)
{
scrollViewer.ScrollToHorizontalOffset((double)(e.NewValue) * scrollViewer.ScrollableWidth);
}
}
}

Animating a Double Collection Value at Runtime

Say I have a shape. I've given it a Stroke and StrokeThickness and a StrokeDashArray to get the desired dashed outline. Then I animate the StrokeDashOffset via VisualStateManager to get the "Marching Ants" style animation to it. Everything works great...
Except I want the default of the shape to NOT have a StrokeDashArray and instead want to set that based on a VisualState in the VisualStateManager except unfortunately as we know I can only do a DoubleAnimation on a Property, and not a Double Collection Value like what StrokeDashArray is...
My question is, is there a clever way I could animate that value during runtime so the shape in an UnSelected State has a solid Stroke but via the 'VisualStateManager' (maybe) still supply the StrokeDashOffset on the Selected State to the same shape? Or am I better off having two separate shapes and toggling the visibility between them so that each have their own default values?
If it would help visualize with a picture or something let me know and I'll add more to the question.
One option would be to animate a double property and create a new DoubleCollection that is bound to the StrokeDashArray.
Xaml:
<UserControl x:Class="SilverlightApplication1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="UI">
<Grid DataContext="{Binding ElementName=UI}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="StrokeDashArrayAnimation">
<Storyboard BeginTime="0">
<DoubleAnimation Duration="0:0:5"
From="0"
Storyboard.TargetName="UI"
Storyboard.TargetProperty="StrokeValue"
To="10" />
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Ellipse x:Name="lo"
Stroke="Red"
StrokeDashArray="{Binding StrokeArray}"
StrokeThickness="5" />
<Button Width="150"
Height="49"
Margin="29,65,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Click="Button_Click_1"
Content="Start" />
</Grid>
Code:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace SilverlightApplication1
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
}
public double StrokeValue
{
get { return (double)GetValue(StrokeValueProperty); }
set { SetValue(StrokeValueProperty, value); }
}
public static readonly DependencyProperty StrokeValueProperty =
DependencyProperty.Register("StrokeValue", typeof(double), typeof(MainPage),
new PropertyMetadata(0.0, OnStrokeValueChanged));
private static void OnStrokeValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var page = d as MainPage;
if (page != null) page.StrokeArray = new DoubleCollection { (double)e.NewValue, 1 };
}
public DoubleCollection StrokeArray
{
get { return (DoubleCollection)GetValue(StrokeArrayProperty); }
set { SetValue(StrokeArrayProperty, value); }
}
public static readonly DependencyProperty StrokeArrayProperty =
DependencyProperty.Register("StrokeArray", typeof(DoubleCollection), typeof(MainPage)
, new PropertyMetadata(new DoubleCollection { 0, 1 }));
private void Button_Click_1(object sender, RoutedEventArgs e)
{
VisualStateManager.GoToState(this, "StrokeDashArrayAnimation", false);
}
}
}

How to animate an Image in a button to shake every 30 seconds in WPF?

I not good when it comes to dealing with anything with styles and animations.
I was hoping to be able to get some help on making an Image that is the only content of a Button shake every 30 seconds when ever the buttons Visibility is set to Visibility.Visible.
It is to get the users attention to encourage them to click the button.
I would prefer to do this as an attached behavior on Image, or if possible even UIControl, to make it easily reusable instead of messing with the style, as I am already using a style from my control vendor, and I don't want to edit it.
Here is the solution I used derived from the marked answer
This is the Attached Behavior that can be applied to any System.Windows.Controls.Image.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Interactivity;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace SampleShakeBehavior
{
public class ShakeBehavior : Behavior<Image>
{
private const double DefaultRepeatInterval = 10.0;
private const double DefaultSpeedRatio = 1.0;
private const string RepeatIntervalName = "RepeatInterval";
private const string SpeedRatioName = "SpeedRatio";
public static readonly DependencyProperty RepeatIntervalProperty =
DependencyProperty.Register(RepeatIntervalName,
typeof(double),
typeof(ShakeBehavior),
new PropertyMetadata(DefaultRepeatInterval));
public static readonly DependencyProperty SpeedRatioProperty =
DependencyProperty.Register(SpeedRatioName,
typeof(double),
typeof(ShakeBehavior),
new PropertyMetadata(DefaultSpeedRatio));
/// <summary>
/// Gets or sets the time interval in in seconds between each shake.
/// </summary>
/// <value>
/// The time interval in in seconds between each shake.
/// </value>
/// <remarks>
/// If interval is less than total shake time, then it will shake
/// constantly without pause. If this is your intention, simply set
/// interval to 0.
/// </remarks>
public double RepeatInterval
{
get { return (double)GetValue(RepeatIntervalProperty); }
set { SetValue(RepeatIntervalProperty, value); }
}
/// <summary>
/// Gets or sets the ratio at which time progresses on the Shakes
/// Timeline, relative to its parent.
/// </summary>
/// <value>
/// The ratio at which time progresses on the Shakes Timeline, relative
/// to its parent.
/// </value>
/// <remarks>
/// If Acceleration or Deceleration are specified, this ratio is the
/// average ratio over the natural length of the Shake's Timeline. This
/// property has a default value of 1.0. If set to zero or less it
/// will be reset back to th default value.
/// </remarks>
public double SpeedRatio
{
get { return (double)GetValue(SpeedRatioProperty); }
set { SetValue(SpeedRatioProperty, value); }
}
private Style _orignalStyle;
protected override void OnAttached()
{
_orignalStyle = AssociatedObject.Style;
AssociatedObject.Style = CreateShakeStyle();
}
protected override void OnDetaching()
{
AssociatedObject.Style = _orignalStyle;
}
private Style CreateShakeStyle()
{
Style newStyle = new Style(AssociatedObject.GetType(), AssociatedObject.Style);
/**
* The following will replace/override any existing RenderTransform
* and RenderTransformOrigin properties on the FrameworkElement
* once the the new Style is applied to it.
*/
newStyle.Setters.Add(new Setter(UIElement.RenderTransformProperty, new RotateTransform(0)));
newStyle.Setters.Add(new Setter(UIElement.RenderTransformOriginProperty, new Point(0.5, 0.5)));
newStyle.Triggers.Add(CreateTrigger());
return newStyle;
}
private DataTrigger CreateTrigger()
{
DataTrigger trigger = new DataTrigger
{
Binding = new Binding
{
RelativeSource = new RelativeSource
{
Mode = RelativeSourceMode.FindAncestor,
AncestorType = typeof(UIElement)
},
Path = new PropertyPath(UIElement.IsVisibleProperty)
},
Value = true,
};
trigger.EnterActions.Add(new BeginStoryboard { Storyboard = CreateStoryboard() });
return trigger;
}
private Storyboard CreateStoryboard()
{
double speedRatio = SpeedRatio;
// Must be greater than zero
if (speedRatio <= 0.0)
SpeedRatio = DefaultSpeedRatio;
Storyboard storyboard = new Storyboard
{
RepeatBehavior = RepeatBehavior.Forever,
SpeedRatio = speedRatio
};
storyboard.Children.Add(CreateAnimationTimeline());
return storyboard;
}
private Timeline CreateAnimationTimeline()
{
DoubleAnimationUsingKeyFrames animation = new DoubleAnimationUsingKeyFrames();
animation.SetValue(Storyboard.TargetPropertyProperty, new PropertyPath("(0).(1)", UIElement.RenderTransformProperty, RotateTransform.AngleProperty));
int keyFrameCount = 8;
double timeOffsetInSeconds = 0.25;
double totalAnimationLength = keyFrameCount * timeOffsetInSeconds;
double repeatInterval = RepeatInterval;
// Can't be less than zero and pointless to be less than total length
if (repeatInterval < totalAnimationLength)
repeatInterval = totalAnimationLength;
animation.Duration = new Duration(TimeSpan.FromSeconds(repeatInterval));
int targetValue = 12;
for (int i = 0; i < keyFrameCount; i++)
animation.KeyFrames.Add(new LinearDoubleKeyFrame(i % 2 == 0 ? targetValue : -targetValue, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(i * timeOffsetInSeconds))));
animation.KeyFrames.Add(new LinearDoubleKeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(totalAnimationLength))));
return animation;
}
}
}
Here is how to use it in Xaml.
<Button>
<Image Source="myImage.png">
<i:Interaction.Behaviors>
<local:ShakeBehavior RepeatInterval="30" SpeedRatio="3.0"/>
</i:Interaction.Behaviors>
</Image>
</Button>
For a clear definition of an Attached Behavior you can look at the System.Windows.Interactivity.Behavior class remarks. Behaviors can optionally have Attached Properties with them as well make them very useful.
For a clear definition of an Attached Property you can read the Attached Properties Overview from MSDN. Attached properties can do anything, and they can be thought of as attached behaviors because they can trigger an action to occur causing an effective behavior, however technically they are still just an attached property.
Since an Attached Property can act like a behavior people have come to also call those types of Attached Properties an Attached Behavior, when in fact it is not really an Attached Behavior unless you derive from Behavior and at it to the attached property Interaction.Behaviors collection.
Blend is not required for any Attached Behavior or Attached Property, as with most things in WPF/Silverlight.
Here it is an attached behaviour. Just be careful as it may be destructive if your control has existing render transforms
public class Wibble
{
public static bool GetWobble(DependencyObject obj)
{
return (bool)obj.GetValue(WobbleProperty);
}
public static void SetWobble(DependencyObject obj, bool value)
{
obj.SetValue(WobbleProperty, value);
}
public static readonly DependencyProperty WobbleProperty = DependencyProperty.RegisterAttached("Wobble", typeof(bool), typeof(Wibble), new UIPropertyMetadata(false, new PropertyChangedCallback(OnWobbleChanged)));
private static void OnWobbleChanged(object sender, DependencyPropertyChangedEventArgs args)
{
var image = sender as Image;
if (image == null)
throw new InvalidOperationException("only images can wobble!");
// don't really need this check (the find ancestor binding would still find the button), but the spec said the image should be the only child of the button
var button = LogicalTreeHelper.GetParent(image) as Button;
if (button == null)
throw new InvalidOperationException("only images that are the only child of a button can wobble!");
var previousStyle = image.Style;
var newStyle = new Style(image.GetType(), previousStyle);
// this will override any existing render transform + origin on the button, hope they didn't already have one (and I'm too lazy to check)
newStyle.Setters.Add(new Setter(Image.RenderTransformProperty, new RotateTransform(0)));
newStyle.Setters.Add(new Setter(Image.RenderTransformOriginProperty, new Point(0.5, 0.5)));
var trigger = new DataTrigger();
var binding = new Binding();
var relativeSource = new RelativeSource();
relativeSource.Mode = RelativeSourceMode.FindAncestor;
relativeSource.AncestorType = typeof(Button);
binding.RelativeSource = relativeSource;
binding.Path = new PropertyPath(Button.VisibilityProperty);
trigger.Binding = binding;
trigger.Value = Visibility.Visible;
var storyboard = new Storyboard();
var animation = new DoubleAnimationUsingKeyFrames();
animation.SetValue(Storyboard.TargetPropertyProperty, new PropertyPath("(0).(1)", Image.RenderTransformProperty, RotateTransform.AngleProperty));
animation.Duration = new Duration(TimeSpan.FromSeconds(5)); // spec said 30, but i wanted to actually see it happen!
animation.KeyFrames.Add(new LinearDoubleKeyFrame(-12, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.2))));
animation.KeyFrames.Add(new LinearDoubleKeyFrame(12, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.4))));
animation.KeyFrames.Add(new LinearDoubleKeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.5))));
storyboard.Children.Add(animation);
storyboard.RepeatBehavior = RepeatBehavior.Forever;
var beginStoryboard = new BeginStoryboard();
beginStoryboard.Storyboard = storyboard;
beginStoryboard.Name = "its_wobble_time"; // it is
trigger.EnterActions.Add(beginStoryboard);
var removeStoryboard = new RemoveStoryboard();
removeStoryboard.BeginStoryboardName = beginStoryboard.Name;
trigger.ExitActions.Add(removeStoryboard);
newStyle.Triggers.Add(trigger);
image.Style = newStyle;
}
}
here is how it would be used:
<Button Width="100" Height="25" >
<Image Source="Untitled.png" xmlns:local="clr-namespace:WpfApplication17" local:Wibble.Wobble="True" />
</Button>
Create a WPF custom control by adding a new item in VS and then navigating to the WPF templates. This will allow you to select "Custom Control (WPF)". Name it "ShakyImageControl". This will create a Themes folder with a generic.xaml in it and a "ShakyImageControl.cs" class file. In the generic.xaml replace the existing style with the following:
<Style TargetType="{x:Type local:ShakyImageControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ShakyImageControl}">
<Image x:Name="image" Source="{TemplateBinding ImageSource}" RenderTransformOrigin="0.5,0.5">
<Image.RenderTransform>
<TransformGroup>
<RotateTransform x:Name="Rotaty"/>
</TransformGroup>
</Image.RenderTransform>
</Image>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Button}, Path=Visibility}" Value="Visible">
<DataTrigger.EnterActions>
<BeginStoryboard Name="fred">
<Storyboard AutoReverse="False" RepeatBehavior="Forever" Duration="0:0:30" Storyboard.TargetName="Rotaty" Storyboard.TargetProperty="Angle">
<DoubleAnimationUsingKeyFrames>
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="-12.0"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.4" Value="12.0"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<StopStoryboard BeginStoryboardName="fred"/>
</DataTrigger.ExitActions>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
In the ShakyImageControl class add a dependency property as follows:
static ShakyImageControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ShakyImageControl), new FrameworkPropertyMetadata(typeof(ShakyImageControl)));
}
public ImageSource ImageSource
{
get { return (ImageSource)GetValue(ImageSourceProperty); }
set { SetValue(ImageSourceProperty, value); }
}
// Using a DependencyProperty as the backing store for ImageSource. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ImageSourceProperty =
DependencyProperty.Register("ImageSource", typeof(ImageSource), typeof(ShakyImageControl), new UIPropertyMetadata(null));
To use the shakyImage in a button just do:
<Button Height="50" Width="500" Name="showy" Visibility="Collapsed">
<local:ShakyImageControl ImageSource="\Expand.png"/>
</Button>
local is an xml namespace like "xmlns:local="clr-namespace:WpfApplication6"
NB: your custom control can be in a seperate assembly if you want

Very strange problem in Silverlight with XAML, custom property and animation

I created a custom user control that has a property of type Storyboard. Something like:
public class UC : UserControl
{
public static readonly DependencyProperty AnimationProperty = DependencyProperty.Register(
"Animation",
typeof(Storyboard),
typeof(UC),
null);
public Storyboard Animation
{
get { return (Storyboard)GetValue(AnimationProperty); }
set { SetValue(AnimationProperty, value); }
}
public UC()
{
this.Loaded += new RoutedEventHandler(UC_Loaded);
}
private void UC_Loaded(object sender, RoutedEventArgs e)
{
if (this.Animation != null)
{
this.Animation.Begin();
}
}
}
In XAML I used it as follows:
<loc:UC x:Name="uc" Opacity="0" >
<TextBlock FontSize="50">Some text</TextBlock>
<loc:UC.Animation>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="uc" Storyboard.TargetProperty="Opacity" To="0.5" Duration="0:0:1" />
</Storyboard>
</loc:UC.Animation>
</loc:UC>
So far so good.
Later on I decided that I needed another item along with the storyboard. So I changed the code to accept a custom object that contains the storyboard and another piece of information. Something like:
public class UC : UserControl
{
public static readonly DependencyProperty AnimationProperty = DependencyProperty.Register(
"Animation",
typeof(AnimationHolder),
typeof(UC),
null);
public AnimationHolder Animation
{
get { return (AnimationHolder)GetValue(AnimationProperty); }
set { SetValue(AnimationProperty, value); }
}
public UC()
{
this.Loaded += new RoutedEventHandler(UC_Loaded);
}
private void UC_Loaded(object sender, RoutedEventArgs e)
{
if (this.Animation != null)
{
this.Animation.Animation.Begin();
}
}
}
public class AnimationHolder
{
public Storyboard Animation
{
get;
set;
}
public int OtherValue
{
get;
set;
}
}
And used it in XAML:
<loc:UC x:Name="uc" Opacity="0" >
<TextBlock FontSize="50">Some text</TextBlock>
<loc:UC.Animation>
<loc:AnimationHolder OtherValue="20">
<loc:AnimationHolder.Animation>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="uc" Storyboard.TargetProperty="Opacity" To="0.5" Duration="0:0:0" />
</Storyboard>
</loc:AnimationHolder.Animation>
</loc:AnimationHolder>
</loc:UC.Animation>
</loc:UC>
However, now when I try to begin the animation I get an InvalidOperationException with the message: "Cannot resolve TargetName uc."
Anyone has an idea why?
I know that in this example I can work around the problem by not using a custom object, and using another property instead. However this is a simplified version of my scenario, which focuses at the problem. In the real scenario I must use a custom object.
Two things need to be done with your new approach:
In AnimationHolder class, make Animation property a dependency property, just like it previously was.
Derive AnimationHolder class from DependencyObject. This is necessary so that you can make Animation a dependency property. Please remember that only classes deriving from DependencyObject can define dependency properties!
I think once you do these two things, it'll solve your problem!

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 canvas 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

Resources