Xamarin.forms: Flyout - bad animation on Android [Bug]

Created on 9 Jul 2019  路  6Comments  路  Source: xamarin/Xamarin.Forms

Description

On Android when selecting a flyout item, the animation is very bad. Everything is shown on the gif below.

Steps to Reproduce

flyout_android_chunky

Expected Behavior

The animation should be smooth.

Actual Behavior

The animation is clutchy.

  • Version with issue: 4.0.0
  • Last known good version: never
  • IDE: Visual Studio 16.1.5
  • Platform Target Frameworks:

    • Android: 9.0

shell 3 Android bug

Most helpful comment

I have a workaround - it's not pretty, but it stops the flyout jitter on close. My hope is that it sheds some light to the development team on a fix for this and get others a temporary workaround.

It looks like the problem is that there is too much going on in the main thread while the flyout close transition is trying to run. (Android DrawerLayout CloseDrawer() animation)

NOTE: this workaround addresses the Xamarin Forms logic for creating and setting the page blocking the UI thread, but any other app logic running on the UI thread will cause the same jittery "close drawer" animation. One way to mitigate this would be to listen for FlyoutIsPresented to become true and pause any update logic running from running on the UI thread until FlyoutIsPresented has been false for some period. (i.e. 1000ms)

In your subclass of Shell:

  1. override OnPropertyChanged and store the last time the FlyoutIsPresented property was set to false
  2. override OnNavigating and if the last time the FlyoutIsPresented property was set to false is less than some value (using 1000ms in the code below), then assume the Flyout was just hidden and perform the following:
  3. cancel the navigation
  4. set FlyoutIsPresented to false
  5. raise OnPropertyChanged for the FlyoutIsPresented to trigger the ShellFlyoutRenderer to close the drawer
  6. wait a brief period for the flyout to close
  7. set a flag that we cancelled the navigation so we know to skip this check on the next call to OnNavigating
  8. re-run the original navigation in the arguments passed to OnNavigating the first time
    public class AppShell : Shell
    {
        ...

        private DateTime LastFlyoutHiddenUtcDateTime { get; set; }

        protected override void OnPropertyChanged(string propertyName = null)
        {
            base.OnPropertyChanged(propertyName);

            if (propertyName == nameof(FlyoutIsPresented))
            {
                if (!FlyoutIsPresented)
                {
                    LastFlyoutHiddenUtcDateTime = DateTime.UtcNow;
                }
            }
        }

        private bool WasNavigationCancelledToCloseFlyoutAndReRunAfterADelayToAvoidJitteryFlyoutCloseTransitionBug = false;

        protected override async void OnNavigating(ShellNavigatingEventArgs args)
        {
            if (!WasNavigationCancelledToCloseFlyoutAndReRunAfterADelayToAvoidJitteryFlyoutCloseTransitionBug)
            {
                // if the above value is true, then this is the re-run navigation from the GoToAsync(args.Target) call below - skip this block this second pass through, as the flyout is now closed
                if ((DateTime.UtcNow - LastFlyoutHiddenUtcDateTime).TotalMilliseconds < 1000)
                {
                    args.Cancel();

                    FlyoutIsPresented = false;

                    OnPropertyChanged(nameof(FlyoutIsPresented));

                    await Task.Delay(300);

                    WasNavigationCancelledToCloseFlyoutAndReRunAfterADelayToAvoidJitteryFlyoutCloseTransitionBug = true;

                    // re-run the originally requested navigation
                    await GoToAsync(args.Target);

                    return;
                }
            }

            WasNavigationCancelledToCloseFlyoutAndReRunAfterADelayToAvoidJitteryFlyoutCloseTransitionBug = false;

            base.OnNavigating(args);
        }

        ...
    }

All 6 comments

My current working theory on this is that right now the code aggressively disposes all the content of any ShellItem you鈥檙e navigating away from and I鈥檓 pretty sure the overlap of that dispose code, create code, and drawer code causes the stutter. Basically when you click on a new item in the flyout the entire current item gets destroyed at the platform level and the item you clicked on gets created. This is my current working theory of the stutter.

We鈥檙e going to need to figure out the best way to balance startup performance and runtime performance. It鈥檚 much like the setOffscreenPageLimit concept on Android.

IMO this should be a high priority issue. Almost every app needs flyout and with this issue flyout on Android is completely unacceptable.

It's really very bad on Android, even to show simple pages. But it only happens the first time you open a page, further navigation after the page is already opened is a lot smoother.

@wagenheimer It's sad that Xamarin team does not prioritize this issue.

Is there something like "isFlyoutAnimating"? I think adding an Await to wait for the flyout to closes should help.

I have a workaround - it's not pretty, but it stops the flyout jitter on close. My hope is that it sheds some light to the development team on a fix for this and get others a temporary workaround.

It looks like the problem is that there is too much going on in the main thread while the flyout close transition is trying to run. (Android DrawerLayout CloseDrawer() animation)

NOTE: this workaround addresses the Xamarin Forms logic for creating and setting the page blocking the UI thread, but any other app logic running on the UI thread will cause the same jittery "close drawer" animation. One way to mitigate this would be to listen for FlyoutIsPresented to become true and pause any update logic running from running on the UI thread until FlyoutIsPresented has been false for some period. (i.e. 1000ms)

In your subclass of Shell:

  1. override OnPropertyChanged and store the last time the FlyoutIsPresented property was set to false
  2. override OnNavigating and if the last time the FlyoutIsPresented property was set to false is less than some value (using 1000ms in the code below), then assume the Flyout was just hidden and perform the following:
  3. cancel the navigation
  4. set FlyoutIsPresented to false
  5. raise OnPropertyChanged for the FlyoutIsPresented to trigger the ShellFlyoutRenderer to close the drawer
  6. wait a brief period for the flyout to close
  7. set a flag that we cancelled the navigation so we know to skip this check on the next call to OnNavigating
  8. re-run the original navigation in the arguments passed to OnNavigating the first time
    public class AppShell : Shell
    {
        ...

        private DateTime LastFlyoutHiddenUtcDateTime { get; set; }

        protected override void OnPropertyChanged(string propertyName = null)
        {
            base.OnPropertyChanged(propertyName);

            if (propertyName == nameof(FlyoutIsPresented))
            {
                if (!FlyoutIsPresented)
                {
                    LastFlyoutHiddenUtcDateTime = DateTime.UtcNow;
                }
            }
        }

        private bool WasNavigationCancelledToCloseFlyoutAndReRunAfterADelayToAvoidJitteryFlyoutCloseTransitionBug = false;

        protected override async void OnNavigating(ShellNavigatingEventArgs args)
        {
            if (!WasNavigationCancelledToCloseFlyoutAndReRunAfterADelayToAvoidJitteryFlyoutCloseTransitionBug)
            {
                // if the above value is true, then this is the re-run navigation from the GoToAsync(args.Target) call below - skip this block this second pass through, as the flyout is now closed
                if ((DateTime.UtcNow - LastFlyoutHiddenUtcDateTime).TotalMilliseconds < 1000)
                {
                    args.Cancel();

                    FlyoutIsPresented = false;

                    OnPropertyChanged(nameof(FlyoutIsPresented));

                    await Task.Delay(300);

                    WasNavigationCancelledToCloseFlyoutAndReRunAfterADelayToAvoidJitteryFlyoutCloseTransitionBug = true;

                    // re-run the originally requested navigation
                    await GoToAsync(args.Target);

                    return;
                }
            }

            WasNavigationCancelledToCloseFlyoutAndReRunAfterADelayToAvoidJitteryFlyoutCloseTransitionBug = false;

            base.OnNavigating(args);
        }

        ...
    }

Was this page helpful?
0 / 5 - 0 ratings