Example #1
0
        /*
         * The important thing to know going into reading this method is that it's not possible to
         * modify the backstack. You can only push and pop to and from the top of the stack.
         * So if a user uses an API like `RemovePage` or `InsertPage` we will typically ignore processing those here
         * unless it requires changes to the NavBar (i.e removing the first page with only 2 pages on the stack).
         * Once the user performs an operation that changes the currently visible page then we process any stack changes
         * that have occurred.
         * Let's say the user has pages A,B,C,D on the stack
         * If they remove Page B and Page C then we don't do anything. Then if the user pushes E onto the stack
         * we just transform A,B,C,D into A,D,E.
         * Platform wise that's a "pop" but we use the correct animation for a "push" so visually it looks like a push.
         * This is also the reason why we aren't using the custom animation features on the navigation component itself.
         * Because we might be popping but visually pushing.
         *
         * The Fragments that are on the stack also do not have a hard connection to the page they originally rendereded.
         * Whenever a fragment is the "visible" fragment it just figures out what the current page is and displays that.
         * Fragments are recreated everytime they are pushed on the stack but the handler renderer is not.
         * It's just attached to a new fragment
         * */
        void ApplyNavigationRequest(NavigationRequest args)
        {
            if (IsNavigating)
            {
                // This should really never fire for the developer. Our xplat code should be handling waiting for navigation to
                // complete before requesting another navigation from Core
                // Maybe some day we'll put a navigation queue into Core? For now we won't
                throw new InvalidOperationException("Previous Navigation Request is still Processing");
            }

            if (args.NavigationStack.Count == 0)
            {
                throw new InvalidOperationException("NavigationStack cannot be empty");
            }

            ActiveRequestedArgs = args;
            IReadOnlyList <IView> newPageStack = args.NavigationStack;
            bool animated                     = args.Animated;
            var  navController                = NavHost.NavController;
            var  previousNavigationStack      = NavigationStack;
            var  previousNavigationStackCount = previousNavigationStack.Count;
            bool initialNavigation            = NavigationStack.Count == 0;

            // This updates the graphs public navigation stack property so it's outwardly correct
            // But we've saved off the previous stack so we can correctly interpret navigation
            UpdateNavigationStack(newPageStack);

            // This indicates that this is the first navigation request so we need to initialize the graph
            if (initialNavigation)
            {
                IsInitialNavigation = true;
                Initialize(args.NavigationStack);
                return;
            }

            // If the new stack isn't changing the visible page or the app bar then we just ignore
            // the changes because there's no point to applying these to the platform back stack
            // We only apply changes when the currently visible page changes and/or the appbar
            // will change (gain a back button)
            if (newPageStack[newPageStack.Count - 1] == previousNavigationStack[previousNavigationStackCount - 1])
            {
                NavigationFinished(NavigationView);

                // There's only one page on the stack then we trigger back button visibility logic
                // so that it can add a back button if it needs to
                if (previousNavigationStackCount == 1 || newPageStack.Count == 1)
                {
                    TriggerBackButtonVisibleUpdate();
                }

                return;
            }

            // The incoming fragment uses these variables to pick the correct animation for the current
            // incoming navigation request
            if (newPageStack[newPageStack.Count - 1] == previousNavigationStack[previousNavigationStackCount - 1])
            {
                IsPopping = null;
            }
            else
            {
                IsPopping = newPageStack.Count < previousNavigationStackCount;
            }

            IsAnimated = animated;

            var iterator = NavHost.NavController.BackQueue.Iterator();
            var fragmentNavDestinations = new List <FragmentNavigator.Destination>();

            while (iterator.HasNext)
            {
                if (iterator.Next() is NavBackStackEntry nbse &&
                    nbse.Destination is FragmentNavigator.Destination nvd)
                {
                    fragmentNavDestinations.Add(nvd);
                }
            }

            // Current BackStack has less entries then incoming new page stack
            // This will add Back Stack Entries until the back stack and the new stack
            // match up
            if (fragmentNavDestinations.Count < newPageStack.Count)
            {
                for (int i = fragmentNavDestinations.Count; i < newPageStack.Count; i++)
                {
                    var dest = AddFragmentDestination();
                    navController.Navigate(dest.Id);
                }
            }
            // User just wants to replace the currently visible page but the number
            // of items in the stack are still the same.
            // In theory we could just prompt the currently active fragment to swap out the new PageView
            // But this way we get an animation
            else if (newPageStack.Count == fragmentNavDestinations.Count)
            {
                int lastFragId = fragmentNavDestinations[newPageStack.Count - 1].Id;
                navController.PopBackStack();
                navController.Navigate(lastFragId);
            }
            // Our back stack has more entries on it then
            else
            {
                int popToId = fragmentNavDestinations[newPageStack.Count - 1].Id;
                navController.PopBackStack(popToId, false);
            }

            // We only keep destinations around that are on the backstack
            // This iterates over the new backstack and removes any destinations
            // that are no longer apart of the back stack
            var iterateNewStack = NavHost.NavController.BackQueue.Iterator();
            int startId         = -1;

            while (iterateNewStack.HasNext)
            {
                if (iterateNewStack.Next() is NavBackStackEntry nbse &&
                    nbse.Destination is FragmentNavigator.Destination nvd)
                {
                    fragmentNavDestinations.Remove(nvd);

                    if (startId == -1)
                    {
                        startId = nvd.Id;
                    }
                }
            }

            foreach (var activeDestinations in fragmentNavDestinations)
            {
                NavGraph.Remove(activeDestinations);
            }

            // If we end up removing the destination that was initially the StartDestination
            // The Navigation Graph can get really confused
            if (NavGraph.StartDestination != startId)
            {
                NavGraph.StartDestination = startId;
            }

            // The NavigationIcon on the toolbar gets set inside the Navigate call so this is the earliest
            // point in time that we can setup toolbar colors for the incoming page
            TriggerBackButtonVisibleUpdate();
        }