diff --git a/Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj b/Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj
index 44a22f02d2d..7c5f840c44d 100644
--- a/Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj
+++ b/Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj
@@ -396,7 +396,7 @@
-
+
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue11769.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue11769.cs
index 9ad22913f1b..f7ab4386664 100644
--- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue11769.cs
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue11769.cs
@@ -110,8 +110,11 @@ protected override void Init()
{
Device.InvokeOnMainThreadAsync(() =>
{
- var page = AddBottomTab("Success");
- page.Content = new Label() { Text = "Success" };
+ var page = AddBottomTab("Flyout Item");
+ page.Content = new Label()
+ {
+ Text = "Success"
+ };
});
}
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/ShellFlyoutContent.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/ShellFlyoutContent.cs
new file mode 100644
index 00000000000..b746f31d95a
--- /dev/null
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/ShellFlyoutContent.cs
@@ -0,0 +1,214 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Xamarin.Forms.CustomAttributes;
+using Xamarin.Forms.Internals;
+using Xamarin.Forms.PlatformConfiguration;
+using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
+
+
+#if UITEST
+using Xamarin.UITest;
+using NUnit.Framework;
+using Xamarin.Forms.Core.UITests;
+#endif
+
+namespace Xamarin.Forms.Controls.Issues
+{
+ [Preserve(AllMembers = true)]
+ [Issue(IssueTracker.None, 0, "Shell Flyout Content",
+ PlatformAffected.All)]
+#if UITEST
+ [NUnit.Framework.Category(UITestCategories.Shell)]
+ [NUnit.Framework.Category(UITestCategories.UwpIgnore)]
+#endif
+ public class ShellFlyoutContent : TestShell
+ {
+ protected override void Init()
+ {
+ var page = new ContentPage();
+
+ this.BindingContext = this;
+ AddFlyoutItem(page, "Flyout Item Top");
+ for (int i = 0; i < 50; i++)
+ {
+ AddFlyoutItem($"Flyout Item :{i}");
+ Items[i].AutomationId = "Flyout Item";
+ }
+
+ Items.Add(new MenuItem() { Text = "Menu Item" });
+ AddFlyoutItem("Flyout Item Bottom");
+
+ var layout = new StackLayout()
+ {
+ Children =
+ {
+ new Label()
+ {
+ Text = "Open the Flyout and Toggle the Content, Header and Footer. If it changes after each click test has passed",
+ AutomationId = "PageLoaded"
+ }
+ }
+ };
+
+ page.Content = layout;
+
+ layout.Children.Add(new Button()
+ {
+ Text = "Toggle Flyout Content Template",
+ Command = new Command(() =>
+ {
+ if (FlyoutContentTemplate == null)
+ {
+ FlyoutContentTemplate = new DataTemplate(() =>
+ {
+ var collectionView = new CollectionView();
+
+ collectionView.SetBinding(CollectionView.ItemsSourceProperty, "FlyoutItems");
+ collectionView.IsGrouped = true;
+
+ collectionView.ItemTemplate =
+ new DataTemplate(() =>
+ {
+ var label = new Label();
+
+ label.SetBinding(Label.TextProperty, "Title");
+
+ var button = new Button()
+ {
+ Text = "Click to Reset",
+ AutomationId = "ContentView",
+ Command = new Command(() =>
+ {
+ FlyoutContentTemplate = null;
+ })
+ };
+
+ return new StackLayout()
+ {
+ Children =
+ {
+ label,
+ button
+ }
+ };
+ });
+
+ return collectionView;
+ });
+ }
+ else if (FlyoutContentTemplate != null)
+ {
+ FlyoutContentTemplate = null;
+ }
+ }),
+ AutomationId = "ToggleFlyoutContentTemplate"
+ });
+
+ layout.Children.Add(new Button()
+ {
+ Text = "Toggle Flyout Content",
+ Command = new Command(() =>
+ {
+ if (FlyoutContent != null)
+ {
+ FlyoutContent = null;
+ }
+ else
+ {
+ var stackLayout = new StackLayout()
+ {
+ Background = SolidColorBrush.Green
+ };
+
+ FlyoutContent = new ScrollView()
+ {
+ Content = stackLayout
+ };
+
+ AddButton("Top Button");
+
+ for (int i = 0; i < 50; i++)
+ {
+ AddButton("Content View");
+ }
+
+ AddButton("Bottom Button");
+
+ void AddButton(string text)
+ {
+ stackLayout.Children.Add(new Button()
+ {
+ Text = text,
+ AutomationId = "ContentView",
+ Command = new Command(() =>
+ {
+ FlyoutContent = null;
+ }),
+ TextColor = Color.White
+ });
+ }
+ }
+ }),
+ AutomationId = "ToggleContent"
+ });
+
+ layout.Children.Add(new Button()
+ {
+ Text = "Toggle Header/Footer View",
+ Command = new Command(() =>
+ {
+ if (FlyoutHeader != null)
+ {
+ FlyoutHeader = null;
+ FlyoutFooter = null;
+ }
+ else
+ {
+ FlyoutHeader = new StackLayout()
+ {
+ Children = {
+ new Label() { Text = "Header" }
+ },
+ AutomationId = "Header View",
+ Background = SolidColorBrush.Yellow
+ };
+
+ FlyoutFooter = new StackLayout()
+ {
+ Background = SolidColorBrush.Orange,
+ Orientation = StackOrientation.Horizontal,
+ Children = {
+ new Label() { Text = "Footer" }
+ },
+ AutomationId = "Footer View"
+ };
+ }
+ }),
+ AutomationId = "ToggleHeaderFooter"
+ });
+ }
+
+
+#if UITEST
+
+ [Test]
+ public void FlyoutContentTests()
+ {
+ RunningApp.WaitForElement("PageLoaded");
+ TapInFlyout("Flyout Item");
+ RunningApp.Tap("ToggleContent");
+ TapInFlyout("ContentView");
+ TapInFlyout("Flyout Item");
+ RunningApp.Tap("ToggleFlyoutContentTemplate");
+ TapInFlyout("ContentView");
+ TapInFlyout("Flyout Item");
+ }
+#endif
+ }
+}
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/TestPages/TestPages.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/TestPages/TestPages.cs
index cfc6808705e..f8949d6cb97 100644
--- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/TestPages/TestPages.cs
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/TestPages/TestPages.cs
@@ -663,7 +663,7 @@ public ContentPage AddBottomTab(string title, string icon = null)
ContentPage page = new ContentPage();
if (Items.Count == 0)
{
- var item = AddContentPage(page);
+ var item = AddContentPage(page, title);
item.Items[0].Items[0].Title = title ?? page.Title;
item.Items[0].Title = title ?? page.Title;
return page;
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
index f2c2bda57ed..f292e9df8f8 100644
--- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
@@ -12,6 +12,7 @@
+
Issue10897.xaml
diff --git a/Xamarin.Forms.Core.UnitTests/ShellFlyoutItemGroupTests.cs b/Xamarin.Forms.Core.UnitTests/ShellFlyoutItemGroupTests.cs
index d01ade754aa..34bcd7e86f8 100644
--- a/Xamarin.Forms.Core.UnitTests/ShellFlyoutItemGroupTests.cs
+++ b/Xamarin.Forms.Core.UnitTests/ShellFlyoutItemGroupTests.cs
@@ -165,9 +165,7 @@ public void FlyoutItemVisibleWorksForMenuItemsFlyoutItemAsMultipleItems()
flyoutItem.CurrentItem.CurrentItem.MenuItems.Add(CreateNonVisibleMenuItem());
shell.Items.Add(flyoutItem);
-
- IShellController shellController = (IShellController)shell;
- var groups = shellController.GenerateFlyoutGrouping();
+ var groups = shell.Controller.GenerateFlyoutGrouping();
Assert.AreEqual(groups.SelectMany(x => x.OfType()).Count(), 0);
}
@@ -221,6 +219,89 @@ public void ReturnTheSameGroupingInstanceIfStructureHasntChanged()
Assert.AreNotSame(flyoutItems, flyoutItems2);
}
+ [Test]
+ public void FlyoutItemsBasicSyncTest()
+ {
+ var shell = new TestShell();
+ shell.Items.Add(CreateShellItem());
+ shell.Items.Add(CreateShellItem());
+ shell.Items.Add(CreateShellItem());
+ shell.Items.Add(CreateShellItem());
+ shell.Items[3].IsVisible = false;
+
+ var flyoutItems = shell.GenerateTestFlyoutItems();
+ Assert.AreEqual(shell.Items[0], flyoutItems[0][0]);
+ Assert.AreEqual(shell.Items[1], flyoutItems[0][1]);
+ Assert.AreEqual(shell.Items[2], flyoutItems[0][2]);
+ Assert.AreEqual(3, flyoutItems[0].Count);
+ Assert.AreEqual(1, flyoutItems.Count);
+ }
+
+ [Test]
+ public void FlyoutItemsGroupTest()
+ {
+ var shell = new TestShell();
+ shell.Items.Add(CreateShellItem());
+ shell.Items.Add(CreateShellItem());
+ var sec1 = shell.Items[0].Items[0];
+ var sec2 = CreateShellSection();
+ var sec3 = CreateShellSection();
+
+ shell.Items[0].FlyoutDisplayOptions = FlyoutDisplayOptions.AsMultipleItems;
+ shell.Items[0].Items.Add(sec2);
+ shell.Items[0].Items.Add(sec3);
+
+ var flyoutItems = shell.GenerateTestFlyoutItems();
+ Assert.AreEqual(sec1, flyoutItems[0][0]);
+ Assert.AreEqual(sec2, flyoutItems[0][1]);
+ Assert.AreEqual(sec3, flyoutItems[0][2]);
+ Assert.AreEqual(shell.Items[1], flyoutItems[1][0]);
+ }
+
+ [Test]
+ public void FlyoutItemsGroupTestWithRemove()
+ {
+ var shell = new TestShell();
+ shell.Items.Add(CreateShellItem());
+ shell.Items.Add(CreateShellItem());
+ var sec1 = shell.Items[0].Items[0];
+ var sec2 = CreateShellSection();
+ var sec3 = CreateShellSection();
+
+ shell.Items[0].FlyoutDisplayOptions = FlyoutDisplayOptions.AsMultipleItems;
+ shell.Items[0].Items.Add(sec2);
+ shell.Items[0].Items.Add(sec3);
+ shell.Items.RemoveAt(0);
+
+ var flyoutItems = shell.GenerateTestFlyoutItems();
+ Assert.AreEqual(shell.Items[0], flyoutItems[0][0]);
+ Assert.AreEqual(1, flyoutItems.Count);
+ }
+
+ [Test]
+ public void FlyoutItemsGroupTestMoveGroup()
+ {
+ var shell = new TestShell();
+ shell.Items.Add(CreateShellItem());
+ shell.Items.Add(CreateShellItem());
+ var sec1 = shell.Items[0].Items[0];
+ var sec2 = CreateShellSection();
+ var sec3 = CreateShellSection();
+
+ shell.Items[0].FlyoutDisplayOptions = FlyoutDisplayOptions.AsMultipleItems;
+ shell.Items[0].Items.Add(sec2);
+ shell.Items[0].Items.Add(sec3);
+
+ var item1 = shell.Items[0];
+ shell.Items.RemoveAt(0);
+ shell.Items.Add(item1);
+ var flyoutItems = shell.GenerateTestFlyoutItems();
+ Assert.AreEqual(sec1, flyoutItems[1][0]);
+ Assert.AreEqual(sec2, flyoutItems[1][1]);
+ Assert.AreEqual(sec3, flyoutItems[1][2]);
+ Assert.AreEqual(shell.Items[0], flyoutItems[0][0]);
+ }
+
MenuItem CreateNonVisibleMenuItem()
{
MenuItem item = new MenuItem();
diff --git a/Xamarin.Forms.Core.UnitTests/ShellTestBase.cs b/Xamarin.Forms.Core.UnitTests/ShellTestBase.cs
index 403e00d39ac..2d4e00206b5 100644
--- a/Xamarin.Forms.Core.UnitTests/ShellTestBase.cs
+++ b/Xamarin.Forms.Core.UnitTests/ShellTestBase.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
@@ -300,6 +301,18 @@ public class TestShell : Shell
public IShellController Controller => this;
+ public List> GenerateTestFlyoutItems()
+ {
+ List> returnValue = new List>();
+
+
+ FlyoutItems
+ .OfType()
+ .ForEach(l => returnValue.Add(l.OfType().ToList()));
+
+ return returnValue;
+ }
+
public TestShell()
{
this.Navigated += (_, __) => NavigatedCount++;
diff --git a/Xamarin.Forms.Core/Page.cs b/Xamarin.Forms.Core/Page.cs
index 04d33c4c247..186748a950c 100644
--- a/Xamarin.Forms.Core/Page.cs
+++ b/Xamarin.Forms.Core/Page.cs
@@ -165,7 +165,6 @@ internal override IEnumerable ChildrenNotDrawnByThisElement
if (titleViewPart2TheNavBar != null)
yield return titleViewPart2TheNavBar;
-
}
}
diff --git a/Xamarin.Forms.Core/Shell/IShellController.cs b/Xamarin.Forms.Core/Shell/IShellController.cs
index 86344198b05..c7bde024c99 100644
--- a/Xamarin.Forms.Core/Shell/IShellController.cs
+++ b/Xamarin.Forms.Core/Shell/IShellController.cs
@@ -25,6 +25,8 @@ public interface IShellController : IPageController
View FlyoutFooter { get; }
+ View FlyoutContent { get; }
+
ImageSource FlyoutIcon { get; }
void AddAppearanceObserver(IAppearanceObserver observer, Element pivot);
diff --git a/Xamarin.Forms.Core/Shell/Shell.cs b/Xamarin.Forms.Core/Shell/Shell.cs
index a02c991e548..6ae6fb775bd 100644
--- a/Xamarin.Forms.Core/Shell/Shell.cs
+++ b/Xamarin.Forms.Core/Shell/Shell.cs
@@ -1,15 +1,14 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
-using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms.Internals;
-using Xamarin.Forms.StyleSheets;
namespace Xamarin.Forms
{
@@ -296,14 +295,13 @@ DataTemplate IShellController.GetFlyoutItemDataTemplate(BindableObject bo)
event EventHandler IShellController.FlyoutItemsChanged
{
- add { _flyoutItemsChanged += value; }
- remove { _flyoutItemsChanged -= value; }
+ add { _flyoutManager.FlyoutItemsChanged += value; }
+ remove { _flyoutManager.FlyoutItemsChanged -= value; }
}
- event EventHandler _flyoutItemsChanged;
-
View IShellController.FlyoutHeader => FlyoutHeaderView;
View IShellController.FlyoutFooter => FlyoutFooterView;
+ View IShellController.FlyoutContent => FlyoutContentView;
IShellController ShellController => this;
@@ -440,7 +438,7 @@ void OnShellItemPropertyChanged(object sender, PropertyChangedEventArgs e)
bool IShellController.ProposeNavigation(ShellNavigationSource source, ShellItem shellItem, ShellSection shellSection, ShellContent shellContent, IReadOnlyList stack, bool canCancel)
{
- return _navigationManager.ProposeNavigationOutsideGotoAsync(source, shellItem, shellSection, shellContent, stack, canCancel);
+ return _navigationManager.ProposeNavigationOutsideGotoAsync(source, shellItem, shellSection, shellContent, stack, canCancel, true);
}
bool IShellController.RemoveAppearanceObserver(IAppearanceObserver observer)
@@ -546,8 +544,8 @@ public Task GoToAsync(ShellNavigationState state, bool animate)
View _flyoutHeaderView;
View _flyoutFooterView;
- List> _currentFlyoutViews;
ShellNavigationManager _navigationManager;
+ ShellFlyoutItemsManager _flyoutManager;
public Shell()
{
@@ -564,6 +562,7 @@ public Shell()
OnNavigating(args);
};
+ _flyoutManager = new ShellFlyoutItemsManager(this);
Navigation = new NavigationImpl(this);
Route = Routing.GenerateImplicitRoute("shell");
Initialize();
@@ -822,166 +821,18 @@ protected override void OnBindingContextChanged()
if (FlyoutFooterView != null)
SetInheritedBindingContext(FlyoutFooterView, BindingContext);
- }
-
-
- internal void SendFlyoutItemsChanged()
- {
- if (UpdateFlyoutGroupings())
- _flyoutItemsChanged?.Invoke(this, EventArgs.Empty);
- }
-
- List> IShellController.GenerateFlyoutGrouping()
- {
- if (_currentFlyoutViews == null)
- UpdateFlyoutGroupings();
- return _currentFlyoutViews;
+ if (FlyoutContentView != null)
+ SetInheritedBindingContext(FlyoutContentView, BindingContext);
}
- bool UpdateFlyoutGroupings()
- {
- // The idea here is to create grouping such that the Flyout would
- // render correctly if it renderered each item in the groups in order
- // but put a line between the groups. This is needed because our grouping can
- // actually go 3 layers deep.
-
- // Ideally this lets us control where lines are drawn in the core code
- // just by changing how we generate these groupings
-
- var result = new List>();
-
- var currentGroup = new List();
-
- foreach (var shellItem in ShellController.GetItems())
- {
- if (!ShowInFlyoutMenu(shellItem))
- continue;
-
- if (Routing.IsImplicit(shellItem) || shellItem.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
- {
- if (shellItem.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
- IncrementGroup();
-
- foreach (var shellSection in (shellItem as IShellItemController).GetItems())
- {
- if (!ShowInFlyoutMenu(shellSection))
- continue;
-
- var shellContents = ((IShellSectionController)shellSection).GetItems();
- if (Routing.IsImplicit(shellSection) || shellSection.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
- {
- foreach (var shellContent in shellContents)
- {
- if (!ShowInFlyoutMenu(shellContent))
- continue;
-
- currentGroup.Add(shellContent);
- if (shellContent == shellSection.CurrentItem)
- {
- AddMenuItems(shellContent.MenuItems);
- }
- }
-
- if (shellSection.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
- IncrementGroup();
- }
- else
- {
- if (!(shellSection.Parent is TabBar))
- {
- if (Routing.IsImplicit(shellSection) && shellContents.Count == 1)
- {
- if (!ShowInFlyoutMenu(shellContents[0]))
- continue;
-
- currentGroup.Add(shellContents[0]);
- }
- else
- currentGroup.Add(shellSection);
- }
-
- // If we have only a single child we will also show the items menu items
- if (shellContents.Count == 1 && shellSection == shellItem.CurrentItem && shellSection.CurrentItem.MenuItems.Count > 0)
- {
- AddMenuItems(shellSection.CurrentItem.MenuItems);
- }
- }
- }
-
- if (shellItem.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
- IncrementGroup();
- }
- else
- {
- if (!(shellItem is TabBar))
- currentGroup.Add(shellItem);
- }
- }
-
- IncrementGroup();
-
- // If the flyout groupings haven't changed just return
- // the same instance so the caller knows it hasn't changed
- // at a later point this will all get converted to an observable collection
- if (_currentFlyoutViews?.Count == result.Count)
- {
- bool hasChanged = false;
- for (var i = 0; i < result.Count && !hasChanged; i++)
- {
- var topLevelNew = result[i];
- var topLevelPrevious = _currentFlyoutViews[i];
-
- if (topLevelNew.Count != topLevelPrevious.Count)
- {
- hasChanged = true;
- break;
- }
-
- for (var j = 0; j > topLevelNew.Count; j++)
- {
- if (topLevelNew[j] != topLevelPrevious[j])
- {
- hasChanged = true;
- break;
- }
- }
-
- }
-
- if (!hasChanged)
- return false;
- }
-
- _currentFlyoutViews = result;
- return true;
-
- bool ShowInFlyoutMenu(BindableObject bo)
- {
- if (bo is MenuShellItem msi)
- return Shell.GetFlyoutItemIsVisible(msi.MenuItem);
- return Shell.GetFlyoutItemIsVisible(bo);
- }
+ internal void SendFlyoutItemsChanged() => _flyoutManager.CheckIfFlyoutItemsChanged();
- void AddMenuItems(MenuItemCollection menuItems)
- {
- foreach (var item in menuItems)
- {
- if (ShowInFlyoutMenu(item))
- currentGroup.Add(item);
- }
- }
+ public IEnumerable FlyoutItems => _flyoutManager.FlyoutItems;
- void IncrementGroup()
- {
- if (currentGroup.Count > 0)
- {
- result.Add(currentGroup);
- currentGroup = new List();
- }
- }
- }
+ List> IShellController.GenerateFlyoutGrouping() =>
+ _flyoutManager.GenerateFlyoutGrouping();
internal void SendStructureChanged()
{
@@ -1070,7 +921,7 @@ static void OnCurrentItemChanging(BindableObject bindable, object oldValue, obje
var shellSection = shellItem.CurrentItem;
var shellContent = shellSection.CurrentItem;
var stack = shellSection.Stack;
- shell._navigationManager.ProposeNavigationOutsideGotoAsync(ShellNavigationSource.ShellItemChanged, shellItem, shellSection, shellContent, stack, false);
+ shell._navigationManager.ProposeNavigationOutsideGotoAsync(ShellNavigationSource.ShellItemChanged, shellItem, shellSection, shellContent, stack, false, true);
}
static void UpdateChecked(Element root, bool isChecked = true)
@@ -1346,6 +1197,85 @@ void IPropertyPropagationController.PropagatePropertyChanged(string propertyName
}
+ #region Shell Flyout Content
+
+
+ public static readonly BindableProperty FlyoutContentProperty =
+ BindableProperty.Create(nameof(FlyoutContent), typeof(object), typeof(Shell), null, BindingMode.OneTime, propertyChanging: OnFlyoutContentChanging);
+
+ public static readonly BindableProperty FlyoutContentTemplateProperty =
+ BindableProperty.Create(nameof(FlyoutContentTemplate), typeof(DataTemplate), typeof(Shell), null, BindingMode.OneTime, propertyChanging: OnFlyoutContentTemplateChanging);
+
+ View _flyoutContentView;
+
+ public View FlyoutContent
+ {
+ get => (View)GetValue(FlyoutContentProperty);
+ set => SetValue(FlyoutContentProperty, value);
+ }
+
+ public DataTemplate FlyoutContentTemplate
+ {
+ get => (DataTemplate)GetValue(FlyoutContentTemplateProperty);
+ set => SetValue(FlyoutContentTemplateProperty, value);
+ }
+
+ View FlyoutContentView
+ {
+ get => _flyoutContentView;
+ set
+ {
+ if (_flyoutContentView == value)
+ return;
+
+ if (_flyoutContentView != null)
+ OnChildRemoved(_flyoutContentView, -1);
+ _flyoutContentView = value;
+ if (_flyoutContentView != null)
+ OnChildAdded(_flyoutContentView);
+ }
+ }
+
+ void OnFlyoutContentChanged(object oldVal, object newVal)
+ {
+ if (FlyoutContentTemplate == null)
+ {
+ if (newVal is View newFlyoutContent)
+ FlyoutContentView = newFlyoutContent;
+ else
+ FlyoutContentView = null;
+ }
+ }
+
+ void OnFlyoutContentTemplateChanged(DataTemplate oldValue, DataTemplate newValue)
+ {
+ if (newValue == null)
+ {
+ if (FlyoutContent is View flyoutContentView)
+ FlyoutContentView = flyoutContentView;
+ else
+ FlyoutContentView = null;
+ }
+ else
+ {
+ var newContentView = (View)newValue.CreateContent(FlyoutContent, this);
+ FlyoutContentView = newContentView;
+ }
+ }
+
+ static void OnFlyoutContentChanging(BindableObject bindable, object oldValue, object newValue)
+ {
+ var shell = (Shell)bindable;
+ shell.OnFlyoutContentChanged(oldValue, newValue);
+ }
+
+ static void OnFlyoutContentTemplateChanging(BindableObject bindable, object oldValue, object newValue)
+ {
+ var shell = (Shell)bindable;
+ shell.OnFlyoutContentTemplateChanged((DataTemplate)oldValue, (DataTemplate)newValue);
+ }
+ #endregion
+
[EditorBrowsable(EditorBrowsableState.Never)]
public static void VerifyShellUWPFlagEnabled(
diff --git a/Xamarin.Forms.Core/Shell/ShellFlyoutItemsManager.cs b/Xamarin.Forms.Core/Shell/ShellFlyoutItemsManager.cs
new file mode 100644
index 00000000000..428de9ef576
--- /dev/null
+++ b/Xamarin.Forms.Core/Shell/ShellFlyoutItemsManager.cs
@@ -0,0 +1,248 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace Xamarin.Forms
+{
+ internal class ShellFlyoutItemsManager
+ {
+ readonly Shell _shell;
+ List> _lastGeneratedFlyoutItems;
+ public event EventHandler FlyoutItemsChanged;
+ IShellController ShellController => _shell;
+ ReadOnlyObservableCollectionWithSource> _flyoutItemsReadonly;
+
+ public IEnumerable FlyoutItems => _flyoutItemsReadonly;
+ public ShellFlyoutItemsManager(Shell shell)
+ {
+ _shell = shell;
+ _flyoutItemsReadonly = new ReadOnlyObservableCollectionWithSource>();
+ }
+
+
+ void SyncFlyoutItemsToReadOnlyCollection()
+ {
+ var flyoutItems = _flyoutItemsReadonly.List;
+
+ // sync the number of groups
+ for (var i = flyoutItems.Count; i < _lastGeneratedFlyoutItems.Count; i++)
+ flyoutItems.Add(new ReadOnlyObservableCollectionWithSource());
+
+ for (var i = _lastGeneratedFlyoutItems.Count; i < flyoutItems.Count; i++)
+ flyoutItems.RemoveAt(i);
+
+ for (var i = 0; i < _lastGeneratedFlyoutItems.Count; i++)
+ {
+ var source = _lastGeneratedFlyoutItems[i];
+ var dest = ((ReadOnlyObservableCollectionWithSource)flyoutItems[i]).List;
+
+ for (var j = dest.Count - 1; j >= 0; j--)
+ {
+ var item = dest[j];
+ if (!source.Contains(item))
+ dest.RemoveAt(j);
+ }
+
+ for (var j = 0; j < source.Count; j++)
+ {
+ var item = source[j];
+ var destIndex = dest.IndexOf(item);
+
+ if (destIndex == -1)
+ {
+ if (j < dest.Count)
+ dest.Insert(j, item);
+ else
+ dest.Add(item);
+ }
+ else
+ {
+ if (j < dest.Count)
+ {
+ if(destIndex != j)
+ dest.Move(destIndex, j);
+ }
+ else
+ dest.Add(item);
+ }
+ }
+ }
+ }
+
+ public void CheckIfFlyoutItemsChanged()
+ {
+ if (UpdateFlyoutGroupings())
+ {
+ FlyoutItemsChanged?.Invoke(this, EventArgs.Empty);
+ SyncFlyoutItemsToReadOnlyCollection();
+ }
+ }
+
+ public List> GenerateFlyoutGrouping()
+ {
+ if (_lastGeneratedFlyoutItems == null)
+ UpdateFlyoutGroupings();
+
+ return _lastGeneratedFlyoutItems;
+ }
+
+ bool UpdateFlyoutGroupings()
+ {
+ // The idea here is to create grouping such that the Flyout would
+ // render correctly if it renderered each item in the groups in order
+ // but put a line between the groups. This is needed because our grouping can
+ // actually go 3 layers deep.
+
+ // Ideally this lets us control where lines are drawn in the core code
+ // just by changing how we generate these groupings
+
+ var result = new List>();
+
+ var currentGroup = new List();
+
+ foreach (var shellItem in ShellController.GetItems())
+ {
+ if (!ShowInFlyoutMenu(shellItem))
+ continue;
+
+ if (Routing.IsImplicit(shellItem) || shellItem.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
+ {
+ if (shellItem.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
+ IncrementGroup();
+
+ foreach (var shellSection in (shellItem as IShellItemController).GetItems())
+ {
+ if (!ShowInFlyoutMenu(shellSection))
+ continue;
+
+ var shellContents = ((IShellSectionController)shellSection).GetItems();
+ if (Routing.IsImplicit(shellSection) || shellSection.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
+ {
+ foreach (var shellContent in shellContents)
+ {
+ if (!ShowInFlyoutMenu(shellContent))
+ continue;
+
+ currentGroup.Add(shellContent);
+ if (shellContent == shellSection.CurrentItem)
+ {
+ AddMenuItems(shellContent.MenuItems);
+ }
+ }
+
+ if (shellSection.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
+ IncrementGroup();
+ }
+ else
+ {
+ if (!(shellSection.Parent is TabBar))
+ {
+ if (Routing.IsImplicit(shellSection) && shellContents.Count == 1)
+ {
+ if (!ShowInFlyoutMenu(shellContents[0]))
+ continue;
+
+ currentGroup.Add(shellContents[0]);
+ }
+ else
+ currentGroup.Add(shellSection);
+ }
+
+ // If we have only a single child we will also show the items menu items
+ if (shellContents.Count == 1 && shellSection == shellItem.CurrentItem && shellSection.CurrentItem.MenuItems.Count > 0)
+ {
+ AddMenuItems(shellSection.CurrentItem.MenuItems);
+ }
+ }
+ }
+
+ if (shellItem.FlyoutDisplayOptions == FlyoutDisplayOptions.AsMultipleItems)
+ IncrementGroup();
+ }
+ else
+ {
+ if (!(shellItem is TabBar))
+ currentGroup.Add(shellItem);
+ }
+ }
+
+ IncrementGroup();
+
+ // If the flyout groupings haven't changed just return
+ // the same instance so the caller knows it hasn't changed
+ // at a later point this will all get converted to an observable collection
+ if (_lastGeneratedFlyoutItems?.Count == result.Count)
+ {
+ bool hasChanged = false;
+ for (var i = 0; i < result.Count && !hasChanged; i++)
+ {
+ var topLevelNew = result[i];
+ var topLevelPrevious = _lastGeneratedFlyoutItems[i];
+
+ if (topLevelNew.Count != topLevelPrevious.Count)
+ {
+ hasChanged = true;
+ break;
+ }
+
+ for (var j = 0; j > topLevelNew.Count; j++)
+ {
+ if (topLevelNew[j] != topLevelPrevious[j])
+ {
+ hasChanged = true;
+ break;
+ }
+ }
+
+ }
+
+ if (!hasChanged)
+ return false;
+ }
+
+ _lastGeneratedFlyoutItems = result;
+ return true;
+
+ bool ShowInFlyoutMenu(BindableObject bo)
+ {
+ if (bo is MenuShellItem msi)
+ return Shell.GetFlyoutItemIsVisible(msi.MenuItem);
+
+ return Shell.GetFlyoutItemIsVisible(bo);
+ }
+
+ void AddMenuItems(MenuItemCollection menuItems)
+ {
+ foreach (var item in menuItems)
+ {
+ if (ShowInFlyoutMenu(item))
+ currentGroup.Add(item);
+ }
+ }
+
+ void IncrementGroup()
+ {
+ if (currentGroup.Count > 0)
+ {
+ result.Add(currentGroup);
+ currentGroup = new List();
+ }
+ }
+ }
+
+ class ReadOnlyObservableCollectionWithSource : ReadOnlyObservableCollection
+ {
+ public ReadOnlyObservableCollectionWithSource() : this(new ObservableCollection())
+ {
+ }
+
+ public ReadOnlyObservableCollectionWithSource(ObservableCollection list) : base(list)
+ {
+ List = list;
+ }
+
+ public ObservableCollection List { get; }
+ }
+ }
+}
diff --git a/Xamarin.Forms.Core/Shell/ShellNavigationManager.cs b/Xamarin.Forms.Core/Shell/ShellNavigationManager.cs
index 63c302632a6..dc9cb98418a 100644
--- a/Xamarin.Forms.Core/Shell/ShellNavigationManager.cs
+++ b/Xamarin.Forms.Core/Shell/ShellNavigationManager.cs
@@ -51,7 +51,7 @@ public async Task GoToAsync(ShellNavigationParameters shellNavigationParameters)
// This scenario only comes up from UI iniated navigation (i.e. switching tabs)
if (deferredArgs == null)
{
- var navigatingArgs = ProposeNavigation(source, state, _shell.CurrentState != null);
+ var navigatingArgs = ProposeNavigation(source, state, _shell.CurrentState != null, animate ?? true);
bool accept = !navigatingArgs.NavigationDelayedOrCancelled;
if (navigatingArgs.DeferredTask != null)
@@ -273,13 +273,20 @@ public static void ApplyQueryAttributes(Element element, IDictionary stack, bool canCancel)
+ public bool ProposeNavigationOutsideGotoAsync(
+ ShellNavigationSource source,
+ ShellItem shellItem,
+ ShellSection shellSection,
+ ShellContent shellContent,
+ IReadOnlyList stack,
+ bool canCancel,
+ bool isAnimated)
{
if (_accumulateNavigatedEvents)
return true;
var proposedState = GetNavigationState(shellItem, shellSection, shellContent, stack, shellSection.Navigation.ModalStack);
- var navArgs = ProposeNavigation(source, proposedState, canCancel);
+ var navArgs = ProposeNavigation(source, proposedState, canCancel, isAnimated);
if (navArgs.DeferralCount > 0)
{
@@ -302,12 +309,20 @@ public bool ProposeNavigationOutsideGotoAsync(ShellNavigationSource source, Shel
return !navArgs.NavigationDelayedOrCancelled;
}
- ShellNavigatingEventArgs ProposeNavigation(ShellNavigationSource source, ShellNavigationState proposedState, bool canCancel)
+ ShellNavigatingEventArgs ProposeNavigation(
+ ShellNavigationSource source,
+ ShellNavigationState proposedState,
+ bool canCancel,
+ bool isAnimated)
{
if (_accumulateNavigatedEvents)
return null;
- var navArgs = new ShellNavigatingEventArgs(_shell.CurrentState, proposedState, source, canCancel);
+ var navArgs = new ShellNavigatingEventArgs(_shell.CurrentState, proposedState, source, canCancel)
+ {
+ Animate = isAnimated
+ };
+
HandleNavigating(navArgs);
return navArgs;
diff --git a/Xamarin.Forms.Platform.Android/Renderers/ShellFlyoutLayout.cs b/Xamarin.Forms.Platform.Android/Renderers/ShellFlyoutLayout.cs
new file mode 100644
index 00000000000..6dfec37bde6
--- /dev/null
+++ b/Xamarin.Forms.Platform.Android/Renderers/ShellFlyoutLayout.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Android.Content;
+using Android.Runtime;
+using Android.Util;
+using AndroidX.CoordinatorLayout.Widget;
+
+namespace Xamarin.Forms.Platform.Android
+{
+ class ShellFlyoutLayout : CoordinatorLayout
+ {
+ public ShellFlyoutLayout(Context context) : base(context)
+ {
+ }
+
+ public ShellFlyoutLayout(Context context, IAttributeSet attrs) : base(context, attrs)
+ {
+ }
+
+ public ShellFlyoutLayout(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr)
+ {
+ }
+
+ protected ShellFlyoutLayout(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
+ {
+ }
+
+ public Action LayoutChanging { get; set; }
+ protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
+ {
+ LayoutChanging?.Invoke();
+ base.OnLayout(changed, left, top, right, bottom);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ LayoutChanging = null;
+
+ base.Dispose(disposing);
+ }
+ }
+}
diff --git a/Xamarin.Forms.Platform.Android/Renderers/ShellFlyoutTemplatedContentRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/ShellFlyoutTemplatedContentRenderer.cs
index 8e4ad3a91d8..430a23bab85 100644
--- a/Xamarin.Forms.Platform.Android/Renderers/ShellFlyoutTemplatedContentRenderer.cs
+++ b/Xamarin.Forms.Platform.Android/Renderers/ShellFlyoutTemplatedContentRenderer.cs
@@ -31,12 +31,13 @@ public class ShellFlyoutTemplatedContentRenderer : Java.Lang.Object, IShellFlyou
Drawable _defaultBackgroundColor;
ImageView _bgImage;
AppBarLayout _appBar;
- RecyclerView _recycler;
- ShellFlyoutRecyclerAdapter _adapter;
+ AView _flyoutContentView;
+ ShellViewRenderer _contentView;
View _flyoutHeader;
ShellViewRenderer _footerView;
int _actionBarHeight;
- ScrollLayoutManager _layoutManager;
+ int _flyoutHeight;
+ int _flyoutWidth;
protected IShellContext ShellContext => _shellContext;
protected AView FooterView => _footerView?.NativeView;
@@ -62,10 +63,7 @@ protected virtual void LoadView(IShellContext shellContext)
return;
}
- var coordinator = LayoutInflater.FromContext(context).Inflate(Resource.Layout.FlyoutContent, null);
-
- Profile.FramePartition("Find Recycler");
- _recycler = coordinator.FindViewById(Resource.Id.flyoutcontent_recycler);
+ var coordinator = (ViewGroup)LayoutInflater.FromContext(context).Inflate(Resource.Layout.FlyoutContent, null);
Profile.FramePartition("Find AppBar");
_appBar = coordinator.FindViewById(Resource.Id.flyoutcontent_appbar);
@@ -79,12 +77,7 @@ protected virtual void LoadView(IShellContext shellContext)
_actionBarHeight = (int)context.ToPixels(56);
UpdateFlyoutHeader();
- Profile.FramePartition("Recycler.SetAdapter");
- _adapter = new ShellFlyoutRecyclerAdapter(shellContext, OnElementSelected);
- _recycler.SetClipToPadding(false);
- _recycler.SetLayoutManager(_layoutManager = new ScrollLayoutManager(context, (int)Orientation.Vertical, false));
- _recycler.SetLayoutManager(new LinearLayoutManager(context, (int)Orientation.Vertical, false));
- _recycler.SetAdapter(_adapter);
+ UpdateFlyoutContent();
Profile.FramePartition("Initialize BgImage");
var metrics = context.Resources.DisplayMetrics;
@@ -121,6 +114,9 @@ protected virtual void LoadView(IShellContext shellContext)
UpdateFlyoutFooter();
Profile.FrameEnd();
+
+ if (View is ShellFlyoutLayout sfl)
+ sfl.LayoutChanging += OnFlyoutViewLayoutChanged;
}
void OnFlyoutHeaderMeasureInvalidated(object sender, EventArgs e)
@@ -154,6 +150,65 @@ protected virtual void OnShellPropertyChanged(object sender, PropertyChangedEven
Shell.FlyoutFooterProperty,
Shell.FlyoutFooterTemplateProperty))
UpdateFlyoutFooter();
+ else if (e.IsOneOf(
+ Shell.FlyoutContentProperty,
+ Shell.FlyoutContentTemplateProperty))
+ UpdateFlyoutContent();
+ }
+
+ protected virtual void UpdateFlyoutContent()
+ {
+ if (!_rootView.IsAlive())
+ return;
+
+ var index = 0;
+ if (_flyoutContentView != null)
+ {
+ index = _rootView.IndexOfChild(_flyoutContentView);
+ _rootView.RemoveView(_flyoutContentView);
+ }
+
+ _flyoutContentView = CreateFlyoutContent(_rootView);
+ if (_flyoutContentView == null)
+ return;
+
+ _rootView.AddView(_flyoutContentView, index);
+ UpdateContentLayout();
+ }
+
+ AView CreateFlyoutContent(ViewGroup rootView)
+ {
+ _rootView = rootView;
+ if (_contentView != null)
+ {
+ var oldContentView = _contentView;
+ _contentView = null;
+ oldContentView.TearDown();
+ }
+
+ var content = ((IShellController)ShellContext.Shell).FlyoutContent;
+ if (content == null)
+ {
+ var lp = new CoordinatorLayout.LayoutParams(CoordinatorLayout.LayoutParams.MatchParent, CoordinatorLayout.LayoutParams.MatchParent);
+ lp.Behavior = new AppBarLayout.ScrollingViewBehavior();
+ var context = ShellContext.AndroidContext;
+ Profile.FramePartition("Recycler.SetAdapter");
+ var recyclerView = new RecyclerViewContainer(context, new ShellFlyoutRecyclerAdapter(ShellContext, OnElementSelected))
+ {
+ LayoutParameters = lp
+ };
+
+ return recyclerView;
+ }
+
+ _contentView = new ShellViewRenderer(ShellContext.AndroidContext, content);
+
+ _contentView.NativeView.LayoutParameters = new CoordinatorLayout.LayoutParams(LP.MatchParent, LP.MatchParent)
+ {
+ Behavior = new AppBarLayout.ScrollingViewBehavior()
+ };
+
+ return _contentView.NativeView;
}
protected virtual void UpdateFlyoutHeader()
@@ -186,6 +241,8 @@ protected virtual void UpdateFlyoutHeader()
};
_appBar.AddView(_headerView);
UpdateFlyoutHeaderBehavior();
+
+ UpdateContentLayout();
}
protected virtual void UpdateFlyoutFooter()
@@ -205,23 +262,76 @@ protected virtual void UpdateFlyoutFooter()
_footerView = new ShellViewRenderer(_shellContext.AndroidContext, footer);
- _footerView.NativeView.LayoutParameters = new CoordinatorLayout.LayoutParams(LP.MatchParent, LP.WrapContent)
+ _rootView.AddView(_footerView.NativeView);
+
+ if (_footerView.NativeView.LayoutParameters is CoordinatorLayout.LayoutParams cl)
+ cl.Gravity = (int)(GravityFlags.Bottom | GravityFlags.End);
+
+ UpdateFooterLayout();
+ UpdateContentLayout();
+ UpdateContentBottomMargin();
+ }
+
+ void UpdateFooterLayout()
+ {
+ if (_footerView != null)
{
- Gravity = (int)(GravityFlags.Bottom | GravityFlags.End)
- };
+ _footerView.LayoutView(_shellContext.AndroidContext.FromPixels(_rootView.LayoutParameters.Width), double.PositiveInfinity);
+ }
+ }
- _footerView.LayoutView(_shellContext.AndroidContext.FromPixels(_rootView.LayoutParameters.Width), double.PositiveInfinity);
- _rootView.AddView(_footerView.NativeView);
- if (_recycler?.LayoutParameters is CoordinatorLayout.LayoutParams cl)
+ void UpdateContentLayout()
+ {
+ if (_contentView != null)
+ {
+ if (_contentView == null)
+ return;
+
+ var height =
+ (View.MeasuredHeight) -
+ (FooterView?.MeasuredHeight ?? 0) -
+ (_headerView?.MeasuredHeight ?? 0);
+
+ var width = View.MeasuredWidth;
+
+ _contentView.LayoutView(
+ ShellContext.AndroidContext.FromPixels(width),
+ ShellContext.AndroidContext.FromPixels(height));
+ }
+ }
+
+ void UpdateContentBottomMargin()
+ {
+ if (_flyoutContentView?.LayoutParameters is CoordinatorLayout.LayoutParams cl)
+ {
+ cl.BottomMargin = (int)_shellContext.AndroidContext.ToPixels(_footerView?.View.Height ?? 0);
+ }
+ }
+
+ void OnFlyoutViewLayoutChanged()
+ {
+
+ if (View?.MeasuredHeight > 0 &&
+ View?.MeasuredWidth > 0 &&
+ (_flyoutHeight != View.MeasuredHeight ||
+ _flyoutWidth != View.MeasuredWidth)
+ )
{
- cl.BottomMargin = (int)_shellContext.AndroidContext.ToPixels(_footerView.View.Height);
+ _flyoutHeight = View.MeasuredHeight;
+ _flyoutWidth = View.MeasuredWidth;
+
+ UpdateFooterLayout();
+ UpdateContentLayout();
+ UpdateContentBottomMargin();
}
}
void UpdateVerticalScrollMode()
{
- if (_layoutManager != null)
- _layoutManager.ScrollVertically = _shellContext.Shell.FlyoutVerticalScrollMode;
+ if (_flyoutContentView is RecyclerView rv && rv.GetLayoutManager() is ScrollLayoutManager lm)
+ {
+ lm.ScrollVertically = _shellContext.Shell.FlyoutVerticalScrollMode;
+ }
}
protected virtual void UpdateFlyoutBackground()
@@ -367,30 +477,25 @@ protected override void Dispose(bool disposing)
if (_rootView != null && _footerView?.NativeView != null)
_rootView.RemoveView(_footerView.NativeView);
- if (_recycler != null)
- {
- _recycler.SetLayoutManager(null);
- _recycler.SetAdapter(null);
- _recycler.Dispose();
- }
+ if (View != null && View is ShellFlyoutLayout sfl)
+ sfl.LayoutChanging -= OnFlyoutViewLayoutChanged;
- _adapter?.Dispose();
+ _contentView?.TearDown();
+ _flyoutContentView?.Dispose();
_headerView.Dispose();
_footerView?.TearDown();
_rootView.Dispose();
- _layoutManager?.Dispose();
_defaultBackgroundColor?.Dispose();
_bgImage?.Dispose();
+ _contentView = null;
_flyoutHeader = null;
_rootView = null;
_headerView = null;
_shellContext = null;
_appBar = null;
- _recycler = null;
- _adapter = null;
+ _flyoutContentView = null;
_defaultBackgroundColor = null;
- _layoutManager = null;
_bgImage = null;
_footerView = null;
}
@@ -478,6 +583,41 @@ protected override void Dispose(bool disposing)
}
}
+ class RecyclerViewContainer : RecyclerView
+ {
+ bool _disposed;
+ ShellFlyoutRecyclerAdapter _shellFlyoutRecyclerAdapter;
+ ScrollLayoutManager _layoutManager;
+
+ public RecyclerViewContainer(Context context, ShellFlyoutRecyclerAdapter shellFlyoutRecyclerAdapter) : base(context)
+ {
+ _shellFlyoutRecyclerAdapter = shellFlyoutRecyclerAdapter;
+ SetClipToPadding(false);
+ SetLayoutManager(_layoutManager = new ScrollLayoutManager(context, (int)Orientation.Vertical, false));
+ SetLayoutManager(new LinearLayoutManager(context, (int)Orientation.Vertical, false));
+ SetAdapter(_shellFlyoutRecyclerAdapter);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (_disposed)
+ return;
+
+ _disposed = true;
+ if (disposing)
+ {
+ SetLayoutManager(null);
+ SetAdapter(null);
+ _shellFlyoutRecyclerAdapter?.Dispose();
+ _layoutManager?.Dispose();
+ _shellFlyoutRecyclerAdapter = null;
+ _layoutManager = null;
+ }
+
+ base.Dispose(disposing);
+ }
+ }
+
internal class ScrollLayoutManager : LinearLayoutManager
{
public ScrollMode ScrollVertically { get; set; } = ScrollMode.Auto;
diff --git a/Xamarin.Forms.Platform.Android/Renderers/ShellSectionRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/ShellSectionRenderer.cs
index 4d3686e1306..26afd186d93 100644
--- a/Xamarin.Forms.Platform.Android/Renderers/ShellSectionRenderer.cs
+++ b/Xamarin.Forms.Platform.Android/Renderers/ShellSectionRenderer.cs
@@ -128,6 +128,7 @@ public ShellSectionRenderer(IShellContext shellContext)
Fragment IShellObservableFragment.Fragment => this;
public ShellSection ShellSection { get; set; }
+ protected IShellContext ShellContext => _shellContext;
IShellSectionController SectionController => (IShellSectionController)ShellSection;
public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
diff --git a/Xamarin.Forms.Platform.Android/Renderers/ShellToolbarTracker.cs b/Xamarin.Forms.Platform.Android/Renderers/ShellToolbarTracker.cs
index d68456be9f5..349b7e66aee 100644
--- a/Xamarin.Forms.Platform.Android/Renderers/ShellToolbarTracker.cs
+++ b/Xamarin.Forms.Platform.Android/Renderers/ShellToolbarTracker.cs
@@ -50,7 +50,7 @@ void IFlyoutBehaviorObserver.OnFlyoutBehaviorChanged(FlyoutBehavior behavior)
SearchHandler _searchHandler;
IShellSearchView _searchView;
ContainerView _titleViewContainer;
- IShellContext _shellContext;
+ protected IShellContext ShellContext { get; private set; }
//assume the default
Color _tintColor = Color.Default;
Toolbar _toolbar;
@@ -62,7 +62,7 @@ void IFlyoutBehaviorObserver.OnFlyoutBehaviorChanged(FlyoutBehavior behavior)
public ShellToolbarTracker(IShellContext shellContext, Toolbar toolbar, DrawerLayout drawerLayout)
{
- _shellContext = shellContext ?? throw new ArgumentNullException(nameof(shellContext));
+ ShellContext = shellContext ?? throw new ArgumentNullException(nameof(shellContext));
_toolbar = toolbar ?? throw new ArgumentNullException(nameof(toolbar));
_drawerLayout = drawerLayout ?? throw new ArgumentNullException(nameof(drawerLayout));
_appBar = _toolbar.Parent.GetParentOfType();
@@ -70,7 +70,7 @@ public ShellToolbarTracker(IShellContext shellContext, Toolbar toolbar, DrawerLa
_globalLayoutListener = new GenericGlobalLayoutListener(() => UpdateNavBarHasShadow(Page));
_appBar.ViewTreeObserver.AddOnGlobalLayoutListener(_globalLayoutListener);
_toolbar.SetNavigationOnClickListener(this);
- ((IShellController)_shellContext.Shell).AddFlyoutBehaviorObserver(this);
+ ((IShellController)ShellContext.Shell).AddFlyoutBehaviorObserver(this);
}
public bool CanNavigateBack
@@ -138,7 +138,7 @@ void AView.IOnClickListener.OnClick(AView v)
else if (CanNavigateBack)
OnNavigateBack();
else
- _shellContext.Shell.FlyoutIsPresented = !_shellContext.Shell.FlyoutIsPresented;
+ ShellContext.Shell.FlyoutIsPresented = !ShellContext.Shell.FlyoutIsPresented;
}
}
@@ -161,9 +161,9 @@ protected override void Dispose(bool disposing)
_toolbar.DisposeMenuItems(_currentToolbarItems, OnToolbarItemPropertyChanged);
- ((IShellController)_shellContext.Shell)?.RemoveFlyoutBehaviorObserver(this);
+ ((IShellController)ShellContext.Shell)?.RemoveFlyoutBehaviorObserver(this);
- UpdateTitleView(_shellContext.AndroidContext, _toolbar, null);
+ UpdateTitleView(ShellContext.AndroidContext, _toolbar, null);
if (_searchView != null)
{
@@ -184,7 +184,7 @@ protected override void Dispose(bool disposing)
_globalLayoutListener = null;
_backButtonBehavior = null;
SearchHandler = null;
- _shellContext = null;
+ ShellContext = null;
_drawerToggle = null;
_searchView = null;
Page = null;
@@ -197,7 +197,7 @@ protected override void Dispose(bool disposing)
protected virtual IShellSearchView GetSearchView(Context context)
{
- return new ShellSearchView(context, _shellContext);
+ return new ShellSearchView(context, ShellContext);
}
protected async virtual void OnNavigateBack()
@@ -427,7 +427,7 @@ protected virtual async void UpdateLeftBarButtonItem(Context context, Toolbar to
//this needs to be set after SyncState
- UpdateToolbarIconAccessibilityText(toolbar, _shellContext.Shell);
+ UpdateToolbarIconAccessibilityText(toolbar, ShellContext.Shell);
}
@@ -467,7 +467,7 @@ protected virtual Task UpdateDrawerArrowFromFlyoutIcon(Context context, ActionBa
protected virtual void UpdateMenuItemIcon(Context context, IMenuItem menuItem, ToolbarItem toolBarItem)
{
- _shellContext.ApplyDrawableAsync(toolBarItem, ToolbarItem.IconImageSourceProperty, baseDrawable =>
+ ShellContext.ApplyDrawableAsync(toolBarItem, ToolbarItem.IconImageSourceProperty, baseDrawable =>
{
if (baseDrawable != null)
{
@@ -547,12 +547,12 @@ protected virtual void UpdateToolbarItems(Toolbar toolbar, Page page)
var menu = toolbar.Menu;
var sortedItems = page.ToolbarItems.OrderBy(x => x.Order);
- toolbar.UpdateMenuItems(sortedItems, _shellContext.AndroidContext, TintColor, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems);
+ toolbar.UpdateMenuItems(sortedItems, ShellContext.AndroidContext, TintColor, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems);
SearchHandler = Shell.GetSearchHandler(page);
if (SearchHandler != null && SearchHandler.SearchBoxVisibility != SearchBoxVisibility.Hidden)
{
- var context = _shellContext.AndroidContext;
+ var context = ShellContext.AndroidContext;
if (_searchView == null)
{
_searchView = GetSearchView(context);
@@ -609,7 +609,7 @@ protected virtual void UpdateToolbarItems(Toolbar toolbar, Page page)
void OnToolbarItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
var sortedItems = Page.ToolbarItems.OrderBy(x => x.Order).ToList();
- _toolbar.OnToolbarItemPropertyChanged(e, (ToolbarItem)sender, sortedItems, _shellContext.AndroidContext, TintColor, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems);
+ _toolbar.OnToolbarItemPropertyChanged(e, (ToolbarItem)sender, sortedItems, ShellContext.AndroidContext, TintColor, OnToolbarItemPropertyChanged, _currentMenuItems, _currentToolbarItems);
}
void OnSearchViewAttachedToWindow(object sender, AView.ViewAttachedToWindowEventArgs e)
@@ -636,12 +636,12 @@ void OnSearchViewAttachedToWindow(object sender, AView.ViewAttachedToWindowEvent
void UpdateLeftBarButtonItem()
{
- UpdateLeftBarButtonItem(_shellContext.AndroidContext, _toolbar, _drawerLayout, Page);
+ UpdateLeftBarButtonItem(ShellContext.AndroidContext, _toolbar, _drawerLayout, Page);
}
void UpdateTitleView()
{
- UpdateTitleView(_shellContext.AndroidContext, _toolbar, Shell.GetTitleView(Page));
+ UpdateTitleView(ShellContext.AndroidContext, _toolbar, Shell.GetTitleView(Page));
}
void UpdateToolbarItems()
diff --git a/Xamarin.Forms.Platform.Android/Resources/Layout/FlyoutContent.axml b/Xamarin.Forms.Platform.Android/Resources/Layout/FlyoutContent.axml
index 4f68147325f..551b6c8b2f9 100644
--- a/Xamarin.Forms.Platform.Android/Resources/Layout/FlyoutContent.axml
+++ b/Xamarin.Forms.Platform.Android/Resources/Layout/FlyoutContent.axml
@@ -1,6 +1,6 @@
-
-
-
-
+
diff --git a/Xamarin.Forms.Platform.UAP/Shell/ShellFlyoutItemRenderer.cs b/Xamarin.Forms.Platform.UAP/Shell/ShellFlyoutItemRenderer.cs
index 4c8239bb732..64870889b45 100644
--- a/Xamarin.Forms.Platform.UAP/Shell/ShellFlyoutItemRenderer.cs
+++ b/Xamarin.Forms.Platform.UAP/Shell/ShellFlyoutItemRenderer.cs
@@ -18,6 +18,8 @@ public class ShellFlyoutItemRenderer : ContentControl
new PropertyMetadata(default(bool), IsSelectedChanged));
View _content;
+ object _previousDataContext;
+ double _previousWidth;
FrameworkElement FrameworkElement { get; set; }
public ShellFlyoutItemRenderer()
@@ -34,6 +36,11 @@ public bool IsSelected
void OnDataContextChanged(Windows.UI.Xaml.FrameworkElement sender, Windows.UI.Xaml.DataContextChangedEventArgs args)
{
+ if (_previousDataContext == args.NewValue)
+ return;
+
+ _previousWidth = -1;
+ _previousDataContext = args.NewValue;
if (_content != null)
{
if (_content.BindingContext is INotifyPropertyChanged inpc)
@@ -95,12 +102,6 @@ void OnMeasureInvalidated(object sender, EventArgs e)
OnMeasureInvalidated();
}
- protected override Windows.Foundation.Size MeasureOverride(Windows.Foundation.Size availableSize)
- {
- return base.MeasureOverride(availableSize);
- }
-
- double _previousWidth;
private void OnLayoutUpdated(object sender, object e)
{
if (this.ActualWidth > 0 && this.ActualWidth != _content.Width && _previousWidth != this.ActualWidth)
diff --git a/Xamarin.Forms.Platform.iOS/Renderers/ShellFlyoutContentRenderer.cs b/Xamarin.Forms.Platform.iOS/Renderers/ShellFlyoutContentRenderer.cs
index e14d192bc1c..4c752455ca0 100644
--- a/Xamarin.Forms.Platform.iOS/Renderers/ShellFlyoutContentRenderer.cs
+++ b/Xamarin.Forms.Platform.iOS/Renderers/ShellFlyoutContentRenderer.cs
@@ -14,6 +14,7 @@ public class ShellFlyoutContentRenderer : UIViewController, IShellFlyoutContentR
UIView _footerView;
View _footer;
ShellTableViewController _tableViewController;
+ ShellFlyoutLayoutManager _shellFlyoutContentManager;
public event EventHandler WillAppear;
public event EventHandler WillDisappear;
@@ -22,10 +23,10 @@ public ShellFlyoutContentRenderer(IShellContext context)
{
_shellContext = context;
_tableViewController = CreateShellTableViewController();
+ _shellFlyoutContentManager = _tableViewController?.ShellFlyoutContentManager;
AddChildViewController(_tableViewController);
context.Shell.PropertyChanged += HandleShellPropertyChanged;
-
}
protected virtual ShellTableViewController CreateShellTableViewController()
@@ -55,6 +56,12 @@ protected virtual void HandleShellPropertyChanged(object sender, PropertyChanged
{
UpdateFlyoutFooter();
}
+ else if (e.IsOneOf(
+ Shell.FlyoutContentProperty,
+ Shell.FlyoutContentTemplateProperty))
+ {
+ UpdateFlyoutContent();
+ }
}
void UpdateFlowDirection()
@@ -123,7 +130,6 @@ void UpdateFlyoutFooter(View view)
View.AddSubview(_footerView);
_footerView.ClipsToBounds = true;
- _tableViewController.FooterView = _footerView;
_footer.MeasureInvalidated += OnFooterMeasureInvalidated;
}
@@ -170,6 +176,7 @@ public override void ViewWillLayoutSubviews()
{
base.ViewWillLayoutSubviews();
UpdateFooterPosition();
+ UpdateFlyoutContent();
}
protected virtual void UpdateBackground()
@@ -251,7 +258,6 @@ public override void ViewDidLoad()
{
base.ViewDidLoad();
- View.AddSubview(_tableViewController.View);
UpdateFlyoutHeader();
UpdateFlyoutFooter();
@@ -273,6 +279,19 @@ public override void ViewDidLoad()
UpdateFlowDirection();
}
+ void UpdateFlyoutContent()
+ {
+ var view = (_shellContext.Shell as IShellController).FlyoutContent;
+
+ if (view != null)
+ _shellFlyoutContentManager.SetCustomContent(view);
+ else
+ _shellFlyoutContentManager.SetDefaultContent(_tableViewController.TableView);
+
+ if(_shellFlyoutContentManager.ContentView != null)
+ View.InsertSubview(_shellFlyoutContentManager.ContentView, 0);
+ }
+
public override void ViewWillAppear(bool animated)
{
UpdateFlowDirection();
diff --git a/Xamarin.Forms.Platform.iOS/Renderers/ShellFlyoutLayoutManager.cs b/Xamarin.Forms.Platform.iOS/Renderers/ShellFlyoutLayoutManager.cs
new file mode 100644
index 00000000000..7e94e16888d
--- /dev/null
+++ b/Xamarin.Forms.Platform.iOS/Renderers/ShellFlyoutLayoutManager.cs
@@ -0,0 +1,332 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using CoreAnimation;
+using CoreGraphics;
+using Foundation;
+using UIKit;
+
+namespace Xamarin.Forms.Platform.iOS
+{
+ class ShellFlyoutLayoutManager
+ {
+ double _headerMin = 56;
+ double _headerOffset = 0;
+ UIView _contentView;
+ UIScrollView ScrollView { get; set; }
+ UIContainerView _headerView;
+ UIView _footerView;
+ double _headerSize;
+ readonly IShellContext _context;
+ Action removeScolledEvent;
+
+ IShellController ShellController => _context.Shell;
+ public ShellFlyoutLayoutManager(IShellContext context)
+ {
+ _context = context;
+ _context.Shell.PropertyChanged += OnShellPropertyChanged;
+ ShellController.StructureChanged += OnStructureChanged;
+ }
+
+ public void SetCustomContent(View content)
+ {
+ if (content == Content)
+ return;
+
+ removeScolledEvent?.Invoke();
+ removeScolledEvent = null;
+
+ if (Content != null)
+ {
+ var oldRenderer = Platform.GetRenderer(Content);
+ var oldContentView = ContentView;
+ var oldContent = Content;
+
+ Content = null;
+ ContentView = null;
+ oldContent.ClearValue(Platform.RendererProperty);
+ oldContentView?.RemoveFromSuperview();
+ oldRenderer?.Dispose();
+ }
+ // If the user hasn't defined custom content then only the ContentView is set
+ else if(ContentView != null)
+ {
+ var oldContentView = ContentView;
+ ContentView = null;
+ oldContentView.RemoveFromSuperview();
+ }
+
+ Content = content;
+ if (Content != null)
+ {
+ var renderer = Platform.CreateRenderer(Content);
+ ContentView = renderer.NativeView;
+ Platform.SetRenderer(Content, renderer);
+ ContentView.ClipsToBounds = true;
+
+ // not sure if there's a more efficient way to do this
+ // I can test the native control to see if it inherits from UIScrollView
+ // But the CollectionViewRenderer doesn't inherit from UIScrollView
+ if (Content is ScrollView sv)
+ {
+ sv.Scrolled += ScrollViewScrolled;
+ removeScolledEvent = () => sv.Scrolled -= ScrollViewScrolled;
+ void ScrollViewScrolled(object sender, ScrolledEventArgs e) =>
+ OnScrolled((nfloat)sv.ScrollY);
+ }
+ else if(Content is CollectionView cv)
+ {
+ cv.Scrolled += CollectionViewScrolled;
+ removeScolledEvent = () => cv.Scrolled -= CollectionViewScrolled;
+ void CollectionViewScrolled(object sender, ItemsViewScrolledEventArgs e) =>
+ OnScrolled((nfloat)e.VerticalOffset);
+ }
+ else if (Content is ListView lv)
+ {
+ lv.Scrolled += ListViewScrolled;
+ removeScolledEvent = () => lv.Scrolled -= ListViewScrolled;
+ void ListViewScrolled(object sender, ScrolledEventArgs e) =>
+ OnScrolled((nfloat)e.ScrollY);
+ }
+ }
+ }
+
+ public void SetDefaultContent(UIView view)
+ {
+ if (ContentView == view)
+ return;
+
+ SetCustomContent(null);
+ ContentView = view;
+ }
+
+ public View Content
+ {
+ get;
+ private set;
+ }
+
+ public UIView ContentView
+ {
+ get
+ {
+ return _contentView;
+ }
+ private set
+ {
+ _contentView = value;
+
+ if (ContentView is UIScrollView sv1)
+ ScrollView = sv1;
+ else if (ContentView is IVisualElementRenderer ver && ver.NativeView is UIScrollView uIScroll)
+ ScrollView = uIScroll;
+
+ if (ScrollView != null && Forms.IsiOS11OrNewer)
+ ScrollView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Never;
+
+ if (ScrollView != null)
+ {
+ LayoutParallax();
+ SetHeaderContentInset();
+ }
+ }
+ }
+
+ public virtual UIContainerView HeaderView
+ {
+ get => _headerView;
+ set
+ {
+ if (_headerView == value)
+ return;
+
+ if (_headerView != null)
+ _headerView.HeaderSizeChanged -= OnHeaderFooterSizeChanged;
+
+ _headerView = value;
+
+ if (_headerView != null)
+ _headerView.HeaderSizeChanged += OnHeaderFooterSizeChanged;
+ }
+ }
+
+ public virtual UIView FooterView
+ {
+ get => _footerView;
+ set
+ {
+ if (_footerView == value)
+ return;
+
+ _footerView = value;
+ }
+ }
+
+ void OnHeaderFooterSizeChanged(object sender, EventArgs e)
+ {
+ _headerSize = HeaderMax;
+ SetHeaderContentInset();
+ LayoutParallax();
+ }
+
+ internal void SetHeaderContentInset()
+ {
+ if (ScrollView == null)
+ return;
+
+ var offset = ScrollView.ContentInset.Top;
+
+ if (HeaderView != null)
+ ScrollView.ContentInset = new UIEdgeInsets((nfloat)HeaderMax, 0, 0, 0);
+ else
+ ScrollView.ContentInset = new UIEdgeInsets(Platform.SafeAreaInsetsForWindow.Top, 0, 0, 0);
+
+ offset -= ScrollView.ContentInset.Top;
+
+ ScrollView.ContentOffset =
+ new CGPoint(ScrollView.ContentOffset.X, ScrollView.ContentOffset.Y + offset);
+
+ UpdateVerticalScrollMode();
+ }
+
+ public void UpdateVerticalScrollMode()
+ {
+ if (ScrollView == null)
+ return;
+
+ switch (_context.Shell.FlyoutVerticalScrollMode)
+ {
+ case ScrollMode.Auto:
+ ScrollView.ScrollEnabled = true;
+ ScrollView.AlwaysBounceVertical = false;
+ break;
+ case ScrollMode.Enabled:
+ ScrollView.ScrollEnabled = true;
+ ScrollView.AlwaysBounceVertical = true;
+ break;
+ case ScrollMode.Disabled:
+ ScrollView.ScrollEnabled = false;
+ ScrollView.AlwaysBounceVertical = false;
+ break;
+ }
+ }
+
+ public void LayoutParallax()
+ {
+ var parent = ContentView?.Superview;
+ if (parent == null)
+ return;
+
+ nfloat footerHeight = 0;
+
+ if (FooterView != null)
+ footerHeight = FooterView.Frame.Height;
+
+ var contentViewYOffset = HeaderView?.Frame.Height ?? 0;
+ if (ScrollView != null)
+ {
+ if (Content == null)
+ {
+ ContentView.Frame =
+ new CGRect(parent.Bounds.X, HeaderTopMargin, parent.Bounds.Width, parent.Bounds.Height - HeaderTopMargin - footerHeight);
+ }
+ else
+ {
+ ContentView.Frame =
+ new CGRect(parent.Bounds.X, HeaderTopMargin, parent.Bounds.Width, parent.Bounds.Height - HeaderTopMargin - footerHeight);
+
+ if (Content != null)
+ Layout.LayoutChildIntoBoundingRegion(Content, new Rectangle(0, 0, ContentView.Frame.Width, ContentView.Frame.Height - contentViewYOffset));
+ }
+ }
+ else
+ {
+ ContentView.Frame =
+ new CGRect(parent.Bounds.X, HeaderTopMargin + contentViewYOffset, parent.Bounds.Width, parent.Bounds.Height - HeaderTopMargin - footerHeight - contentViewYOffset);
+
+ if (Content != null)
+ Layout.LayoutChildIntoBoundingRegion(Content, new Rectangle(0, 0, ContentView.Frame.Width, ContentView.Frame.Height));
+ }
+
+ if (HeaderView != null && !double.IsNaN(_headerSize))
+ {
+ var margin = HeaderView.Margin;
+ var leftMargin = margin.Left - margin.Right;
+
+ HeaderView.Frame = new CGRect(leftMargin, _headerOffset, parent.Frame.Width, _headerSize + HeaderTopMargin);
+
+ if (_context.Shell.FlyoutHeaderBehavior == FlyoutHeaderBehavior.Scroll && HeaderTopMargin > 0 && _headerOffset < 0)
+ {
+ var headerHeight = Math.Max(_headerMin, _headerSize + _headerOffset);
+ CAShapeLayer shapeLayer = new CAShapeLayer();
+ CGRect rect = new CGRect(0, _headerOffset * -1, parent.Frame.Width, headerHeight);
+ var path = CGPath.FromRect(rect);
+ shapeLayer.Path = path;
+ HeaderView.Layer.Mask = shapeLayer;
+ }
+ else if (HeaderView.Layer.Mask != null)
+ HeaderView.Layer.Mask = null;
+ }
+ }
+
+ void OnStructureChanged(object sender, EventArgs e) => UpdateVerticalScrollMode();
+
+ void OnShellPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.Is(Shell.FlyoutHeaderBehaviorProperty))
+ {
+ SetHeaderContentInset();
+ LayoutParallax();
+ }
+ else if (e.Is(Shell.FlyoutVerticalScrollModeProperty))
+ UpdateVerticalScrollMode();
+ }
+
+ public void ViewDidLoad()
+ {
+ HeaderView?.MeasureIfNeeded();
+ SetHeaderContentInset();
+ }
+
+ public void OnScrolled(nfloat contentOffsetY)
+ {
+ var headerBehavior = _context.Shell.FlyoutHeaderBehavior;
+
+ switch (headerBehavior)
+ {
+ case FlyoutHeaderBehavior.Default:
+ case FlyoutHeaderBehavior.Fixed:
+ _headerSize = HeaderMax;
+ _headerOffset = 0;
+ break;
+
+ case FlyoutHeaderBehavior.Scroll:
+ _headerSize = HeaderMax;
+ _headerOffset = Math.Min(0, -(HeaderMax + contentOffsetY));
+ break;
+
+ case FlyoutHeaderBehavior.CollapseOnScroll:
+ _headerSize = Math.Max(_headerMin, -contentOffsetY);
+ _headerOffset = 0;
+ break;
+ }
+
+ LayoutParallax();
+ }
+
+ double HeaderMax => HeaderView?.MeasuredHeight ?? 0;
+ double HeaderTopMargin => (HeaderView != null) ? HeaderView.Margin.Top - HeaderView.Margin.Bottom : 0;
+
+ public void TearDown()
+ {
+ _context.Shell.PropertyChanged -= OnShellPropertyChanged;
+ ShellController.StructureChanged -= OnStructureChanged;
+ SetCustomContent(null);
+ ContentView = null;
+ HeaderView = null;
+ FooterView = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewController.cs b/Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewController.cs
index 02aa5835b4d..4208155548d 100644
--- a/Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewController.cs
+++ b/Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewController.cs
@@ -11,167 +11,75 @@ public class ShellTableViewController : UITableViewController
{
readonly IShellContext _context;
readonly ShellTableViewSource _source;
- double _headerMin = 56;
- double _headerOffset = 0;
- double _headerSize;
bool _isDisposed;
Action _onElementSelected;
- UIContainerView _headerView;
- UIView _footerView;
-
- IShellController ShellController => ((IShellController)_context.Shell);
+ IShellController ShellController => _context.Shell;
public ShellTableViewController(IShellContext context, UIContainerView headerView, Action onElementSelected) : this(context, onElementSelected)
{
+ ShellFlyoutContentManager = new ShellFlyoutLayoutManager(context);
HeaderView = headerView;
}
public ShellTableViewController(IShellContext context, Action onElementSelected)
{
+ ShellFlyoutContentManager = ShellFlyoutContentManager ?? new ShellFlyoutLayoutManager(context);
_context = context;
_onElementSelected = onElementSelected;
_source = CreateShellTableViewSource();
- _source.ScrolledEvent += OnScrolled;
ShellController.FlyoutItemsChanged += OnFlyoutItemsChanged;
- _context.Shell.PropertyChanged += OnShellPropertyChanged;
+ _source.ScrolledEvent += OnScrolled;
}
- public virtual UIContainerView HeaderView
+ internal ShellFlyoutLayoutManager ShellFlyoutContentManager
{
- get => _headerView;
- set
- {
- if (_headerView == value)
- return;
-
- if (_headerView != null)
- _headerView.HeaderSizeChanged -= OnHeaderFooterSizeChanged;
-
- _headerView = value;
-
- if (_headerView != null)
- _headerView.HeaderSizeChanged += OnHeaderFooterSizeChanged;
- }
+ get;
+ set;
}
- public virtual UIView FooterView
+ void OnScrolled(object sender, UIScrollView e)
{
- get => _footerView;
- set
- {
- if (_footerView == value)
- return;
-
- _footerView = value;
- }
+ ShellFlyoutContentManager.OnScrolled(e.ContentOffset.Y);
}
- protected ShellTableViewSource CreateShellTableViewSource()
+ public virtual UIContainerView HeaderView
{
- return new ShellTableViewSource(_context, _onElementSelected);
+ get => ShellFlyoutContentManager.HeaderView;
+ set => ShellFlyoutContentManager.HeaderView = value;
}
- void OnShellPropertyChanged(object sender, PropertyChangedEventArgs e)
+ public virtual UIView FooterView
{
- if (e.Is(Shell.FlyoutHeaderBehaviorProperty))
- {
- SetHeaderContentInset();
- LayoutParallax();
- }
- else if (e.Is(Shell.FlyoutVerticalScrollModeProperty))
- UpdateVerticalScrollMode();
+ get => ShellFlyoutContentManager.FooterView;
+ set => ShellFlyoutContentManager.FooterView = value;
}
- void OnHeaderFooterSizeChanged(object sender, EventArgs e)
+ protected ShellTableViewSource CreateShellTableViewSource()
{
- _headerSize = HeaderMax;
- SetHeaderContentInset();
- LayoutParallax();
+ return new ShellTableViewSource(_context, _onElementSelected);
}
void OnFlyoutItemsChanged(object sender, EventArgs e)
{
_source.ClearCache();
TableView.ReloadData();
- UpdateVerticalScrollMode();
- }
-
- void UpdateVerticalScrollMode()
- {
- switch (_context.Shell.FlyoutVerticalScrollMode)
- {
- case ScrollMode.Auto:
- TableView.ScrollEnabled = true;
- TableView.AlwaysBounceVertical = false;
- break;
- case ScrollMode.Enabled:
- TableView.ScrollEnabled = true;
- TableView.AlwaysBounceVertical = true;
- break;
- case ScrollMode.Disabled:
- TableView.ScrollEnabled = false;
- TableView.AlwaysBounceVertical = false;
- break;
- }
- }
-
- public void LayoutParallax()
- {
- if (TableView?.Superview == null)
- return;
-
- var parent = TableView.Superview;
-
- nfloat footerHeight = 0;
-
- if (FooterView != null)
- footerHeight = FooterView.Frame.Height;
-
- TableView.Frame =
- new CGRect(parent.Bounds.X, HeaderTopMargin, parent.Bounds.Width, parent.Bounds.Height - HeaderTopMargin - footerHeight);
-
- if (HeaderView != null && !double.IsNaN(_headerSize))
- {
- var margin = HeaderView.Margin;
- var leftMargin = margin.Left - margin.Right;
-
- HeaderView.Frame = new CGRect(leftMargin, _headerOffset + HeaderTopMargin, parent.Frame.Width, _headerSize);
-
- if (_context.Shell.FlyoutHeaderBehavior == FlyoutHeaderBehavior.Scroll && HeaderTopMargin > 0 && _headerOffset < 0)
- {
- var headerHeight = Math.Max(_headerMin, _headerSize + _headerOffset);
- CAShapeLayer shapeLayer = new CAShapeLayer();
- CGRect rect = new CGRect(0, _headerOffset * -1, parent.Frame.Width, headerHeight);
- var path = CGPath.FromRect(rect);
- shapeLayer.Path = path;
- HeaderView.Layer.Mask = shapeLayer;
- }
- else if (HeaderView.Layer.Mask != null)
- HeaderView.Layer.Mask = null;
- }
+ ShellFlyoutContentManager.UpdateVerticalScrollMode();
}
- void SetHeaderContentInset()
- {
- if (HeaderView != null)
- TableView.ContentInset = new UIEdgeInsets((nfloat)HeaderMax, 0, 0, 0);
- else
- TableView.ContentInset = new UIEdgeInsets(Platform.SafeAreaInsetsForWindow.Top, 0, 0, 0);
- UpdateVerticalScrollMode();
- }
+ public void LayoutParallax() =>
+ ShellFlyoutContentManager.LayoutParallax();
public override void ViewDidLoad()
- {
+ {
base.ViewDidLoad();
- HeaderView?.MeasureIfNeeded();
TableView.SeparatorStyle = UITableViewCellSeparatorStyle.None;
if (Forms.IsiOS11OrNewer)
TableView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Never;
- SetHeaderContentInset();
TableView.Source = _source;
+ ShellFlyoutContentManager.ViewDidLoad();
}
protected override void Dispose(bool disposing)
@@ -187,50 +95,12 @@ protected override void Dispose(bool disposing)
if (_source != null)
_source.ScrolledEvent -= OnScrolled;
- if (HeaderView != null)
- HeaderView.HeaderSizeChanged -= OnHeaderFooterSizeChanged;
-
- _context.Shell.PropertyChanged -= OnShellPropertyChanged;
-
+ ShellFlyoutContentManager.TearDown();
_onElementSelected = null;
}
-
_isDisposed = true;
base.Dispose(disposing);
}
-
-
- void OnScrolled(object sender, UIScrollView e)
- {
- if (HeaderView == null)
- return;
-
- var headerBehavior = _context.Shell.FlyoutHeaderBehavior;
-
- switch (headerBehavior)
- {
- case FlyoutHeaderBehavior.Default:
- case FlyoutHeaderBehavior.Fixed:
- _headerSize = HeaderMax;
- _headerOffset = 0;
- break;
-
- case FlyoutHeaderBehavior.Scroll:
- _headerSize = HeaderMax;
- _headerOffset = Math.Min(0, -(HeaderMax + e.ContentOffset.Y));
- break;
-
- case FlyoutHeaderBehavior.CollapseOnScroll:
- _headerSize = Math.Max(_headerMin, -e.ContentOffset.Y);
- _headerOffset = 0;
- break;
- }
-
- LayoutParallax();
- }
-
- double HeaderMax => HeaderView?.MeasuredHeight ?? 0;
- double HeaderTopMargin => (HeaderView != null) ? HeaderView.Margin.Top - HeaderView.Margin.Bottom : 0;
}
}
diff --git a/Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewSource.cs b/Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewSource.cs
index e317687a67f..a4c079c9035 100644
--- a/Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewSource.cs
+++ b/Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewSource.cs
@@ -12,7 +12,6 @@ public class ShellTableViewSource : UITableViewSource
readonly Action _onElementSelected;
List> _groups;
Dictionary _cells;
-
IShellController ShellController => _context.Shell;
public ShellTableViewSource(IShellContext context, Action onElementSelected)
@@ -213,4 +212,4 @@ public override void LayoutSubviews()
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Xamarin.Forms.Platform.iOS/Renderers/UIContainerView.cs b/Xamarin.Forms.Platform.iOS/Renderers/UIContainerView.cs
index 9e8d996b042..b2ec3067934 100644
--- a/Xamarin.Forms.Platform.iOS/Renderers/UIContainerView.cs
+++ b/Xamarin.Forms.Platform.iOS/Renderers/UIContainerView.cs
@@ -22,6 +22,11 @@ public UIContainerView(View view)
ClipsToBounds = true;
view.MeasureInvalidated += OnMeasureInvalidated;
MeasuredHeight = double.NaN;
+ _view.BatchCommitted += _view_BatchCommitted;
+ }
+
+ private void _view_BatchCommitted(object sender, Internals.EventArg e)
+ {
}
internal View View => _view;
@@ -30,7 +35,10 @@ public UIContainerView(View view)
internal bool MeasureIfNeeded()
{
- if (double.IsNaN(MeasuredHeight))
+ if (View == null)
+ return false;
+
+ if (double.IsNaN(MeasuredHeight) || Frame.Width != View.Width)
{
ReMeasure();
return true;
@@ -44,7 +52,12 @@ public Thickness Margin
{
if(!_view.IsSet(View.MarginProperty))
{
- _view.Margin = new Thickness(0, (float)Platform.SafeAreaInsetsForWindow.Top, 0, 0);
+ var newMargin = new Thickness(0, (float)Platform.SafeAreaInsetsForWindow.Top, 0, 0);
+
+ if (newMargin != _view.Margin)
+ {
+ _view.Margin = newMargin;
+ }
}
return _view.Margin;
@@ -54,7 +67,6 @@ public Thickness Margin
void ReMeasure()
{
var request = _view.Measure(Frame.Width, double.PositiveInfinity, MeasureFlags.None);
- Layout.LayoutChildIntoBoundingRegion(_view, new Rectangle(0, 0, Frame.Width, request.Request.Height));
MeasuredHeight = request.Request.Height;
HeaderSizeChanged?.Invoke(this, EventArgs.Empty);
}
@@ -62,12 +74,18 @@ void ReMeasure()
void OnMeasureInvalidated(object sender, System.EventArgs e)
{
ReMeasure();
+ LayoutSubviews();
+ }
+
+ public override void WillMoveToSuperview(UIView newsuper)
+ {
+ base.WillMoveToSuperview(newsuper);
+ ReMeasure();
}
public override void LayoutSubviews()
{
- if(!MeasureIfNeeded())
- _view.Layout(Bounds.ToRectangle());
+ _view.Layout(new Rectangle(0, Margin.Top, Frame.Width, MeasuredHeight));
}
protected override void Dispose(bool disposing)
diff --git a/Xamarin.Forms.Platform.iOS/Xamarin.Forms.Platform.iOS.csproj b/Xamarin.Forms.Platform.iOS/Xamarin.Forms.Platform.iOS.csproj
index 7448c38cbf8..286114369bb 100644
--- a/Xamarin.Forms.Platform.iOS/Xamarin.Forms.Platform.iOS.csproj
+++ b/Xamarin.Forms.Platform.iOS/Xamarin.Forms.Platform.iOS.csproj
@@ -192,6 +192,7 @@
+