Drafted by @rmarinho
This proposal aims to make animation more useful for VisualStateManager with the introduction of animation and storyboards with XAML support.
Extend the Animation api to be used on XAML by introducing Storyboard, we should be able to specify at least the animation between 2 double values, colors and positions, user can specify the TargetElement (by default the attached element), TargetProperty, From, To, Duration and Easing of the animation, this should then be translated to a Animation callback to be applied on the TargetElement ( by default that the VisualState is attached to). Storyboard should also add a Completed event, and a way to start and stop the animation from code behind.
VSM should allow transitions between states
public class VisualTransition
{
public string From { get; set; }
public string To { get; set; }
public Storyboard Animation { get; set; }
public double Duration { get; set; }
}
public sealed class VisualStateGroup
{
public string IList<VisualTransition> Transitions { get; }
}
[ContentProperty(nameof(Storyboard))]
public class VisualState
{
...
public Storyboard Storyboard { get; set; }
}
[ContentProperty(nameof(Children))]
public class Storyboard : Timeline, IList<Timeline>
{
public static void SetTargetName(BindableObject bindable, string value)
public static VisualElement GetTargetName(BindableObject bindable)
public TimelineCollection Children { get; }
public static readonly BindableProperty TargetNameProperty = BindableProperty.CreateAttached("TargetName", typeof(VisualElement), typeof(Timeline), null);
public static readonly BindableProperty TargetPropertyProperty = BindableProperty.CreateAttached("TargetProperty", typeof(BindableProperty), typeof(Timeline), null);
public static void SetTarget(BindableObject bindable, VisualElement value)
{
bindable.SetValue(TargetNameProperty, value);
}
public static VisualElement GetTarget(BindableObject bindable)
{
return (VisualElement)bindable.GetValue(TargetNameProperty);
}
public static void SetTargetProperty(BindableObject bindable, BindableProperty value)
{
bindable.SetValue(TargetPropertyProperty, value);
}
public static BindableProperty GetTargetProperty(BindableObject bindable)
{
return (BindableProperty)bindable.GetValue(TargetPropertyProperty);
}
public Storyboard()
{
Children = new TimelineCollection();
}
public void Add(Timeline animation)
{
Children.Add(animation);
}
public void Remove(Timeline animation)
{
Children.Remove(animation);
}
public void Begin();
public void End();
public void Pause();
public void Resume();
public void Loop();
}
public interface IAnimation
{
void Begin();
void End();
}
public interface IAnimation<T> : IAnimation
{
T From { get; set; }
T To { get; set; }
}
public interface IInterpolatable
{
object InterpolateTo(object from, object target, double interpolation);
}
public interface IInterpolatable<T> : IInterpolatable
{
T InterpolateTo(T from, T target, double interpolation);
}
public abstract class Timeline : BindableObject, IAnimation
{
protected Timeline()
{
Duration = defaultDuration;
Easing = Easing.SpringOut;
}
public uint BeginTime { get; set; }
public uint Duration { get; set; }
public bool Reverse { get; set; }
public bool Loop { get; set; }
public Easing Easing { get; set; }
public EventHandler Completed { get; set; }
public void Begin();
public void End();
}
public abstract class Animation<T> : Timeline
{
public abstract T From { get; set; }
public abstract T To { get; set; }
}
public abstract class InterpolatableAnimation<T> : Animation<T> where T : IInterpolatable<T>
{
}
public class PositionAnimation : Animation<Point>
{
}
public class DoubleAnimation : Animation<double>
{
public static readonly BindableProperty FromProperty = BindableProperty.Create(nameof(From), typeof(double), typeof(DoubleAnimation), .0);
public static readonly BindableProperty ToProperty = BindableProperty.Create(nameof(To), typeof(double), typeof(DoubleAnimation), .0);
public override double From
{
get { return (double)GetValue(FromProperty); }
set { SetValue(FromProperty, value); }
}
public override double To
{
get { return (double)GetValue(ToProperty); }
set { SetValue(ToProperty, value); }
}
}
public class TimelineCollection : List<Timeline>
{
}
[ContentProperty(nameof(Storyboard))]
public sealed class VisualState
{
...
public Storyboard Storyboard { get; set; }
}
public struct Color : IInterpolatable<Color>
[Xaml.TypeConversion(typeof(VisualElement))]
public sealed class VisualElementConverter : TypeConverter, IExtendedTypeConverter
{
}
[TypeConverter(typeof(VisualElementConverter))]
public partial class VisualElement : Element, IAnimatable, IVisualElementController, IResourcesProvider, IFlowDirectionController
Add a ,markup extension so the developer doesn t have to worry about the type of animation and write less xaml.
public class AnimateExtension : BindableObject, IMarkupExtension<Timeline>
{
public BindableProperty Property { set; get; }
public string Binding { set; get; }
public Timeline ProvideValue(IServiceProvider serviceProvider)
{
var anim = new PositionAnimation();
BindingBase GetBinding()
{
return new Binding(Binding);
}
anim.SetBinding(PositionAnimation.ToProperty, GetBinding());
Storyboard.SetTargetProperty(anim, Property);
return anim;
}
object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
{
return (this as IMarkupExtension<Timeline>).ProvideValue(serviceProvider);
}
}
If the specs are final, i would like to give it a try.
This is possibly a separate issue, but worth thinking about if animations are to be improved:
Is there any plan to be able to animate WidthRequest and HeightRequest properties to the elements "auto" width and height (i.e. what you'd get with -1)?
One thing I've wanted to do with the existing animation system is animate something becoming visible by height, from 0 to whatever its height would normally be. Getting the size of Forms elements before they've been rendered doesn't seem possible.
There is a Measure method, but it seems to always return 0,0 for elements that haven't been rendered.
Oh, love this spec. If it can help in someway: https://github.com/jsuarezruiz/Xamanimation
During the last days i have been playing with this spec here are some examples of usage and videos:
I played with the idea of auto animate automatically as the property changes, this way the developer doesn't need to actually call any Begin to start the storyboard
Example of usage:
<Frame x:Name="boxView">
<Frame.Triggers>
<AnimateTrigger>
<Animate Property="Frame.X" Binding="X" />
<PositionAnimation To="{Binding Y}" Storyboard.TargetProperty="Frame.Y" />
<DoubleAnimation To="{Binding Scale}" Storyboard.TargetProperty="Frame.Scale" />
<DoubleAnimation To="{Binding Rotation}" Storyboard.TargetProperty="Frame.Rotation" />
</AnimateTrigger>
</Frame.Triggers>
</Frame
Following feedback from @davidortinau the developer doesn't need to know all about the Animation API if he just wants to express that he wants to animate a given property he could Say
<Animate Property="Frame.X" Binding="X" />
or
<Animate Property="Frame.X" To="{Binding X}" />
Not sure the second will work with Styles.
Specify a Storyboard on the Page.Resources with a Storyboard.TargetName so we can trigger for example from other Element similar to EventTrigger.
<Page.Resources>
<Storyboard x:Key="loop"
Reverse="True"
Loop="True"
Storyboard.TargetName="boxView"
Duration="500">
<DoubleAnimation To="2" Storyboard.TargetProperty="Frame.Scale" />
<DoubleAnimation To="180" Storyboard.TargetProperty="Frame.Rotation" />
<DoubleAnimation To="0.5" Storyboard.TargetProperty="Frame.Opacity" />
</Storyboard>
</Page.Resources>
<Button Text="Loop Animation">
<Button.Triggers>
<AnimateTrigger Event="Clicked" Storyboard="{StaticResource loop}"/>
</Button.Triggers>
</Button>
Our new AnimateTrigger based on EventTrigger that will allow us to plug storyboards to Events for example.
[ContentProperty(nameof(Storyboard))]
public class AnimateTrigger : EventTrigger
{
internal override void OnAttachedTo(BindableObject bindable)
{
bindable.BindingContextChanged += BindableBindingContextChanged;
base.OnAttachedTo(bindable);
var existingTarget = Storyboard.GetTargetName(Storyboard);
if (existingTarget == null)
{
Storyboard.SetTarget(Storyboard, bindable as VisualElement);
}
if (!string.IsNullOrEmpty(Event))
{
Actions.Add(new StoryboardTriggerAction(Storyboard));
}
}
internal override void OnDetachingFrom(BindableObject bindable)
{
bindable.BindingContextChanged -= BindableBindingContextChanged;
base.OnDetachingFrom(bindable);
}
void BindableBindingContextChanged(object sender, EventArgs e)
{
SetInheritedBindingContext(Storyboard, (sender as BindableObject).BindingContext);
}
public Storyboard Storyboard { get; set; }
Adding a StoryboardTriggerAction
public class StoryboardTriggerAction : TriggerAction<VisualElement>
{
Storyboard _storyboard;
public StoryboardTriggerAction(Storyboard storyboard)
{
_storyboard = storyboard;
}
protected override void Invoke(VisualElement sender)
{
_storyboard?.Begin();
}
}
Adding a Animate markup extension that will figure what the correct animation and will be easier for the new developers:
public class AnimateExtension : BindableObject, IMarkupExtension<Timeline>
{
public BindableProperty Property { set; get; }
public string Binding { set; get; }
public Timeline ProvideValue(IServiceProvider serviceProvider)
{
var anim = new PositionAnimation();
BindingBase GetBinding()
{
return new Binding(Binding);
}
anim.SetBinding(PositionAnimation.ToProperty, GetBinding());
Storyboard.SetTargetProperty(anim, Property);
return anim;
}
object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
{
return (this as IMarkupExtension<Timeline>).ProvideValue(serviceProvider);
}
}
Suggested changes
public interface IAnimation
{
void Begin();
void End();
}
public interface IAnimation<T> : IAnimation
{
T From { get; set; }
T To { get; set; }
}
public abstract class Animation<T> : Timeline
{
public abstract T From { get; set; }
public abstract T To { get; set; }
}
public class PositionAnimation : Animation<Point>
{
}
[ContentProperty(nameof(Children))]
public class Storyboard : Timeline, IList<Timeline>
{
public static void SetTargetName(BindableObject bindable, string value)
public static VisualElement GetTargetName(BindableObject bindable)
}
public abstract class Timeline : BindableObject, IAnimation
{
public uint BeginTime { get; set; }
public uint Duration { get; set; }
public bool Reverse { get; set; }
public bool Loop { get; set; }
public Easing Easing { get; set; }
public EventHandler Completed { get; set; }
public void Begin();
}
Is an EventHandler that fires before the animation is executed possible? Something like BeforeStarted? This looks very powerful!
Most helpful comment
This is possibly a separate issue, but worth thinking about if animations are to be improved:
Is there any plan to be able to animate
WidthRequestandHeightRequestproperties to the elements "auto" width and height (i.e. what you'd get with -1)?One thing I've wanted to do with the existing animation system is animate something becoming visible by height, from 0 to whatever its height would normally be. Getting the size of Forms elements before they've been rendered doesn't seem possible.
There is a
Measuremethod, but it seems to always return 0,0 for elements that haven't been rendered.