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 @@ +