AvaloniaUI / Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
https://avaloniaui.net
MIT License
25.87k stars 2.24k forks source link

Animating RenderTransform crashes with No animator registered for the property RenderTransform #8640

Open hhyyrylainen opened 2 years ago

hhyyrylainen commented 2 years ago

Describe the bug I'm trying to animate a control sliding in from offscreen but my app crashes on startup.

To Reproduce Steps to reproduce the behavior:

  1. Create the following style in App.axaml:
        <Style Selector="Border.Popup[IsVisible=true]">
            <Style.Animations>
                <Animation Duration="0:0:0.1" FillMode="None">
                    <KeyFrame Cue="0%">
                        <Setter Property="RenderTransform" Value="translateX(-500px)"/>
                    </KeyFrame>
                    <KeyFrame Cue="100%">
                        <Setter Property="RenderTransform" Value="translateX(0px)"/>
                    </KeyFrame>
                </Animation>
            </Style.Animations>
        </Style>
  2. Add something like this to a window:
            <Border HorizontalAlignment="Center" VerticalAlignment="Center" Background="{DynamicResource SubMenuBackground}"
                    BorderBrush="Black" BorderThickness="1" CornerRadius="5" Classes="Popup"
                    IsVisible="{Binding $parent.IsVisible}">
  3. Start app
  4. Get an exception on startup:
    System.InvalidOperationException: No animator registered for the property RenderTransform. Add an animator to the Animation.Animators collection that matches this property to animate it.
    at Avalonia.Animation.Animation.InterpretKeyframes(Animatable control) in /_/src/Avalonia.Animation/Animation.cs:line 283
    at Avalonia.Animation.Animation.Apply(Animatable control, IClock clock, IObservable`1 match, Action onComplete) in /_/src/Avalonia.Animation/Animation.cs:line 326
    at Avalonia.Styling.StyleInstance..ctor(IStyle source, IStyleable target, IReadOnlyList`1 setters, IReadOnlyList`1 animations, IStyleActivator activator) in /_/src/Avalonia.Styling/Styling/StyleInstance.cs:line 54
    at Avalonia.Styling.Style.TryAttach(IStyleable target, IStyleHost host) in /_/src/Avalonia.Styling/Styling/Style.cs:line 106
    at Avalonia.Styling.Styles.TryAttach(IStyleable target, IStyleHost host) in /_/src/Avalonia.Styling/Styling/Styles.cs:line 138
    at Avalonia.Styling.Styler.ApplyStyles(IStyleable target, IStyleHost host) in /_/src/Avalonia.Styling/Styling/Styler.cs:line 30
    at Avalonia.Styling.Styler.ApplyStyles(IStyleable target, IStyleHost host) in /_/src/Avalonia.Styling/Styling/Styler.cs:line 25
    at Avalonia.Styling.Styler.ApplyStyles(IStyleable target, IStyleHost host) in /_/src/Avalonia.Styling/Styling/Styler.cs:line 25
    at Avalonia.Styling.Styler.ApplyStyles(IStyleable target, IStyleHost host) in /_/src/Avalonia.Styling/Styling/Styler.cs:line 25
    at Avalonia.Styling.Styler.ApplyStyles(IStyleable target, IStyleHost host) in /_/src/Avalonia.Styling/Styling/Styler.cs:line 25
    at Avalonia.Styling.Styler.ApplyStyles(IStyleable target) in /_/src/Avalonia.Styling/Styling/Styler.cs:line 15
    at Avalonia.StyledElement.ApplyStyling() in /_/src/Avalonia.Styling/StyledElement.cs:line 340
    at Avalonia.StyledElement.EndInit() in /_/src/Avalonia.Styling/StyledElement.cs:line 321
    at ThriveLauncher.Views.MainWindow.!XamlIlPopulate(IServiceProvider , MainWindow ) in /home/hhyyrylainen/Projects/Thrive-Launcher/ThriveLauncher/Views/MainWindow.axaml:line 54
    at ThriveLauncher.Views.MainWindow.!XamlIlPopulateTrampoline(MainWindow )
    at ThriveLauncher.Views.MainWindow.InitializeComponent(Boolean loadXaml, Boolean attachDevTools) in /home/hhyyrylainen/Projects/Thrive-Launcher/ThriveLauncher/Avalonia.NameGenerator/Avalonia.NameGenerator.AvaloniaNameSourceGenerator/MainWindow.g.cs:line 23
    at ThriveLauncher.Views.MainWindow..ctor() in /home/hhyyrylainen/Projects/Thrive-Launcher/ThriveLauncher/Views/MainWindow.axaml.cs:line 11
    at ThriveLauncher.App.OnFrameworkInitializationCompleted() in /home/hhyyrylainen/Projects/Thrive-Launcher/ThriveLauncher/App.axaml.cs:line 28
    at Avalonia.Controls.AppBuilderBase`1.Setup() in /_/src/Avalonia.Controls/AppBuilderBase.cs:line 312
    at Avalonia.Controls.AppBuilderBase`1.SetupWithLifetime(IApplicationLifetime lifetime) in /_/src/Avalonia.Controls/AppBuilderBase.cs:line 179
    at Avalonia.ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime[T](T builder, String[] args, ShutdownMode shutdownMode) in /_/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs:line 208
    at ThriveLauncher.Program.Main(String[] args) in /home/hhyyrylainen/Projects/Thrive-Launcher/ThriveLauncher/Program.cs:line 15

So the animation doesn't even need to start, just existing seems to be enough to crash.

I found that I can get around the startup exception by running this code before the app loads:

Animation.RegisterAnimator<TransformAnimator>(prop => typeof(ITransform).IsAssignableFrom(prop.PropertyType));

But that results in an error later, and an error print:

[Animations] Cannot apply animation: Target property owner Avalonia.Visual is not a Transform object. (Border #3663598)

exception with that extra TransformAnimator:

System.ArgumentException: Disposables collection can not contain null values. (Parameter 'disposables')
   at System.Reactive.Disposables.CompositeDisposable.ToList(IEnumerable`1 disposables) in /_/Rx.NET/Source/src/System.Reactive/Disposables/CompositeDisposable.cs:line 107
   at System.Reactive.Disposables.CompositeDisposable..ctor(IEnumerable`1 disposables) in /_/Rx.NET/Source/src/System.Reactive/Disposables/CompositeDisposable.cs:line 83
   at Avalonia.Animation.Animation.Apply(Animatable control, IClock clock, IObservable`1 match, Action onComplete) in /_/src/Avalonia.Animation/Animation.cs:line 353
   at Avalonia.Styling.StyleInstance..ctor(IStyle source, IStyleable target, IReadOnlyList`1 setters, IReadOnlyList`1 animations, IStyleActivator activator) in /_/src/Avalonia.Styling/Styling/StyleInstance.cs:line 54
   at Avalonia.Styling.Style.TryAttach(IStyleable target, IStyleHost host) in /_/src/Avalonia.Styling/Styling/Style.cs:line 106
   at Avalonia.Styling.Styles.TryAttach(IStyleable target, IStyleHost host) in /_/src/Avalonia.Styling/Styling/Styles.cs:line 138
   at Avalonia.Styling.Styler.ApplyStyles(IStyleable target, IStyleHost host) in /_/src/Avalonia.Styling/Styling/Styler.cs:line 30
   at Avalonia.Styling.Styler.ApplyStyles(IStyleable target, IStyleHost host) in /_/src/Avalonia.Styling/Styling/Styler.cs:line 25
   at Avalonia.Styling.Styler.ApplyStyles(IStyleable target, IStyleHost host) in /_/src/Avalonia.Styling/Styling/Styler.cs:line 25
   at Avalonia.Styling.Styler.ApplyStyles(IStyleable target, IStyleHost host) in /_/src/Avalonia.Styling/Styling/Styler.cs:line 25
   at Avalonia.Styling.Styler.ApplyStyles(IStyleable target, IStyleHost host) in /_/src/Avalonia.Styling/Styling/Styler.cs:line 25
   at Avalonia.Styling.Styler.ApplyStyles(IStyleable target) in /_/src/Avalonia.Styling/Styling/Styler.cs:line 15
   at Avalonia.StyledElement.ApplyStyling() in /_/src/Avalonia.Styling/StyledElement.cs:line 340
   at Avalonia.StyledElement.EndInit() in /_/src/Avalonia.Styling/StyledElement.cs:line 321
   at ThriveLauncher.Views.MainWindow.!XamlIlPopulate(IServiceProvider , MainWindow ) in /home/hhyyrylainen/Projects/Thrive-Launcher/ThriveLauncher/Views/MainWindow.axaml:line 54
   at ThriveLauncher.Views.MainWindow.!XamlIlPopulateTrampoline(MainWindow )
   at ThriveLauncher.Views.MainWindow.InitializeComponent(Boolean loadXaml, Boolean attachDevTools) in /home/hhyyrylainen/Projects/Thrive-Launcher/ThriveLauncher/Avalonia.NameGenerator/Avalonia.NameGenerator.AvaloniaNameSourceGenerator/MainWindow.g.cs:line 23
   at ThriveLauncher.Views.MainWindow..ctor() in /home/hhyyrylainen/Projects/Thrive-Launcher/ThriveLauncher/Views/MainWindow.axaml.cs:line 11
   at ThriveLauncher.App.OnFrameworkInitializationCompleted() in /home/hhyyrylainen/Projects/Thrive-Launcher/ThriveLauncher/App.axaml.cs:line 28
   at Avalonia.Controls.AppBuilderBase`1.Setup() in /_/src/Avalonia.Controls/AppBuilderBase.cs:line 312
   at Avalonia.Controls.AppBuilderBase`1.SetupWithLifetime(IApplicationLifetime lifetime) in /_/src/Avalonia.Controls/AppBuilderBase.cs:line 179
   at Avalonia.ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime[T](T builder, String[] args, ShutdownMode shutdownMode) in /_/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs:line 208
   at ThriveLauncher.Program.Main(String[] args) in /home/hhyyrylainen/Projects/Thrive-Launcher/ThriveLauncher/Program.cs:line 15

Which is maybe why the transform animator is not hooked up by default...

Expected behavior I expect that it would be easy to animate the render transform to make controls move around. For example to make a popup alert appear in a much more pleasing way. I also tried animating RenderTransformOrigin which did nothing with <Setter Property="RenderTransformOrigin" Value="0,-500"/>, that didn't even bother crashing.

I got an opacity value to fade in nicely using the same approach but when I do the same thing with RenderTransform it crashes.

Screenshots If applicable, add screenshots to help explain your problem.

Desktop (please complete the following information):

Additional context I got the impression that this should work based on reading these documentation pages:

pr8x commented 2 years ago

You have to target TranslateTransform.X in the setter IIRC. (see example here: (https://docs.avaloniaui.net/docs/animations/keyframe-animations#keyframes)

hhyyrylainen commented 2 years ago

I'll test that later.

That page doesn't mention TranslateTransform only RotateTransform. I guess if it works then this is a case of incomplete documentation, but on this page https://docs.avaloniaui.net/docs/animations/transitions there's a whole section about render transforms, and no mention of them in that animation documentation so I assumed it would work the same way.

hhyyrylainen commented 2 years ago

I just now got to around to this this and sure enough this actually works correctly:

        <Style Selector="Border.Popup[IsVisible=true]">
            <Style.Animations>
                <Animation Duration="0:0:0.4" FillMode="None">
                    <KeyFrame Cue="0%">
                        <Setter Property="TranslateTransform.Y" Value="-500"/>
                    </KeyFrame>
                    <KeyFrame Cue="100%">
                        <Setter Property="TranslateTransform.Y" Value="0"/>
                    </KeyFrame>
                </Animation>
            </Style.Animations>
        </Style>

Thanks for the hint!

So I suppose this is mostly just a documentation problem? At least until me or someone else wants to animate with the full power of the RenderTransform property.

This might not be the best place to ask but if anyone knows how to animate something when it is being hidden, let me know.

Edit: I found the solution myself. I basically use a separate property to control when the animations start / end and the animation sets the visibility flag:

    <Style Selector="controls|PopupDialog[ShowPopup=true] /template/ Border#Popup">
        <Style.Animations>
            <Animation Duration="0:0:0.3" FillMode="Forward" Easing="CubicEaseIn">
                <KeyFrame Cue="0%">
                    <Setter Property="TranslateTransform.Y" Value="-500" />
                    <Setter Property="IsVisible" Value="True" />
                </KeyFrame>
                <KeyFrame Cue="100%">
                    <Setter Property="TranslateTransform.Y" Value="0" />
                    <Setter Property="IsVisible" Value="True" />
                </KeyFrame>
            </Animation>
        </Style.Animations>
    </Style>
    <Style Selector="controls|PopupDialog[ShowPopup=false] /template/ Border#Popup">
        <Style.Animations>
            <Animation Duration="0:0:0.3" FillMode="Forward" Easing="CubicEaseIn">
                <KeyFrame Cue="0%">
                    <Setter Property="TranslateTransform.Y" Value="0" />
                </KeyFrame>
                <KeyFrame Cue="100%">
                    <Setter Property="TranslateTransform.Y" Value="500" />
                    <Setter Property="IsVisible" Value="False" />
                </KeyFrame>
            </Animation>
        </Style.Animations>
    </Style>

Full code is here: https://github.com/Revolutionary-Games/Thrive-Launcher/blob/922d90c98e7764cb47e48c427e10c0c19949dcab/ThriveLauncher/Controls/PopupDialog.axaml (implemented as a custom control)

hhyyrylainen commented 2 years ago

Animating TranslateTransform.Y seems to work correctly, however it causes the devtools to crash. I opened a separate issue for that: https://github.com/AvaloniaUI/Avalonia/issues/8695