From 4d5a1f5482eeb43661d0ac972262e09c53e61117 Mon Sep 17 00:00:00 2001 From: Samantha Houts Date: Mon, 25 Jun 2018 05:00:03 -0700 Subject: [PATCH] TitleView (#2586) * Stop crashing when I accidentally click on the Perf gallery * [Core] Add TitleView to NavigationPage - also add unit tests - fixes #1716 * [Android] Add ClearRenderer static method to Platform * [Android] Use Platform.ClearRenderer it's a straight extract from this class * [Android] Implement TitleView and TitleIcon on AppCompat backend using lessons (and some duplicated code) from ListView header/footer views. * [iOS] Implement TitleView * [UWP] Implement TitleIcon and TitleView * [Core] Add NavigationPage.BarHeight * [Android] Use BarHeight * NavigationBarGallery updates * [Core] Add iOS HideNavigationBarSeparator Platform Specific * [iOS] Implement HideNavigationBarSeparator Platform Specific * sample search page * Convert BarHeight to Android platform specific * Reset BarHeight when leaving the gallery * Add a sample TitleView xaml page * VisualElement >> View * Fix comment * Improved SearchTitle sample page * [Core] Set TitleView Parent on Changing instead of Changed Changing is too late for the iOS layout * [iOS] Fix layouts in iOS10 * [iOS] Stop content clipping * Expanded test page * [iOS] Fix HideNavigationBarSeparator for iOS<11 * [iOS] Layout TitleView with margins * More tweaks to test page * [UWP] Fix OnDetailPropertyChanged if/else * [UWP] Comment empty setters * Convert commented code to more useful comment. * Adjust performance test async call --- .../SearchbarEffect.cs | 46 +++ .../Xamarin.Forms.ControlGallery.iOS.csproj | 1 + .../PerformanceGallery/PerformanceGallery.cs | 5 +- Xamarin.Forms.Controls/CoreGallery.cs | 5 +- .../GalleryPages/NavigationBarGallery.cs | 362 +++++++++++++++--- .../GalleryPages/TitleView.xaml | 19 + .../GalleryPages/TitleView.xaml.cs | 20 + .../Xamarin.Forms.Controls.csproj | 3 + .../NavigationUnitTest.cs | 43 +++ Xamarin.Forms.Core/NavigationPage.cs | 30 ++ .../AppCompat/NavigationPage.cs | 30 ++ .../iOSSpecific/NavigationPage.cs | 27 ++ .../AppCompat/NavigationPageRenderer.cs | 217 ++++++++++- Xamarin.Forms.Platform.Android/Platform.cs | 18 + .../Renderers/ListViewRenderer.cs | 25 +- Xamarin.Forms.Platform.UAP/Extensions.cs | 24 ++ .../ITitleIconProvider.cs | 7 + .../ITitleViewProvider.cs | 7 + .../MasterDetailControl.cs | 59 +++ .../MasterDetailControlStyle.xaml | 23 +- .../MasterDetailPageRenderer.cs | 64 +++- .../NavigationPageRenderer.cs | 79 +++- Xamarin.Forms.Platform.UAP/PageControl.cs | 63 ++- .../PageControlStyle.xaml | 36 +- .../ViewToRendererConverter.cs | 4 +- .../Xamarin.Forms.Platform.UAP.csproj | 2 + .../Renderers/NavigationRenderer.cs | 362 +++++++++++++++--- 27 files changed, 1416 insertions(+), 165 deletions(-) create mode 100644 Xamarin.Forms.ControlGallery.iOS/SearchbarEffect.cs create mode 100644 Xamarin.Forms.Controls/GalleryPages/TitleView.xaml create mode 100644 Xamarin.Forms.Controls/GalleryPages/TitleView.xaml.cs create mode 100644 Xamarin.Forms.Core/PlatformConfiguration/AndroidSpecific/AppCompat/NavigationPage.cs create mode 100644 Xamarin.Forms.Platform.UAP/ITitleIconProvider.cs create mode 100644 Xamarin.Forms.Platform.UAP/ITitleViewProvider.cs diff --git a/Xamarin.Forms.ControlGallery.iOS/SearchbarEffect.cs b/Xamarin.Forms.ControlGallery.iOS/SearchbarEffect.cs new file mode 100644 index 000000000..2d66113c0 --- /dev/null +++ b/Xamarin.Forms.ControlGallery.iOS/SearchbarEffect.cs @@ -0,0 +1,46 @@ +using System; +using UIKit; +using Xamarin.Forms; +using Xamarin.Forms.ControlGallery.iOS; +using Xamarin.Forms.Platform.iOS; + +[assembly: ExportEffect(typeof(SearchbarEffect), "SearchbarEffect")] +namespace Xamarin.Forms.ControlGallery.iOS +{ + public class SearchbarEffect : PlatformEffect + { + UIColor _defaultBackColor; + UIColor _defaultTintColor; + UIImage _defaultBackImage; + protected override void OnAttached() + { + if (_defaultBackColor == null) + _defaultBackColor = Control.BackgroundColor; + + Control.BackgroundColor = Color.Cornsilk.ToUIColor(); + + if (Control is UISearchBar searchBar) + { + if (_defaultTintColor == null) + _defaultTintColor = searchBar.BarTintColor; + + if (_defaultBackImage == null) + _defaultBackImage = searchBar.BackgroundImage; + + searchBar.BarTintColor = Color.Goldenrod.ToUIColor(); + searchBar.BackgroundImage = new UIImage(); + } + } + + protected override void OnDetached() + { + Control.BackgroundColor = _defaultBackColor; + + if (Control is UISearchBar searchBar) + { + searchBar.BarTintColor = _defaultTintColor; + searchBar.BackgroundImage = _defaultBackImage; + } + } + } +} diff --git a/Xamarin.Forms.ControlGallery.iOS/Xamarin.Forms.ControlGallery.iOS.csproj b/Xamarin.Forms.ControlGallery.iOS/Xamarin.Forms.ControlGallery.iOS.csproj index 94f2ad988..495bf3d25 100644 --- a/Xamarin.Forms.ControlGallery.iOS/Xamarin.Forms.ControlGallery.iOS.csproj +++ b/Xamarin.Forms.ControlGallery.iOS/Xamarin.Forms.ControlGallery.iOS.csproj @@ -104,6 +104,7 @@ + diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/PerformanceGallery/PerformanceGallery.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/PerformanceGallery/PerformanceGallery.cs index 8038b6fc2..8577aef51 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/PerformanceGallery/PerformanceGallery.cs +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/PerformanceGallery/PerformanceGallery.cs @@ -6,6 +6,7 @@ using System.Reflection; using Xamarin.Forms.CustomAttributes; using Xamarin.Forms.Internals; using System.IO; +using System.Threading.Tasks; #if UITEST using Xamarin.UITest; @@ -43,7 +44,7 @@ namespace Xamarin.Forms.Controls.Issues PerformanceViewModel ViewModel => BindingContext as PerformanceViewModel; - protected override async void Init() + protected override void Init() { _BuildInfo = GetBuildNumber(); @@ -82,7 +83,7 @@ namespace Xamarin.Forms.Controls.Issues Content = new StackLayout { Children = { testRunRef, nextButton, _PerformanceTracker } }; - ViewModel.BenchmarkResults = await PerformanceDataManager.GetScenarioResults(_DeviceIdentifier); + ViewModel.BenchmarkResults = Task.Run(() => PerformanceDataManager.GetScenarioResults(_DeviceIdentifier)).GetAwaiter().GetResult(); nextButton.IsEnabled = true; nextButton.Text = Next; diff --git a/Xamarin.Forms.Controls/CoreGallery.cs b/Xamarin.Forms.Controls/CoreGallery.cs index 468f494e3..d01820d7f 100644 --- a/Xamarin.Forms.Controls/CoreGallery.cs +++ b/Xamarin.Forms.Controls/CoreGallery.cs @@ -384,9 +384,10 @@ namespace Xamarin.Forms.Controls _titleToPage = _pages.ToDictionary(o => o.Title); // avoid NRE for root pages without NavigationBar - if (navigationBehavior == NavigationBehavior.PushAsync && rootPage.GetType() == typeof(CoreNavigationPage)) + if (navigationBehavior == NavigationBehavior.PushAsync && rootPage.GetType () == typeof (CoreNavigationPage)) { - _pages.Add(new GalleryPageFactory(() => new NavigationBarGallery((NavigationPage)rootPage), "NavigationBar Gallery - Legacy")); + _pages.Insert (0, new GalleryPageFactory(() => new NavigationBarGallery((NavigationPage)rootPage), "NavigationBar Gallery - Legacy")); + _pages.Insert(1, new GalleryPageFactory(() => new TitleView(), "TitleView")); } var template = new DataTemplate(typeof(TextCell)); diff --git a/Xamarin.Forms.Controls/GalleryPages/NavigationBarGallery.cs b/Xamarin.Forms.Controls/GalleryPages/NavigationBarGallery.cs index e571539ad..d40e5513b 100644 --- a/Xamarin.Forms.Controls/GalleryPages/NavigationBarGallery.cs +++ b/Xamarin.Forms.Controls/GalleryPages/NavigationBarGallery.cs @@ -1,64 +1,326 @@ -using System.Diagnostics; - +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Xamarin.Forms.PlatformConfiguration; +using Xamarin.Forms.PlatformConfiguration.iOSSpecific; +using Xamarin.Forms.PlatformConfiguration.AndroidSpecific.AppCompat; +using static Xamarin.Forms.PlatformConfiguration.AndroidSpecific.AppCompat.NavigationPage; namespace Xamarin.Forms.Controls { public class NavigationBarGallery : ContentPage { - public NavigationBarGallery (NavigationPage rootNavPage) + NavigationPage _rootNavPage; + public NavigationBarGallery(NavigationPage rootNavPage) { + _rootNavPage = rootNavPage; int toggleBarTextColor = 0; int toggleBarBackgroundColor = 0; - Content = new StackLayout { - Children = { - new Button { - Text = "Change BarTextColor", - Command = new Command (() => { - if (toggleBarTextColor % 2 == 0) { - rootNavPage.BarTextColor = Color.Teal; - } else { - rootNavPage.BarTextColor = Color.Default; - } - toggleBarTextColor++; - }) - }, - new Button { - Text = "Change BarBackgroundColor", - Command = new Command (() => { - if (toggleBarBackgroundColor % 2 == 0) { - rootNavPage.BarBackgroundColor = Color.Navy; - } else { - rootNavPage.BarBackgroundColor = Color.Default; - } - toggleBarBackgroundColor++; + ToolbarItems.Add(new ToolbarItem { Text = "Save" }); - }) - }, - new Button { - Text = "Change Both to default", - Command = new Command (() => { - rootNavPage.BarTextColor = Color.Default; - rootNavPage.BarBackgroundColor = Color.Default; - }) - }, - new Button { - Text = "Make sure Tint still works", - Command = new Command (() => { -#pragma warning disable 618 - rootNavPage.Tint = Color.Red; -#pragma warning restore 618 - }) - }, - new Button { - Text = "Black background, white text", - Command = new Command (() => { - rootNavPage.BarTextColor = Color.White; - rootNavPage.BarBackgroundColor = Color.Black; - }) + NavigationPage.SetTitleIcon(this, "coffee.png"); + + SearchBar searchBar = new SearchBar { HeightRequest = 44, WidthRequest = 100 }; + + // Note: Large and complex controls, such as ListView and TableView, are not recommended. + var controls = new List + { + searchBar, + new ActivityIndicator{ IsRunning = true }, + new BoxView{ BackgroundColor = Color.Red }, + new Button{ Text = "Button!"}, + new DatePicker{}, + new Editor{ Text = "Editor"}, + new Entry{ Placeholder = "Entry"}, + new Image{ Source = "crimson.jpg", HeightRequest = 44 }, + new Label{ Text = "Title View Label!" }, + new Picker{ ItemsSource = Enumerable.Range(0,10).Select(i => $"Item {i}").ToList(), Title = "Picker" }, + new ProgressBar{ Progress = 50 }, + new Slider{}, + new Stepper{}, + new Switch{}, + new TimePicker{} + }; + + int idx = 0; + + NavigationPage.SetTitleView(this, CreateTitleView(controls[idx])); + + rootNavPage.On().SetBarHeight(450); + rootNavPage.On().SetPrefersLargeTitles(false); + + Content = new ScrollView + { + Content = + new StackLayout + { + Children = { + new Button { + Text = "Go to SearchBarTitlePage", + Command = new Command (() => { + rootNavPage.PushAsync(new SearchBarTitlePage(rootNavPage)); + }) + }, + new Button { + Text = "Change BarTextColor", + Command = new Command (() => { + if (toggleBarTextColor % 2 == 0) { + rootNavPage.BarTextColor = Color.Teal; + } else { + rootNavPage.BarTextColor = Color.Default; + } + toggleBarTextColor++; + }) + }, + new Button { + Text = "Change BarBackgroundColor", + Command = new Command (() => { + if (toggleBarBackgroundColor % 2 == 0) { + rootNavPage.BarBackgroundColor = Color.Navy; + } else { + rootNavPage.BarBackgroundColor = Color.Default; + } + toggleBarBackgroundColor++; + + }) + }, + new Button { + Text = "Change Both to default", + Command = new Command (() => { + rootNavPage.BarTextColor = Color.Default; + rootNavPage.BarBackgroundColor = Color.Default; + }) + }, + new Button { + Text = "Make sure Tint still works", + Command = new Command (() => { + #pragma warning disable 618 + rootNavPage.Tint = Color.Red; + #pragma warning restore 618 + }) + }, + new Button { + Text = "Black background, white text", + Command = new Command (() => { + rootNavPage.BarTextColor = Color.White; + rootNavPage.BarBackgroundColor = Color.Black; + }) + }, + new Button { + Text = "Toggle TitleIcon", + Command = new Command (() => { + + var titleIcon = NavigationPage.GetTitleIcon(this); + + if (titleIcon == null) + titleIcon = "coffee.png"; + else + titleIcon = null; + + NavigationPage.SetTitleIcon(this, titleIcon); + }) + }, + new Button { + Text = "Toggle TitleView", + Command = new Command (() => { + + var titleView = NavigationPage.GetTitleView(this); + + if (titleView == null) + titleView = CreateTitleView(controls[idx]); + else + titleView = null; + + NavigationPage.SetTitleView(this, titleView); + }) + }, + new Button { + Text = "Next TitleView", + Command = new Command (() => { + + idx++; + if(idx >=controls.Count) + idx = 0; + + var titleView = CreateTitleView(controls[idx]); + + NavigationPage.SetTitleView(this, titleView); + }) + }, + new Button { + Text = "Toggle Back Title", + Command = new Command (() => { + + var backTitle = NavigationPage.GetBackButtonTitle(rootNavPage); + + if (backTitle == null) + backTitle= "Go back home"; + else + backTitle = null; + + NavigationPage.SetBackButtonTitle(rootNavPage, backTitle); + }) + }, + new Button { + Text = "Toggle Toolbar Item", + Command = new Command (() => { + + if (ToolbarItems.Count > 0) + ToolbarItems.Clear(); + else + ToolbarItems.Add(new ToolbarItem { Text = "Save" }); + }) + }, + new Button { + Text = "Toggle Title", + Command = new Command (() => { + + if (Title == null) + Title = "NavigationBar Gallery - Legacy"; + else + Title = null; + }) + }, + new Button { + Text = "Toggle BarHeight", + Command = new Command (() => { + + if (rootNavPage.On().GetBarHeight() == -1) + rootNavPage.On().SetBarHeight(450); + else + rootNavPage.ClearValue(BarHeightProperty); + }) + } + } } - } }; } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + _rootNavPage.ClearValue(BarHeightProperty); + } + + static View CreateTitleView(View control) + { + control.HorizontalOptions = LayoutOptions.Fill; + control.VerticalOptions = LayoutOptions.CenterAndExpand; + + var titleView = new StackLayout + { + Children = { control }, + BackgroundColor = Color.FromHex("#ccc"), + Margin = new Thickness(15, 0), + }; + return titleView; + } + + class SearchBarTitlePage : ContentPage + { + bool _extended = false; + List items = new List { "The Ocean at the End of the Lane", "So Long, and Thanks for All the Fish", "Twenty Thousand Leagues Under the Sea", "Rosencrantz and Guildenstern Are Dead" }; + ObservableCollection filtereditems; + SearchBar search; + Button button; + ListView list; + public SearchBarTitlePage(NavigationPage parent) + { + filtereditems = new ObservableCollection(items); + + search = new SearchBar { BackgroundColor = Color.Cornsilk, HorizontalOptions = LayoutOptions.FillAndExpand, Margin = new Thickness(10, 0) }; + search.Effects.Add(Effect.Resolve($"{Issues.Effects.ResolutionGroupName}.SearchbarEffect")); + search.TextChanged += Search_TextChanged; + + list = new ListView + { + ItemsSource = filtereditems + }; + + parent.BarBackgroundColor = Color.Cornsilk; + parent.BarTextColor = Color.Orange; + NavigationPage.SetBackButtonTitle(parent, ""); + + switch (Device.RuntimePlatform) + { + case Device.iOS: + + button = new Button(); + + button.Clicked += (s, e) => + { + ToggleContent(parent); + }; + + ToggleContent(parent); + break; + + default: + NavigationPage.SetTitleView(this, search); + Content = list; + break; + } + } + + void ToggleContent(NavigationPage parent) + { + StackLayout topStack = new StackLayout { Children = { button }, BackgroundColor = Color.Cornsilk }; + StackLayout layout = new StackLayout { Children = { topStack, list } }; + + if (_extended) + { + parent.On().SetPrefersLargeTitles(false) + .SetHideNavigationBarSeparator(false); + + NavigationPage.SetTitleView(this, new StackLayout { Children = { search }, HorizontalOptions = LayoutOptions.Fill }); + NavigationPage.SetHasBackButton(this, false); + + button.Text = "Expand"; + Title = "Small Titles"; + } + else + { + topStack.Children.Insert(0, search); + + parent.On().SetPrefersLargeTitles(true) + .SetHideNavigationBarSeparator(true); + + ClearValue(NavigationPage.TitleViewProperty); + NavigationPage.SetHasBackButton(this, true); + + button.Text = "Collapse"; + Title = "Large Titles"; + } + + _extended = !_extended; + Content = layout; + } + + void Search_TextChanged(object sender, TextChangedEventArgs e) + { + for (int i = 0; i < filtereditems.Count; i++) + { + filtereditems.RemoveAt(0); + } + + if (search.Text?.Length >= 3) + { + foreach (var item in items.Where(i => i.ToLower().Contains(search.Text.ToLower()))) + { + if (!filtereditems.Contains(item)) + filtereditems.Add(item); + } + } + else + { + foreach (var item in items) + { + if (!filtereditems.Contains(item)) + filtereditems.Add(item); + } + } + } + } } -} \ No newline at end of file +} diff --git a/Xamarin.Forms.Controls/GalleryPages/TitleView.xaml b/Xamarin.Forms.Controls/GalleryPages/TitleView.xaml new file mode 100644 index 000000000..792bc5fe3 --- /dev/null +++ b/Xamarin.Forms.Controls/GalleryPages/TitleView.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Xamarin.Forms.Controls/GalleryPages/TitleView.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/TitleView.xaml.cs new file mode 100644 index 000000000..44369cf0d --- /dev/null +++ b/Xamarin.Forms.Controls/GalleryPages/TitleView.xaml.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Xamarin.Forms; +using Xamarin.Forms.Xaml; + +namespace Xamarin.Forms.Controls.GalleryPages +{ + [XamlCompilation(XamlCompilationOptions.Compile)] + public partial class TitleView : ContentPage + { + public TitleView () + { + InitializeComponent (); + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj b/Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj index 464b37330..d374bb520 100644 --- a/Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj +++ b/Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj @@ -43,6 +43,9 @@ OnPlatformExample.xaml + + MSBuild:UpdateDesignTimeXaml + MSBuild:UpdateDesignTimeXaml diff --git a/Xamarin.Forms.Core.UnitTests/NavigationUnitTest.cs b/Xamarin.Forms.Core.UnitTests/NavigationUnitTest.cs index 304089487..595db9a11 100644 --- a/Xamarin.Forms.Core.UnitTests/NavigationUnitTest.cs +++ b/Xamarin.Forms.Core.UnitTests/NavigationUnitTest.cs @@ -338,6 +338,49 @@ namespace Xamarin.Forms.Core.UnitTests } + [Test] + public void TitleViewSetProperty() + { + var root = new ContentPage(); + var nav = new NavigationPage(root); + + View target = new View(); + + NavigationPage.SetTitleView(root, target); + + var result = NavigationPage.GetTitleView(root); + + Assert.AreSame(result, target); + } + + [Test] + public void TitleViewSetsParentWhenAdded() + { + var root = new ContentPage(); + var nav = new NavigationPage(root); + + View target = new View(); + + NavigationPage.SetTitleView(root, target); + + Assert.AreSame(root, target.Parent); + } + + [Test] + public void TitleViewClearsParentWhenRemoved() + { + var root = new ContentPage(); + var nav = new NavigationPage(root); + + View target = new View(); + + NavigationPage.SetTitleView(root, target); + + NavigationPage.SetTitleView(root, null); + + Assert.IsNull(target.Parent); + } + [Test] public async Task NavigationChangedEventArgs () { diff --git a/Xamarin.Forms.Core/NavigationPage.cs b/Xamarin.Forms.Core/NavigationPage.cs index b6572cee0..8e3f97b95 100644 --- a/Xamarin.Forms.Core/NavigationPage.cs +++ b/Xamarin.Forms.Core/NavigationPage.cs @@ -27,6 +27,8 @@ namespace Xamarin.Forms public static readonly BindableProperty TitleIconProperty = BindableProperty.CreateAttached("TitleIcon", typeof(FileImageSource), typeof(NavigationPage), default(FileImageSource)); + public static readonly BindableProperty TitleViewProperty = BindableProperty.CreateAttached("TitleView", typeof(View), typeof(NavigationPage), null, propertyChanging: TitleViewPropertyChanging); + static readonly BindablePropertyKey CurrentPagePropertyKey = BindableProperty.CreateReadOnly("CurrentPage", typeof(Page), typeof(NavigationPage), null); public static readonly BindableProperty CurrentPageProperty = CurrentPagePropertyKey.BindableProperty; @@ -103,6 +105,24 @@ namespace Xamarin.Forms private set { SetValue(RootPagePropertyKey, value); } } + static void TitleViewPropertyChanging(BindableObject bindable, object oldValue, object newValue) + { + if (oldValue == newValue) + return; + + if (oldValue != null) + { + var oldElem = (View)oldValue; + oldElem.Parent = null; + } + + if (newValue != null && bindable != null) + { + var newElem = (View)newValue; + newElem.Parent = (Page)bindable; + } + } + public static string GetBackButtonTitle(BindableObject page) { return (string)page.GetValue(BackButtonTitleProperty); @@ -125,6 +145,11 @@ namespace Xamarin.Forms return (FileImageSource)bindable.GetValue(TitleIconProperty); } + public static View GetTitleView(BindableObject bindable) + { + return (View)bindable.GetValue(TitleViewProperty); + } + public Task PopAsync() { return PopAsync(true); @@ -222,6 +247,11 @@ namespace Xamarin.Forms bindable.SetValue(TitleIconProperty, value); } + public static void SetTitleView(BindableObject bindable, View value) + { + bindable.SetValue(TitleViewProperty, value); + } + protected override bool OnBackButtonPressed() { if (CurrentPage.SendBackButtonPressed()) diff --git a/Xamarin.Forms.Core/PlatformConfiguration/AndroidSpecific/AppCompat/NavigationPage.cs b/Xamarin.Forms.Core/PlatformConfiguration/AndroidSpecific/AppCompat/NavigationPage.cs new file mode 100644 index 000000000..a88e1d379 --- /dev/null +++ b/Xamarin.Forms.Core/PlatformConfiguration/AndroidSpecific/AppCompat/NavigationPage.cs @@ -0,0 +1,30 @@ +namespace Xamarin.Forms.PlatformConfiguration.AndroidSpecific.AppCompat +{ + using FormsElement = Forms.NavigationPage; + + public static class NavigationPage + { + public static readonly BindableProperty BarHeightProperty = BindableProperty.Create("BarHeight", typeof(int), typeof(NavigationPage), default(int)); + + public static int GetBarHeight(BindableObject element) + { + return (int)element.GetValue(BarHeightProperty); + } + + public static void SetBarHeight(BindableObject element, int value) + { + element.SetValue(BarHeightProperty, value); + } + + public static int GetBarHeight(this IPlatformElementConfiguration config) + { + return GetBarHeight(config.Element); + } + + public static IPlatformElementConfiguration SetBarHeight(this IPlatformElementConfiguration config, int value) + { + SetBarHeight(config.Element, value); + return config; + } + } +} diff --git a/Xamarin.Forms.Core/PlatformConfiguration/iOSSpecific/NavigationPage.cs b/Xamarin.Forms.Core/PlatformConfiguration/iOSSpecific/NavigationPage.cs index 9edc61809..562df2e6a 100644 --- a/Xamarin.Forms.Core/PlatformConfiguration/iOSSpecific/NavigationPage.cs +++ b/Xamarin.Forms.Core/PlatformConfiguration/iOSSpecific/NavigationPage.cs @@ -72,6 +72,7 @@ namespace Xamarin.Forms.PlatformConfiguration.iOSSpecific } #endregion + #region PrefersLargeTitles public static readonly BindableProperty PrefersLargeTitlesProperty = BindableProperty.Create(nameof(PrefersLargeTitles), typeof(bool), typeof(Page), false); public static bool GetPrefersLargeTitles(BindableObject element) @@ -94,5 +95,31 @@ namespace Xamarin.Forms.PlatformConfiguration.iOSSpecific { return GetPrefersLargeTitles(config.Element); } + #endregion + + #region HideNavigationBarSeparator + public static readonly BindableProperty HideNavigationBarSeparatorProperty = BindableProperty.Create(nameof(HideNavigationBarSeparator), typeof(bool), typeof(Page), false); + + public static bool GetHideNavigationBarSeparator(BindableObject element) + { + return (bool)element.GetValue(HideNavigationBarSeparatorProperty); + } + + public static void SetHideNavigationBarSeparator(BindableObject element, bool value) + { + element.SetValue(HideNavigationBarSeparatorProperty, value); + } + + public static IPlatformElementConfiguration SetHideNavigationBarSeparator(this IPlatformElementConfiguration config, bool value) + { + SetHideNavigationBarSeparator(config.Element, value); + return config; + } + + public static bool HideNavigationBarSeparator(this IPlatformElementConfiguration config) + { + return GetHideNavigationBarSeparator(config.Element); + } + #endregion } } diff --git a/Xamarin.Forms.Platform.Android/AppCompat/NavigationPageRenderer.cs b/Xamarin.Forms.Platform.Android/AppCompat/NavigationPageRenderer.cs index e62dbbb79..59a60f9a2 100644 --- a/Xamarin.Forms.Platform.Android/AppCompat/NavigationPageRenderer.cs +++ b/Xamarin.Forms.Platform.Android/AppCompat/NavigationPageRenderer.cs @@ -23,7 +23,10 @@ using Fragment = Android.Support.V4.App.Fragment; using FragmentManager = Android.Support.V4.App.FragmentManager; using FragmentTransaction = Android.Support.V4.App.FragmentTransaction; using Object = Java.Lang.Object; +using static Xamarin.Forms.PlatformConfiguration.AndroidSpecific.AppCompat.NavigationPage; using static Android.Views.View; +using System.IO; +using Android.Widget; namespace Xamarin.Forms.Platform.Android.AppCompat { @@ -45,6 +48,10 @@ namespace Xamarin.Forms.Platform.Android.AppCompat DrawerLayout _drawerLayout; MasterDetailPage _masterDetailPage; bool _toolbarVisible; + IVisualElementRenderer _titleViewRenderer; + Container _titleView; + ImageView _titleIconView; + ImageSource _imageSource; bool _isAttachedToWindow; bool _didInitialPushPages; @@ -135,6 +142,22 @@ namespace Xamarin.Forms.Platform.Android.AppCompat { _disposed = true; + if (_titleViewRenderer != null) + { + Android.Platform.ClearRenderer(_titleViewRenderer.View); + _titleViewRenderer.Dispose(); + _titleViewRenderer = null; + } + + _toolbar.RemoveView(_titleView); + _titleView?.Dispose(); + _titleView = null; + + _toolbar.RemoveView(_titleIconView); + _titleIconView?.Dispose(); + _titleIconView = null; + + _imageSource = null; if (_toolbarTracker != null) { @@ -312,6 +335,10 @@ namespace Xamarin.Forms.Platform.Android.AppCompat UpdateToolbar(); else if (e.PropertyName == NavigationPage.BarTextColorProperty.PropertyName) UpdateToolbar(); + else if (e.PropertyName == NavigationPage.BackButtonTitleProperty.PropertyName) + UpdateToolbar(); + else if (e.PropertyName == BarHeightProperty.PropertyName) + UpdateToolbar(); } protected override void OnLayout(bool changed, int l, int t, int r, int b) @@ -324,6 +351,9 @@ namespace Xamarin.Forms.Platform.Android.AppCompat int barHeight = ActionBarHeight(); + if (Element.IsSet(BarHeightProperty)) + barHeight = Element.OnThisPlatform().GetBarHeight(); + if (barHeight != _lastActionBarHeight && _lastActionBarHeight > 0) { ResetToolbar(); @@ -476,6 +506,9 @@ namespace Xamarin.Forms.Platform.Android.AppCompat UpdateToolbar(); else if (e.PropertyName == NavigationPage.HasBackButtonProperty.PropertyName) UpdateToolbar(); + else if (e.PropertyName == NavigationPage.TitleIconProperty.PropertyName || + e.PropertyName == NavigationPage.TitleViewProperty.PropertyName) + UpdateToolbar(); } #pragma warning disable 1998 // considered for removal @@ -646,6 +679,20 @@ namespace Xamarin.Forms.Platform.Android.AppCompat { AToolbar oldToolbar = _toolbar; + if (_titleViewRenderer != null) + { + Android.Platform.ClearRenderer(_titleViewRenderer.View); + _titleViewRenderer = null; + } + + _toolbar.RemoveView(_titleView); + _titleView = null; + + _toolbar.RemoveView(_titleIconView); + _titleIconView = null; + + _imageSource = null; + _toolbar.RemoveFromParent(); _toolbar.SetNavigationOnClickListener(null); _toolbar = null; @@ -852,6 +899,7 @@ namespace Xamarin.Forms.Platform.Android.AppCompat bool isNavigated = ((INavigationPageController)Element).StackDepth > 1; bar.NavigationIcon = null; + Page currentPage = Element.CurrentPage; if (isNavigated) { @@ -861,7 +909,7 @@ namespace Xamarin.Forms.Platform.Android.AppCompat toggle.SyncState(); } - if (NavigationPage.GetHasBackButton(Element.CurrentPage)) + if (NavigationPage.GetHasBackButton(currentPage)) { var icon = new DrawerArrowDrawable(activity.SupportActionBar.ThemedContext); icon.Progress = 1; @@ -905,7 +953,112 @@ namespace Xamarin.Forms.Platform.Android.AppCompat if (!textColor.IsDefault) bar.SetTitleTextColor(textColor.ToAndroid().ToArgb()); - bar.Title = Element.CurrentPage.Title ?? ""; + bar.Title = currentPage.Title ?? ""; + + UpdateTitleIcon(); + + UpdateTitleView(); + } + + void UpdateTitleIcon() + { + Page currentPage = Element.CurrentPage; + var source = NavigationPage.GetTitleIcon(currentPage); + + if (source == null) + { + _toolbar.RemoveView(_titleIconView); + _titleIconView?.Dispose(); + _titleIconView = null; + _imageSource = null; + return; + } + + if (_titleIconView == null) + { + _titleIconView = new ImageView(Context); + _toolbar.AddView(_titleIconView, 0); + } + + UpdateBitmap(source, _imageSource); + _imageSource = source; + } + + async void UpdateBitmap(ImageSource source, ImageSource previousSource = null) + { + if (Equals(source, previousSource)) + return; + + _titleIconView.SetImageResource(global::Android.Resource.Color.Transparent); + + Bitmap bitmap = null; + IImageSourceHandler handler; + + if (source != null && (handler = Registrar.Registered.GetHandlerForObject(source)) != null) + { + try + { + bitmap = await handler.LoadImageAsync(source, Context); + } + catch (TaskCanceledException) + { + } + catch (IOException ex) + { + Internals.Log.Warning("Xamarin.Forms.Platform.Android.AppCompat.NavigationPageRenderer", "Error updating bitmap: {0}", ex); + } + } + + if (bitmap == null && source is FileImageSource) + _titleIconView.SetImageResource(ResourceManager.GetDrawableByName(((FileImageSource)source).File)); + else + _titleIconView.SetImageBitmap(bitmap); + + bitmap?.Dispose(); + } + + void UpdateTitleView() + { + AToolbar bar = _toolbar; + + if (bar == null) + return; + + Page currentPage = Element.CurrentPage; + VisualElement titleView = NavigationPage.GetTitleView(currentPage); + if (_titleViewRenderer != null) + { + var reflectableType = _titleViewRenderer as System.Reflection.IReflectableType; + var rendererType = reflectableType != null ? reflectableType.GetTypeInfo().AsType() : _titleViewRenderer.GetType(); + if (titleView == null || Registrar.Registered.GetHandlerTypeForObject(titleView) != rendererType) + { + if (_titleView != null) + _titleView.Child = null; + Android.Platform.ClearRenderer(_titleViewRenderer.View); + _titleViewRenderer.Dispose(); + _titleViewRenderer = null; + } + } + + if (titleView == null) + return; + + if (_titleViewRenderer != null) + _titleViewRenderer.SetElement(titleView); + else + { + _titleViewRenderer = Android.Platform.CreateRenderer(titleView, Context); + + if (_titleView == null) + { + _titleView = new Container(Context); + bar.AddView(_titleView); + } + + _titleView.Child = _titleViewRenderer; + } + + Android.Platform.SetRenderer(titleView, _titleViewRenderer); } void AddTransitionTimer(TaskCompletionSource tcs, Fragment fragment, FragmentManager fragmentManager, IReadOnlyCollection fragmentsToRemove, int duration, bool shouldUpdateToolbar) @@ -969,6 +1122,66 @@ namespace Xamarin.Forms.Platform.Android.AppCompat } } + internal class Container : ViewGroup + { + IVisualElementRenderer _child; + + public Container(IntPtr p, global::Android.Runtime.JniHandleOwnership o) : base(p, o) + { + // Added default constructor to prevent crash in Dispose + } + + public Container(Context context) : base(context) + { + } + + public IVisualElementRenderer Child + { + set + { + if (_child != null) + RemoveView(_child.View); + + _child = value; + + if (value != null) + AddView(value.View); + } + } + + protected override void OnLayout(bool changed, int l, int t, int r, int b) + { + if (_child == null) + return; + + _child.UpdateLayout(); + } + + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + if (_child == null) + { + SetMeasuredDimension(0, 0); + return; + } + + VisualElement element = _child.Element; + + Context ctx = Context; + + var width = (int)ctx.FromPixels(MeasureSpecFactory.GetSize(widthMeasureSpec)); + + SizeRequest request = _child.Element.Measure(width, double.PositiveInfinity, MeasureFlags.IncludeMargins); + Xamarin.Forms.Layout.LayoutChildIntoBoundingRegion(_child.Element, new Rectangle(0, 0, width, request.Request.Height)); + + int widthSpec = MeasureSpecFactory.MakeMeasureSpec((int)ctx.ToPixels(width), MeasureSpecMode.Exactly); + int heightSpec = MeasureSpecFactory.MakeMeasureSpec((int)ctx.ToPixels(request.Request.Height), MeasureSpecMode.Exactly); + + _child.View.Measure(widthMeasureSpec, heightMeasureSpec); + SetMeasuredDimension(widthSpec, heightSpec); + } + } + class DrawerMultiplexedListener : Object, DrawerLayout.IDrawerListener { public List Listeners { get; } = new List(2); diff --git a/Xamarin.Forms.Platform.Android/Platform.cs b/Xamarin.Forms.Platform.Android/Platform.cs index 02b971b6a..52983fa9c 100644 --- a/Xamarin.Forms.Platform.Android/Platform.cs +++ b/Xamarin.Forms.Platform.Android/Platform.cs @@ -17,6 +17,7 @@ using Android.Widget; using Xamarin.Forms.Platform.Android.AppCompat; using FragmentManager = Android.Support.V4.App.FragmentManager; using Xamarin.Forms.Internals; +using AView = Android.Views.View; namespace Xamarin.Forms.Platform.Android { @@ -298,6 +299,23 @@ namespace Xamarin.Forms.Platform.Android throw new InvalidOperationException("RemovePage is not supported globally on Android, please use a NavigationPage."); } + public static void ClearRenderer(AView renderedView) + { + var element = (renderedView as IVisualElementRenderer)?.Element; + var view = element as View; + if (view != null) + { + var renderer = GetRenderer(view); + if (renderer == renderedView) + element.ClearValue(RendererProperty); + renderer?.Dispose(); + renderer = null; + } + var layout = view as IVisualElementRenderer; + layout?.Dispose(); + layout = null; + } + [Obsolete("CreateRenderer(VisualElement) is obsolete as of version 2.5. Please use CreateRendererWithContext(VisualElement, Context) instead.")] public static IVisualElementRenderer CreateRenderer(VisualElement element) { diff --git a/Xamarin.Forms.Platform.Android/Renderers/ListViewRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/ListViewRenderer.cs index b20104f98..a39147db9 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/ListViewRenderer.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/ListViewRenderer.cs @@ -57,7 +57,7 @@ namespace Xamarin.Forms.Platform.Android { if (_headerRenderer != null) { - ClearRenderer(_headerRenderer.View); + Platform.ClearRenderer(_headerRenderer.View); _headerRenderer.Dispose(); _headerRenderer = null; } @@ -67,7 +67,7 @@ namespace Xamarin.Forms.Platform.Android if (_footerRenderer != null) { - ClearRenderer(_footerRenderer.View); + Platform.ClearRenderer(_footerRenderer.View); _footerRenderer.Dispose(); _footerRenderer = null; } @@ -286,23 +286,6 @@ namespace Xamarin.Forms.Platform.Android Control.SetSelectionFromTop(realPositionWithHeader, y); } - void ClearRenderer(AView renderedView) - { - var element = (renderedView as IVisualElementRenderer)?.Element; - var view = element as View; - if (view != null) - { - var renderer = Platform.GetRenderer(view); - if (renderer == renderedView) - element.ClearValue(Platform.RendererProperty); - renderer?.Dispose(); - renderer = null; - } - var layout = view as IVisualElementRenderer; - layout?.Dispose(); - layout = null; - } - void UpdateFooter() { var footer = (VisualElement)Controller.FooterElement; @@ -314,7 +297,7 @@ namespace Xamarin.Forms.Platform.Android { if (_footerView != null) _footerView.Child = null; - ClearRenderer(_footerRenderer.View); + Platform.ClearRenderer(_footerRenderer.View); _footerRenderer.Dispose(); _footerRenderer = null; } @@ -346,7 +329,7 @@ namespace Xamarin.Forms.Platform.Android { if (_headerView != null) _headerView.Child = null; - ClearRenderer(_headerRenderer.View); + Platform.ClearRenderer(_headerRenderer.View); _headerRenderer.Dispose(); _headerRenderer = null; } diff --git a/Xamarin.Forms.Platform.UAP/Extensions.cs b/Xamarin.Forms.Platform.UAP/Extensions.cs index c65dfda74..97a51b5fc 100644 --- a/Xamarin.Forms.Platform.UAP/Extensions.cs +++ b/Xamarin.Forms.Platform.UAP/Extensions.cs @@ -1,8 +1,11 @@ using System; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Windows.Foundation; using Windows.UI.Xaml; using Windows.UI.Xaml.Input; +using Xamarin.Forms.Internals; +using WImageSource = Windows.UI.Xaml.Media.ImageSource; namespace Xamarin.Forms.Platform.UWP { @@ -28,6 +31,27 @@ namespace Xamarin.Forms.Platform.UWP self.SetBinding(property, new Windows.UI.Xaml.Data.Binding { Path = new PropertyPath(path), Converter = converter }); } + public static async Task ToWindowsImageSource(this ImageSource source) + { + IImageSourceHandler handler; + if (source != null && (handler = Registrar.Registered.GetHandlerForObject(source)) != null) + { + try + { + return await handler.LoadImageAsync(source); + } + catch (OperationCanceledException) + { + return null; + } + + } + else + { + return null; + } + } + internal static InputScopeNameValue GetKeyboardButtonType(this ReturnType returnType) { switch (returnType) diff --git a/Xamarin.Forms.Platform.UAP/ITitleIconProvider.cs b/Xamarin.Forms.Platform.UAP/ITitleIconProvider.cs new file mode 100644 index 000000000..f27c0fc4f --- /dev/null +++ b/Xamarin.Forms.Platform.UAP/ITitleIconProvider.cs @@ -0,0 +1,7 @@ +namespace Xamarin.Forms.Platform.UWP +{ + internal interface ITitleIconProvider + { + Windows.UI.Xaml.Media.ImageSource TitleIcon { get; set; } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Platform.UAP/ITitleViewProvider.cs b/Xamarin.Forms.Platform.UAP/ITitleViewProvider.cs new file mode 100644 index 000000000..1262c87da --- /dev/null +++ b/Xamarin.Forms.Platform.UAP/ITitleViewProvider.cs @@ -0,0 +1,7 @@ +namespace Xamarin.Forms.Platform.UWP +{ + internal interface ITitleViewProvider + { + View TitleView { get; set; } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Platform.UAP/MasterDetailControl.cs b/Xamarin.Forms.Platform.UAP/MasterDetailControl.cs index a7c4d92e6..d0a39f0e0 100644 --- a/Xamarin.Forms.Platform.UAP/MasterDetailControl.cs +++ b/Xamarin.Forms.Platform.UAP/MasterDetailControl.cs @@ -4,6 +4,7 @@ using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media; using Xamarin.Forms.PlatformConfiguration.WindowsSpecific; +using WImageSource = Windows.UI.Xaml.Media.ImageSource; namespace Xamarin.Forms.Platform.UWP { @@ -33,6 +34,10 @@ namespace Xamarin.Forms.Platform.UWP public static readonly DependencyProperty DetailTitleProperty = DependencyProperty.Register("DetailTitle", typeof(string), typeof(MasterDetailControl), new PropertyMetadata(default(string))); + public static readonly DependencyProperty DetailTitleIconProperty = DependencyProperty.Register(nameof(DetailTitleIcon), typeof(WImageSource), typeof(MasterDetailControl), new PropertyMetadata(default(WImageSource))); + + public static readonly DependencyProperty DetailTitleViewProperty = DependencyProperty.Register(nameof(DetailTitleView), typeof(View), typeof(MasterDetailControl), new PropertyMetadata(default(View), OnTitleViewPropertyChanged)); + public static readonly DependencyProperty ToolbarForegroundProperty = DependencyProperty.Register("ToolbarForeground", typeof(Brush), typeof(MasterDetailControl), new PropertyMetadata(default(Brush))); @@ -45,6 +50,9 @@ namespace Xamarin.Forms.Platform.UWP public static readonly DependencyProperty DetailTitleVisibilityProperty = DependencyProperty.Register("DetailTitleVisibility", typeof(Visibility), typeof(MasterDetailControl), new PropertyMetadata(default(Visibility))); + public static readonly DependencyProperty DetailTitleViewVisibilityProperty = DependencyProperty.Register(nameof(DetailTitleViewVisibility), typeof(Visibility), typeof(MasterDetailControl), + new PropertyMetadata(default(Visibility))); + public static readonly DependencyProperty MasterToolbarVisibilityProperty = DependencyProperty.Register("MasterToolbarVisibility", typeof(Visibility), typeof(MasterDetailControl), new PropertyMetadata(default(Visibility))); @@ -66,6 +74,7 @@ namespace Xamarin.Forms.Platform.UWP FrameworkElement _detailPresenter; SplitView _split; ToolbarPlacement _toolbarPlacement; + FrameworkElement _titleViewPresenter; public MasterDetailControl() { @@ -110,12 +119,30 @@ namespace Xamarin.Forms.Platform.UWP set { SetValue(DetailTitleProperty, value); } } + public WImageSource DetailTitleIcon + { + get { return (WImageSource)GetValue(DetailTitleIconProperty); } + set { SetValue(DetailTitleIconProperty, value); } + } + + public View DetailTitleView + { + get { return (View)GetValue(DetailTitleViewProperty); } + set { SetValue(DetailTitleViewProperty, value); } + } + public Visibility DetailTitleVisibility { get { return (Visibility)GetValue(DetailTitleVisibilityProperty); } set { SetValue(DetailTitleVisibilityProperty, value); } } + public Visibility DetailTitleViewVisibility + { + get { return (Visibility)GetValue(DetailTitleViewVisibilityProperty); } + set { SetValue(DetailTitleViewVisibilityProperty, value); } + } + public bool IsPaneOpen { get { return (bool)GetValue(IsPaneOpenProperty); } @@ -260,6 +287,7 @@ namespace Xamarin.Forms.Platform.UWP _masterPresenter = GetTemplateChild("MasterPresenter") as FrameworkElement; _detailPresenter = GetTemplateChild("DetailPresenter") as FrameworkElement; + _titleViewPresenter = GetTemplateChild("TitleViewPresenter") as FrameworkElement; _commandBar = GetTemplateChild("CommandBar") as CommandBar; _toolbarPlacementHelper.Initialize(_commandBar, () => ToolbarPlacement, GetTemplateChild); @@ -285,11 +313,24 @@ namespace Xamarin.Forms.Platform.UWP ((MasterDetailControl)dependencyObject).UpdateMode(); } + static void OnTitleViewPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) + { + ((MasterDetailControl)dependencyObject).UpdateTitleViewPresenter(); + } + void OnToggleClicked(object sender, RoutedEventArgs args) { IsPaneOpen = !IsPaneOpen; } + void OnTitleViewPresenterLoaded(object sender, RoutedEventArgs e) + { + if (DetailTitleView == null || _titleViewPresenter == null || _commandBar == null) + return; + + _titleViewPresenter.Width = _commandBar.ActualWidth; + } + void UpdateMode() { if (_split == null) @@ -324,5 +365,23 @@ namespace Xamarin.Forms.Platform.UWP _firstLoad = true; } + + void UpdateTitleViewPresenter() + { + if (DetailTitleView == null) + { + DetailTitleViewVisibility = Visibility.Collapsed; + + if (_titleViewPresenter != null) + _titleViewPresenter.Loaded -= OnTitleViewPresenterLoaded; + } + else + { + DetailTitleViewVisibility = Visibility.Visible; + + if (_titleViewPresenter != null) + _titleViewPresenter.Loaded += OnTitleViewPresenterLoaded; + } + } } } diff --git a/Xamarin.Forms.Platform.UAP/MasterDetailControlStyle.xaml b/Xamarin.Forms.Platform.UAP/MasterDetailControlStyle.xaml index ee51204cf..f9b3790f0 100644 --- a/Xamarin.Forms.Platform.UAP/MasterDetailControlStyle.xaml +++ b/Xamarin.Forms.Platform.UAP/MasterDetailControlStyle.xaml @@ -30,19 +30,30 @@ - + - - + + -