Xamarin.forms: Spec: Storyboard Animations in XAML

Created on 26 Oct 2018  Â·  5Comments  Â·  Source: xamarin/Xamarin.Forms

Drafted by @rmarinho

Rationale

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

Implementation

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);
    }
}
animation 🎬 in-progress high impact proposal-open enhancement ➕

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

All 5 comments

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!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

AppGrate picture AppGrate  Â·  3Comments

mfeingol picture mfeingol  Â·  3Comments

deakjahn picture deakjahn  Â·  3Comments

simontocknell picture simontocknell  Â·  3Comments

rmarinho picture rmarinho  Â·  3Comments