adospace / reactorui-maui

MauiReactor is a MVU UI framework built on top of .NET MAUI
MIT License
586 stars 49 forks source link

AnimationController question #59

Closed adospace closed 1 year ago

adospace commented 1 year ago

@Otuyishime I apologize I couldn't find anywhere else to ask you this question, but Is there a way to smoothly reverse animations with AnimationController? I've playing with it and it seems toggling IsEnabled resets controls to their original position. You can use this small component as an example. Appreciated.

class AnimatedBoxState { public double Translate { get; set; } = 0.0; public bool ToggleAnimation { get; set; } = false; }

class AnimatedBox : Component { public override VisualNode Render() { return new ContentPage("Animated Box") { new Grid() { new AnimationController { new SequenceAnimation { new DoubleAnimation() .StartValue(0) .TargetValue(50) .Duration(TimeSpan.FromMilliseconds(600)) .Easing(Easing.SinInOut) .OnTick(v => SetState(s => s.Translate = v)), } .Loop(false) } .IsEnabled(State.ToggleAnimation) .OnIsEnabledChanged(enabled => SetState(s => s.ToggleAnimation = enabled)), new BoxView() .GridRow(0) .HCenter() .VCenter() .HeightRequest(80) .WidthRequest(80) .CornerRadius(10) .TranslationX(State.Translate), new Button("Animate") .GridRow(1) .HCenter() .VCenter() .WidthRequest(150) .OnClicked(Toggle) } .Rows(new MauiControls.RowDefinitionCollection { new MauiControls.RowDefinition(), new MauiControls.RowDefinition(GridLength.Auto) }) .Padding(10) .BackgroundColor(Colors.LightSkyBlue) }; }

private void Toggle()
{
    SetState(s => s.ToggleAnimation = !s.ToggleAnimation);
}

}

adospace commented 1 year ago

Hi, IsEnabled completely reset the animation while IsPaused just pause it.

So if you want to restart the animation from where it stopped you should use the IsPaused property as shown in the below example:

class AnimatedBoxState
{
    public double Translate { get; set; } = 0.0;
    public bool AnimationPaused { get; set; } = true;
}

class AnimatedBox : Component<AnimatedBoxState>
{
    public override VisualNode Render()
    {
        return new ContentPage("Animated Box")
        {
            new Grid("*, Auto", "*")
            {
                new AnimationController
                {
                    new SequenceAnimation
                    {
                        new DoubleAnimation()
                            .StartValue(0)
                            .TargetValue(50)
                            .Duration(600)
                            .Easing(Easing.SinInOut)
                            .OnTick(v => SetState(s => s.Translate = v)),
                    }
                    .Loop(true)
                }
                .IsEnabled(true)
                .IsPaused(State.AnimationPaused)
                .OnIsPausedChanged(paused => SetState(s => s.AnimationPaused = paused)),
                new BoxView()
                    .GridRow(0)
                    .HCenter()
                    .VCenter()
                    .HeightRequest(80)
                    .WidthRequest(80)
                    .CornerRadius(10)
                    .TranslationX(State.Translate),
                new Button("Animate")
                    .GridRow(1)
                    .HCenter()
                    .VCenter()
                    .WidthRequest(150)
                    .OnClicked(Toggle)
            }
            .Padding(10)
            .BackgroundColor(Colors.LightSkyBlue)
        };
    }

    private void Toggle()
    {
        SetState(s => s.AnimationPaused = !s.AnimationPaused);
    }
}

I'm not sure if this answers your question but if that isn't the case please elaborate a bit more so that I can understand what you mean,

Thanks

Otuyishime commented 1 year ago

@adospace Thank you for your quick response! I'm afraid that's not the desired effect I'm looking for. Using the provided example, by toggling the button, I want the BoxView to animate forward and backward. I want the same animation to apply as the BoxView moves back in it's original place. I'm thinking along the lines of what Flutter does with AnimationController.forward() and AnimationController.reverse().

I'm basically trying to re-implement this:

class AnimatedBoxState
{
    public bool AnimateBox { get; set; } = false;
}

class AnimatedBox : Component<AnimatedBoxState>
{
    public override VisualNode Render()
    {
        return new ContentPage("Main Page")
        {
            new Grid("*, Auto", "*")
            {
                new BoxView()
                    .GridRow(0)
                    .HCenter()
                    .VCenter()
                    .HeightRequest(80)
                    .WidthRequest(80)
                    .CornerRadius(10)
                    .TranslationX(State.AnimateBox ? -100 : 0)
                    .WithAnimation(Easing.CubicOut, 500.0),
                new Button("Animate")
                    .GridRow(1)
                    .HCenter()
                    .VCenter()
                    .WidthRequest(150)
                    .OnClicked(Toggle)
            }
            .Padding(10)
            .BackgroundColor(Colors.LightSkyBlue)
        };
    }

    private void Toggle()
    {
        SetState(s => s.AnimateBox = !s.AnimateBox);
    }
}

using AnimationController

Here's a short clip of what I'm trying to achieve. https://user-images.githubusercontent.com/15788997/231931719-fe25edb9-96c7-459a-a665-4dd036fdc340.mp4

Appreciate your help

adospace commented 1 year ago

Interesting question, if I had to choose I would surely opt for RxAnimation in this case but if you really want to use the AnimationController one possible way is something like this:

class AnimatedBoxState
{
    public double Start { get;set; }
    public double Target { get; set; } = 250;
    public double Translate { get; set; } = 0.0;
    public bool ToggleAnimation { get; set; } = false;
}

class AnimatedBox : Component<AnimatedBoxState>
{
    public override VisualNode Render()
    {
        return new ContentPage("Animated Box")
        {
            new Grid("*, Auto", "*")
            {
                new AnimationController
                {
                    new SequenceAnimation
                    {
                        new DoubleAnimation()
                            .StartValue(State.Start)
                            .TargetValue(State.Target)
                            .Duration(TimeSpan.FromMilliseconds(600))
                            .Easing(Easing.SinInOut)
                            .OnTick(v => SetState(s => s.Translate = v)),
                    }
                    .Loop(false)
                }
                .IsEnabled(State.ToggleAnimation)
                .OnIsEnabledChanged(enabled => SetState(s =>
                {
                    s.ToggleAnimation = enabled;
                    s.Start = s.Translate;
                    s.Target = s.Translate > 0 ? 0 : 250;
                })),
                new BoxView()
                    .GridRow(0)
                    .HCenter()
                    .VCenter()
                    .HeightRequest(80)
                    .WidthRequest(80)
                    .CornerRadius(10)
                    .TranslationX(State.Translate),
                new Button("Animate")
                    .GridRow(1)
                    .HCenter()
                    .VCenter()
                    .WidthRequest(150)
                    .OnClicked(Toggle)
            }
            .Padding(10)
            .BackgroundColor(Colors.LightSkyBlue)
        };
    }

    private void Toggle()
    {
        SetState(s => s.ToggleAnimation = !s.ToggleAnimation);
    }
}

but it's still not optimal as the RxAnimation solution because the user could potentially be able to click the toggle animation button before the end of the animation (for example double-clicking) resulting in a nasty effect: You could enhance the AnitmationController solution to even handle the double click event, for example, avoiding to toggle AnimationBox property 2 times before the end of the animation but as you can see RxAnimation remains a better and cleaner approach.

Said that just wanted to point out that MauiReactor supports RxAnimation and AnimationController that can be used at the same time inside a component but have two different purposes:

1) RxAnimations (inspired by SwiftUI animation system) is a way to animate objects between states, so if you clearly can identify the 2 state values go for it (as you shown in your example). I like a lot this approach because you aren't required to dirty up the state with properties that are there only to support the animation.

2) AnimationController (inspired by Flutter animation system) is a more powerful way to achieve complex animations because you can concatenate multiple animations, create hierarchies, start/stop them etc but this comes with a cost: complexity. So go for AnimationController only if you need it. Do not try to control it because you can't (at least for now) call methods like forward() or backward() (in an "imperative" way) but set/specify properties of the animations optionally getting the values from the state.

Otuyishime commented 1 year ago

@adospace Thanks for your help. This is a great library!

adospace commented 1 year ago

thank you @Otuyishime I'll close this ticket, please open a new one if you encounter other issues