Forum Xamarin.Forms

Announcement:

The Xamarin Forums have officially moved to the new Microsoft Q&A experience. Microsoft Q&A is the home for technical questions and answers at across all products at Microsoft now including Xamarin!

To create new threads and ask questions head over to Microsoft Q&A for .NET and get involved today.

How to display a Lottie animation at least one time when IsBusy is true during data loading?

PacodosoPacodoso FRUniversity ✭✭✭

On my Xamarin.Forms project, I would like to display a Lottie animation during API calls or during the loading of a website in a WebView.

For this, I've bounded the IsVisible property of the Lottie animation to the IsBusy property of my ViewModels: this works well.

<lottie:AnimationView Animation="resource://lottie_loading.json?assembly=MyApp"
        AnimationSource="EmbeddedResource"
        BackgroundColor="Transparent"
        AutoPlay="True"
        RepeatMode="Infinite"
        IsVisible="{Binding IsBusy}">

But the loading duration is sometimes very short, so I would like to found a way to display the Lottie animation once in full before to hidden it.

=> Is it possible? What would be the better approach to achieve this?

Best Answer

  • PacodosoPacodoso FRUniversity ✭✭✭
    Accepted Answer

    I've found another approach that works, even if this solution is a bit heavy and can be improved.

    Firstly, as recommended there, I've created 2 Triggers:

    public class PlayLottieAnimationTriggerAction : TriggerAction<AnimationView>
    {
        protected override void Invoke(AnimationView sender)
        {
            Debug.WriteLine($"PlayLottieAnimationTriggerAction()");
            sender.PlayAnimation();
        }
    }
    
    public class StopLottieAnimationTriggerAction : TriggerAction<AnimationView>
    {
        protected override void Invoke(AnimationView sender)
        {
            Debug.WriteLine($"StopLottieAnimationTriggerAction()");
            sender.StopAnimation();
        }
    }
    

    I also used EventToCommandBehaviors, like described there.

    After this I can use the Lottie animation like this:

    <forms:AnimationView 
        x:Name="animationView" 
        BackgroundColor="Transparent"
        AutoPlay="True"
        IsVisible="{Binding ShowAnimation}"
        Animation="resource://lottie_4squares_apricot_blond.json?assembly=Example.Forms"
        AnimationSource="EmbeddedResource"
        VerticalOptions="FillAndExpand" 
        HorizontalOptions="FillAndExpand">
        <forms:AnimationView.Triggers>
            <MultiTrigger TargetType="forms:AnimationView">
                <MultiTrigger.Conditions>
                    <BindingCondition Binding="{Binding ShowAnimation}" Value="True" />
                </MultiTrigger.Conditions>
                <MultiTrigger.EnterActions>
                    <triggers:LottieTriggerAction />
                </MultiTrigger.EnterActions>
                <MultiTrigger.ExitActions>
                    <actions:StopLottieAnimationTriggerAction />
                </MultiTrigger.ExitActions>
            </MultiTrigger>
        </forms:AnimationView.Triggers>
        <forms:AnimationView.Behaviors>
            <behaviors:EventToCommandBehavior
                EventName="OnFinishedAnimation"
                Command="{Binding OnFinishedAnimationCommand}"
                CommandParameter="{x:Reference animationView}"/>
        </forms:AnimationView.Behaviors>
    </forms:AnimationView>
    

    And in my ViewModel, I've declared a property ShowAnimation that is related to IsBusy and the Command OnFinishedAnimationCommand like this:

    private bool _showAnimation;
    public bool ShowAnimation
    {
        get => _showAnimation;
        set => Set(ref _showAnimation, value);
    }
    
    public ICommand OnFinishedAnimationCommand
    {
        get
        {
            return new Xamarin.Forms.Command<object>(async (object sender) =>
            {
                if (sender != null)
                {
                    await OnFinishedAnimation(sender);
                }
            });
        }
    }
    
    private Task OnFinishedAnimation(object sender)
    {
        var view = sender as AnimationView;
        if (IsBusy)
        {
            view.PlayAnimation();
        }
        else
        {
            ShowAnimation = false;
        }
        return Task.CompletedTask;
    }
    

    In case of the Loader is related to a WebView, the ShowLoadingView property is set like this:

    private Task WebViewNavigatingAsync(WebNavigatingEventArgs eventArgs)
    {
        IsBusy = true;
        ShowLoadingView = true;
        return Task.CompletedTask;
    }
    
    private async Task WebViewNavigatedAsync(WebNavigatedEventArgs eventArgs)
    {
        IsBusy = false;
    }
    

    But, as I also display an ErrorView in case of issues (timeout, unreachable server, ...) and a Reload/Retry button, I had to add some code:

    private async Task WebViewNavigatedAsync(WebNavigatedEventArgs eventArgs)
    {
        IsBusy = false;
        // for display loading animation on Refresh
        while (ShowLoadingView)
            await Task.Delay(50);
        SetServiceError();
    }
    

    In case of the Loader is related to Data loading, the ShowLoadingView property is set like this:

    private async Task GetNewsAsync(bool forceRefresh = false)
    {
        try
        {
            ShowErrorView = false;
            ErrorKind = ServiceErrorKind.None;
            IsBusy = true;
            ShowLoadingView = true;
            var _news = await _dataService.GetNews(forceRefresh);
            News = new ObservableCollection<News>(_news);
        }
        catch (Exception ex)
        {
            ErrorKind = ServiceErrorKind.ServiceIssue;
        }
        finally
        {
            IsBusy = false;
            await SetServiceError();
        }
    

    }

    However, I noticed that in some cases the SetServiceError() was not fired, as OnFinishedAnimation() was called in the same time. I haven't yet investigated, but I've fixed this by adding the call to SetServiceError() in in OnFinishedAnimation():

    private async Task OnFinishedAnimation(object sender)
    {
        var view = sender as AnimationView;
        if (IsBusy)
        {
            view.PlayAnimation();
        }
        else
        {
            ShowLoadingView = false;
            // fix SetServiceError() call issue
            await SetServiceError();
        }
    }
    

    Don't hesitate to tell what could be done to optimize this.

Answers

  • LandLuLandLu Member, Xamarin Team Xamurai

    Define a field like:

    DateTime loadTime;
    

    Assign the value before loading:

    loadTime = DateTime.Now;
    

    Get the interval when loading finished:

    DateTime dateTime = DateTime.Now;
    var interval = (dateTime - loadTime).TotalSeconds;
    if (interval < 2)
    {
        await Task.Delay(1000);
    }
    

    If the interval is less than 2 seconds, add 1 more sec to dismiss the animation.

  • PacodosoPacodoso FRUniversity ✭✭✭
    Accepted Answer

    I've found another approach that works, even if this solution is a bit heavy and can be improved.

    Firstly, as recommended there, I've created 2 Triggers:

    public class PlayLottieAnimationTriggerAction : TriggerAction<AnimationView>
    {
        protected override void Invoke(AnimationView sender)
        {
            Debug.WriteLine($"PlayLottieAnimationTriggerAction()");
            sender.PlayAnimation();
        }
    }
    
    public class StopLottieAnimationTriggerAction : TriggerAction<AnimationView>
    {
        protected override void Invoke(AnimationView sender)
        {
            Debug.WriteLine($"StopLottieAnimationTriggerAction()");
            sender.StopAnimation();
        }
    }
    

    I also used EventToCommandBehaviors, like described there.

    After this I can use the Lottie animation like this:

    <forms:AnimationView 
        x:Name="animationView" 
        BackgroundColor="Transparent"
        AutoPlay="True"
        IsVisible="{Binding ShowAnimation}"
        Animation="resource://lottie_4squares_apricot_blond.json?assembly=Example.Forms"
        AnimationSource="EmbeddedResource"
        VerticalOptions="FillAndExpand" 
        HorizontalOptions="FillAndExpand">
        <forms:AnimationView.Triggers>
            <MultiTrigger TargetType="forms:AnimationView">
                <MultiTrigger.Conditions>
                    <BindingCondition Binding="{Binding ShowAnimation}" Value="True" />
                </MultiTrigger.Conditions>
                <MultiTrigger.EnterActions>
                    <triggers:LottieTriggerAction />
                </MultiTrigger.EnterActions>
                <MultiTrigger.ExitActions>
                    <actions:StopLottieAnimationTriggerAction />
                </MultiTrigger.ExitActions>
            </MultiTrigger>
        </forms:AnimationView.Triggers>
        <forms:AnimationView.Behaviors>
            <behaviors:EventToCommandBehavior
                EventName="OnFinishedAnimation"
                Command="{Binding OnFinishedAnimationCommand}"
                CommandParameter="{x:Reference animationView}"/>
        </forms:AnimationView.Behaviors>
    </forms:AnimationView>
    

    And in my ViewModel, I've declared a property ShowAnimation that is related to IsBusy and the Command OnFinishedAnimationCommand like this:

    private bool _showAnimation;
    public bool ShowAnimation
    {
        get => _showAnimation;
        set => Set(ref _showAnimation, value);
    }
    
    public ICommand OnFinishedAnimationCommand
    {
        get
        {
            return new Xamarin.Forms.Command<object>(async (object sender) =>
            {
                if (sender != null)
                {
                    await OnFinishedAnimation(sender);
                }
            });
        }
    }
    
    private Task OnFinishedAnimation(object sender)
    {
        var view = sender as AnimationView;
        if (IsBusy)
        {
            view.PlayAnimation();
        }
        else
        {
            ShowAnimation = false;
        }
        return Task.CompletedTask;
    }
    

    In case of the Loader is related to a WebView, the ShowLoadingView property is set like this:

    private Task WebViewNavigatingAsync(WebNavigatingEventArgs eventArgs)
    {
        IsBusy = true;
        ShowLoadingView = true;
        return Task.CompletedTask;
    }
    
    private async Task WebViewNavigatedAsync(WebNavigatedEventArgs eventArgs)
    {
        IsBusy = false;
    }
    

    But, as I also display an ErrorView in case of issues (timeout, unreachable server, ...) and a Reload/Retry button, I had to add some code:

    private async Task WebViewNavigatedAsync(WebNavigatedEventArgs eventArgs)
    {
        IsBusy = false;
        // for display loading animation on Refresh
        while (ShowLoadingView)
            await Task.Delay(50);
        SetServiceError();
    }
    

    In case of the Loader is related to Data loading, the ShowLoadingView property is set like this:

    private async Task GetNewsAsync(bool forceRefresh = false)
    {
        try
        {
            ShowErrorView = false;
            ErrorKind = ServiceErrorKind.None;
            IsBusy = true;
            ShowLoadingView = true;
            var _news = await _dataService.GetNews(forceRefresh);
            News = new ObservableCollection<News>(_news);
        }
        catch (Exception ex)
        {
            ErrorKind = ServiceErrorKind.ServiceIssue;
        }
        finally
        {
            IsBusy = false;
            await SetServiceError();
        }
    

    }

    However, I noticed that in some cases the SetServiceError() was not fired, as OnFinishedAnimation() was called in the same time. I haven't yet investigated, but I've fixed this by adding the call to SetServiceError() in in OnFinishedAnimation():

    private async Task OnFinishedAnimation(object sender)
    {
        var view = sender as AnimationView;
        if (IsBusy)
        {
            view.PlayAnimation();
        }
        else
        {
            ShowLoadingView = false;
            // fix SetServiceError() call issue
            await SetServiceError();
        }
    }
    

    Don't hesitate to tell what could be done to optimize this.

Sign In or Register to comment.