From 8a7ee8632e0ff7dbcb9034704f742b170b28b211 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Thu, 2 May 2019 04:26:42 -0600 Subject: [PATCH] [Android] Fix previewer exceptions with shell (#5955) * shell preivewer * nonappcompat hack * - moove up null check * internal IsDesignerContext --- .../AppCompat/CarouselPageRenderer.cs | 2 +- .../AppCompat/MasterDetailContainer.cs | 2 +- .../AppCompat/NavigationPageRenderer.cs | 10 ++-- .../AppCompat/Platform.cs | 7 +-- .../AppCompat/TabbedPageRenderer.cs | 4 +- .../ContextExtensions.cs | 45 +++++++++++++++++ .../Renderers/ContainerView.cs | 2 +- .../ShellFlyoutTemplatedContentRenderer.cs | 58 ++++++++++++++++++---- .../Renderers/ShellRenderer.cs | 56 +++++++++++++-------- .../Renderers/ShellToolbarTracker.cs | 18 +++---- .../Resources/Layout/FlyoutContent.axml | 4 +- Xamarin.Forms.Sandbox/ShellPage.xaml | 9 ++++ Xamarin.Forms.Sandbox/ShellPage.xaml.cs | 20 ++++++++ Xamarin.Forms.Sandbox/Xamarin.Forms.Sandbox.csproj | 6 +++ 14 files changed, 184 insertions(+), 59 deletions(-) create mode 100644 Xamarin.Forms.Sandbox/ShellPage.xaml create mode 100644 Xamarin.Forms.Sandbox/ShellPage.xaml.cs diff --git a/Xamarin.Forms.Platform.Android/AppCompat/CarouselPageRenderer.cs b/Xamarin.Forms.Platform.Android/AppCompat/CarouselPageRenderer.cs index 33b8cb0..374febe 100644 --- a/Xamarin.Forms.Platform.Android/AppCompat/CarouselPageRenderer.cs +++ b/Xamarin.Forms.Platform.Android/AppCompat/CarouselPageRenderer.cs @@ -101,7 +101,7 @@ namespace Xamarin.Forms.Platform.Android.AppCompat { base.OnElementChanged(e); - var activity = (FormsAppCompatActivity)Context; + var activity = (FormsAppCompatActivity)Context.GetActivity(); if (e.OldElement != null) ((IPageController)e.OldElement).InternalChildren.CollectionChanged -= OnChildrenCollectionChanged; diff --git a/Xamarin.Forms.Platform.Android/AppCompat/MasterDetailContainer.cs b/Xamarin.Forms.Platform.Android/AppCompat/MasterDetailContainer.cs index d279d5d..9670dc8 100644 --- a/Xamarin.Forms.Platform.Android/AppCompat/MasterDetailContainer.cs +++ b/Xamarin.Forms.Platform.Android/AppCompat/MasterDetailContainer.cs @@ -26,7 +26,7 @@ namespace Xamarin.Forms.Platform.Android.AppCompat public bool MarkedForDispose { get; internal set; } = false; - FragmentManager FragmentManager => _fragmentManager ?? (_fragmentManager = ((FormsAppCompatActivity)Context).SupportFragmentManager); + FragmentManager FragmentManager => _fragmentManager ?? (_fragmentManager = Context.GetFragmentManager()); protected override void OnLayout(bool changed, int l, int t, int r, int b) { diff --git a/Xamarin.Forms.Platform.Android/AppCompat/NavigationPageRenderer.cs b/Xamarin.Forms.Platform.Android/AppCompat/NavigationPageRenderer.cs index 961982d..2aa4356 100644 --- a/Xamarin.Forms.Platform.Android/AppCompat/NavigationPageRenderer.cs +++ b/Xamarin.Forms.Platform.Android/AppCompat/NavigationPageRenderer.cs @@ -88,7 +88,7 @@ namespace Xamarin.Forms.Platform.Android.AppCompat { if (_platform == null) { - if (Context is FormsAppCompatActivity activity) + if (Context.GetActivity() is FormsAppCompatActivity activity) { _platform = activity.Platform; } @@ -122,7 +122,7 @@ namespace Xamarin.Forms.Platform.Android.AppCompat } } - FragmentManager FragmentManager => _fragmentManager ?? (_fragmentManager = ((FormsAppCompatActivity)Context).SupportFragmentManager); + FragmentManager FragmentManager => _fragmentManager ?? (_fragmentManager = Context.GetFragmentManager()); IPageController PageController => Element; @@ -745,7 +745,7 @@ namespace Xamarin.Forms.Platform.Android.AppCompat void SetupToolbar() { Context context = Context; - var activity = (FormsAppCompatActivity)context; + var activity = context.GetActivity(); AToolbar bar; if (FormsAppCompatActivity.ToolbarResource != 0) @@ -935,7 +935,6 @@ namespace Xamarin.Forms.Platform.Android.AppCompat return; Context context = Context; - var activity = (FormsAppCompatActivity)context; AToolbar bar = _toolbar; ActionBarDrawerToggle toggle = _drawerToggle; @@ -954,8 +953,9 @@ namespace Xamarin.Forms.Platform.Android.AppCompat toggle.SyncState(); } - if (NavigationPage.GetHasBackButton(currentPage)) + if (NavigationPage.GetHasBackButton(currentPage) && !Context.IsDesignerContext()) { + var activity = (global::Android.Support.V7.App.AppCompatActivity)context.GetActivity(); var icon = new DrawerArrowDrawable(activity.SupportActionBar.ThemedContext); icon.Progress = 1; bar.NavigationIcon = icon; diff --git a/Xamarin.Forms.Platform.Android/AppCompat/Platform.cs b/Xamarin.Forms.Platform.Android/AppCompat/Platform.cs index 8ebca28..2436d9f 100644 --- a/Xamarin.Forms.Platform.Android/AppCompat/Platform.cs +++ b/Xamarin.Forms.Platform.Android/AppCompat/Platform.cs @@ -369,8 +369,7 @@ namespace Xamarin.Forms.Platform.Android.AppCompat void LayoutRootPage(Page page, int width, int height) { - var activity = (FormsAppCompatActivity)_context; - page.Layout(new Rectangle(0, 0, activity.FromPixels(width), activity.FromPixels(height))); + page.Layout(new Rectangle(0, 0, _context.FromPixels(width), _context.FromPixels(height))); } Task PresentModal(Page modal, bool animated) @@ -457,9 +456,7 @@ namespace Xamarin.Forms.Platform.Android.AppCompat { if (changed) { - var activity = (FormsAppCompatActivity)Context; - - _modal.Layout(new Rectangle(0, 0, activity.FromPixels(r - l), activity.FromPixels(b - t))); + _modal.Layout(new Rectangle(0, 0, Context.FromPixels(r - l), Context.FromPixels(b - t))); _backgroundView.Layout(0, 0, r - l, b - t); } diff --git a/Xamarin.Forms.Platform.Android/AppCompat/TabbedPageRenderer.cs b/Xamarin.Forms.Platform.Android/AppCompat/TabbedPageRenderer.cs index d844500..cbfaf01 100644 --- a/Xamarin.Forms.Platform.Android/AppCompat/TabbedPageRenderer.cs +++ b/Xamarin.Forms.Platform.Android/AppCompat/TabbedPageRenderer.cs @@ -73,7 +73,7 @@ namespace Xamarin.Forms.Platform.Android.AppCompat } } - FragmentManager FragmentManager => _fragmentManager ?? (_fragmentManager = ((FormsAppCompatActivity)Context).SupportFragmentManager); + FragmentManager FragmentManager => _fragmentManager ?? (_fragmentManager = Context.GetFragmentManager()); bool IsBottomTabPlacement => (Element != null) ? Element.OnThisPlatform().GetToolbarPlacement() == ToolbarPlacement.Bottom : false; public Color BarItemColor @@ -231,7 +231,7 @@ namespace Xamarin.Forms.Platform.Android.AppCompat { base.OnElementChanged(e); - var activity = (FormsAppCompatActivity)Context; + var activity = Context.GetActivity(); if (e.OldElement != null) ((IPageController)e.OldElement).InternalChildren.CollectionChanged -= OnChildrenCollectionChanged; diff --git a/Xamarin.Forms.Platform.Android/ContextExtensions.cs b/Xamarin.Forms.Platform.Android/ContextExtensions.cs index 868b74c..b34f260 100644 --- a/Xamarin.Forms.Platform.Android/ContextExtensions.cs +++ b/Xamarin.Forms.Platform.Android/ContextExtensions.cs @@ -5,6 +5,7 @@ using Android.Util; using Android.Views.InputMethods; using AApplicationInfoFlags = Android.Content.PM.ApplicationInfoFlags; using AActivity = Android.App.Activity; +using AFragmentManager = Android.Support.V4.App.FragmentManager; namespace Xamarin.Forms.Platform.Android { @@ -96,5 +97,49 @@ namespace Xamarin.Forms.Platform.Android return null; } + + internal static Context GetThemedContext(this Context context) + { + if (context == null) + return null; + + if (context.IsDesignerContext()) + return context; + + if (context is global::Android.Support.V7.App.AppCompatActivity activity) + return activity.SupportActionBar.ThemedContext; + + if (context is ContextWrapper contextWrapper) + return contextWrapper.BaseContext.GetThemedContext(); + + return null; + } + + internal static bool IsDesignerContext(this Context context) + { + if (context == null) + return false; + + if ($"{context.ToString()}".Contains("com.android.layoutlib.bridge.android.BridgeContext")) + return true; + + if (context is ContextWrapper contextWrapper) + return contextWrapper.BaseContext.IsDesignerContext(); + + return false; + } + + public static AFragmentManager GetFragmentManager(this Context context) + { + if (context == null) + return null; + + var activity = context.GetActivity(); + + if (activity is global::Android.Support.V4.App.FragmentActivity fa) + return fa.SupportFragmentManager; + + return null; + } } } diff --git a/Xamarin.Forms.Platform.Android/Renderers/ContainerView.cs b/Xamarin.Forms.Platform.Android/Renderers/ContainerView.cs index 73b5492..3ef2c39 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/ContainerView.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/ContainerView.cs @@ -50,7 +50,7 @@ namespace Xamarin.Forms.Platform.Android if (disposing) { - _renderer.Dispose(); + _renderer?.Dispose(); _renderer = null; _view = null; _context = null; diff --git a/Xamarin.Forms.Platform.Android/Renderers/ShellFlyoutTemplatedContentRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/ShellFlyoutTemplatedContentRenderer.cs index 63581a5..5ab3e2a 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/ShellFlyoutTemplatedContentRenderer.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/ShellFlyoutTemplatedContentRenderer.cs @@ -38,13 +38,48 @@ namespace Xamarin.Forms.Platform.Android protected virtual void LoadView(IShellContext shellContext) { var context = shellContext.AndroidContext; - var coordinator = LayoutInflater.FromContext(context).Inflate(Resource.Layout.FlyoutContent, null); - var recycler = coordinator.FindViewById(Resource.Id.flyoutcontent_recycler); - var appBar = coordinator.FindViewById(Resource.Id.flyoutcontent_appbar); + var coordinator = (ViewGroup)LayoutInflater.FromContext(context).Inflate(Resource.Layout.FlyoutContent, null); + var recycler = coordinator.FindViewById(Resource.Id.flyoutcontent_recycler); + var appBar = coordinator.FindViewById(Resource.Id.flyoutcontent_appbar); - _rootView = coordinator; + _rootView = coordinator; - appBar.AddOnOffsetChangedListener(this); + if((recycler == null || appBar == null) && !context.IsDesignerContext()) + { + if (recycler == null) + throw new ArgumentNullException("flyoutcontent_recycler", "Unable to find layout for flyoutcontent_recycler"); + + // PREVIEWER HACK for some reason previewer pulls this out as a FrameLayout and ignores the internal resources + if (appBar == null) + throw new ArgumentNullException("flyoutcontent_appbar", "Unable to find layout for flyoutcontent_recycler"); + + } + + + try + { + // PREVIEWER HACK for some reason previewer can't find the resources for the recycler or the appBar + if (recycler == null) + recycler = (RecyclerView)coordinator.GetChildAt(1); + if (appBar == null) + appBar = (AppBarLayout)coordinator.GetChildAt(0); + } + catch + { + // PReviewer hack. + // appcompat and non appcompat initialize this whole thing differently so the above are for appcompat the below are for non + } + + // PREVIEWER HACK for some reason previewer can't find the resources for the recycler or the appBar + if (recycler == null) + recycler = coordinator.FindViewById(context.Resources.GetIdentifier("flyoutcontent_recycler", "id", context.PackageName)); + + // PREVIEWER HACK for some reason previewer pulls this out as a FrameLayout and ignores the internal resources + if (appBar == null) + appBar = coordinator.FindViewById(context.Resources.GetIdentifier("flyoutcontent_appbar", "id", context.PackageName)); + + + (appBar as AppBarLayout)?.AddOnOffsetChangedListener(this); int actionBarHeight = (int)context.ToPixels(56); @@ -68,11 +103,14 @@ namespace Xamarin.Forms.Platform.Android var metrics = context.Resources.DisplayMetrics; var width = Math.Min(metrics.WidthPixels, metrics.HeightPixels); - TypedValue tv = new TypedValue(); - if (context.Theme.ResolveAttribute(global::Android.Resource.Attribute.ActionBarSize, tv, true)) - { - actionBarHeight = TypedValue.ComplexToDimensionPixelSize(tv.Data, metrics); - } + using (TypedValue tv = new TypedValue()) + { + if (context.Theme.ResolveAttribute(global::Android.Resource.Attribute.ActionBarSize, tv, true)) + { + actionBarHeight = TypedValue.ComplexToDimensionPixelSize(tv.Data, metrics); + } + } + width -= actionBarHeight; coordinator.LayoutParameters = new LP(width, LP.MatchParent); diff --git a/Xamarin.Forms.Platform.Android/Renderers/ShellRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/ShellRenderer.cs index f6d8263..030ac25 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/ShellRenderer.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/ShellRenderer.cs @@ -36,7 +36,10 @@ namespace Xamarin.Forms.Platform.Android VisualElementTracker IVisualElementRenderer.Tracker => null; AView IVisualElementRenderer.View => _flyoutRenderer.AndroidView; - ViewGroup IVisualElementRenderer.ViewGroup => _flyoutRenderer.AndroidView as ViewGroup; + + // Used by Previewer + [EditorBrowsable(EditorBrowsableState.Never)] + public ViewGroup ViewGroup => _flyoutRenderer.AndroidView as ViewGroup; SizeRequest IVisualElementRenderer.GetDesiredSize(int widthConstraint, int heightConstraint) { @@ -59,7 +62,9 @@ namespace Xamarin.Forms.Platform.Android { } - void IVisualElementRenderer.UpdateLayout() + // Used by Previewer + [EditorBrowsable(EditorBrowsableState.Never)] + public void UpdateLayout() { var width = (int)AndroidContext.ToPixels(Element.Width); var height = (int)AndroidContext.ToPixels(Element.Height); @@ -151,7 +156,7 @@ namespace Xamarin.Forms.Platform.Android protected Context AndroidContext { get; } protected Shell Element { get; private set; } - private FragmentManager FragmentManager => ((FormsAppCompatActivity)AndroidContext).SupportFragmentManager; + FragmentManager FragmentManager => AndroidContext.GetFragmentManager(); protected virtual IShellObservableFragment CreateFragmentForPage(Page page) { @@ -202,9 +207,7 @@ namespace Xamarin.Forms.Platform.Android protected virtual void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == Shell.CurrentItemProperty.PropertyName) - { SwitchFragment(FragmentManager, _frameLayout, Element.CurrentItem); - } _elementPropertyChanged?.Invoke(sender, e); } @@ -223,13 +226,18 @@ namespace Xamarin.Forms.Platform.Android ((IShellController)shell).AddAppearanceObserver(this, shell); - SwitchFragment(FragmentManager, _frameLayout, shell.CurrentItem, false); + // Previewer Hack + if(AndroidContext.GetActivity() != null) + SwitchFragment(FragmentManager, _frameLayout, shell.CurrentItem, false); } IShellItemRenderer _currentRenderer; protected virtual void SwitchFragment(FragmentManager manager, AView targetView, ShellItem newItem, bool animate = true) { + if (AndroidContext.IsDesignerContext()) + return; + var previousRenderer = _currentRenderer; _currentRenderer = CreateShellItemRenderer(newItem); _currentRenderer.ShellItem = newItem; @@ -266,10 +274,10 @@ namespace Xamarin.Forms.Platform.Android void UpdateStatusBarColor(ShellAppearance appearance) { - var activity = ((FormsAppCompatActivity)AndroidContext); - var window = activity.Window; - var decorView = window.DecorView; - var resources = activity.Resources; + var activity = AndroidContext.GetActivity(); + var window = activity?.Window; + var decorView = window?.DecorView; + var resources = AndroidContext.Resources; int statusBarHeight = 0; int resourceId = resources.GetIdentifier("status_bar_height", "dimen", "android"); @@ -285,19 +293,23 @@ namespace Xamarin.Forms.Platform.Android navigationBarHeight = resources.GetDimensionPixelSize(resourceId); } - // we are using the split drawable here to avoid GPU overdraw. - // All it really is is a drawable that only draws under the statusbar/bottom bar to make sure - // we dont draw over areas we dont need to. This has very limited benefits considering its - // only saving us a flat color fill BUT it helps people not freak out about overdraw. - if (appearance != null) - { - var color = appearance.BackgroundColor.ToAndroid(Color.FromHex("#03A9F4")); - decorView.SetBackground(new SplitDrawable(color, statusBarHeight, navigationBarHeight)); - } - else + // TODO Previewer Hack + if (decorView != null) { - var color = Color.FromHex("#03A9F4").ToAndroid(); - decorView.SetBackground(new SplitDrawable(color, statusBarHeight, navigationBarHeight)); + // we are using the split drawable here to avoid GPU overdraw. + // All it really is is a drawable that only draws under the statusbar/bottom bar to make sure + // we dont draw over areas we dont need to. This has very limited benefits considering its + // only saving us a flat color fill BUT it helps people not freak out about overdraw. + if (appearance != null) + { + var color = appearance.BackgroundColor.ToAndroid(Color.FromHex("#03A9F4")); + decorView.SetBackground(new SplitDrawable(color, statusBarHeight, navigationBarHeight)); + } + else + { + var color = Color.FromHex("#03A9F4").ToAndroid(); + decorView.SetBackground(new SplitDrawable(color, statusBarHeight, navigationBarHeight)); + } } } diff --git a/Xamarin.Forms.Platform.Android/Renderers/ShellToolbarTracker.cs b/Xamarin.Forms.Platform.Android/Renderers/ShellToolbarTracker.cs index 42fe361..482eb01 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/ShellToolbarTracker.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/ShellToolbarTracker.cs @@ -236,23 +236,21 @@ namespace Xamarin.Forms.Platform.Android var backButtonHandler = Shell.GetBackButtonBehavior(page); toolbar.SetNavigationOnClickListener(this); - var activity = (FormsAppCompatActivity)context; - if (backButtonHandler != null) { - await UpdateDrawerArrowFromBackButtonBehavior(context, toolbar, drawerLayout, backButtonHandler, activity); + await UpdateDrawerArrowFromBackButtonBehavior(context, toolbar, drawerLayout, backButtonHandler); } else { - await UpdateDrawerArrow(context, toolbar, drawerLayout, activity); + await UpdateDrawerArrow(context, toolbar, drawerLayout); } } - protected virtual async Task UpdateDrawerArrow(Context context, Toolbar toolbar, DrawerLayout drawerLayout, FormsAppCompatActivity activity) + protected virtual async Task UpdateDrawerArrow(Context context, Toolbar toolbar, DrawerLayout drawerLayout) { - if (_drawerToggle == null) + if (_drawerToggle == null && !context.IsDesignerContext()) { - _drawerToggle = new ActionBarDrawerToggle((Activity)context, drawerLayout, toolbar, + _drawerToggle = new ActionBarDrawerToggle(context.GetActivity(), drawerLayout, toolbar, R.String.Ok, R.String.Ok) { ToolbarNavigationClickListener = this, @@ -267,7 +265,7 @@ namespace Xamarin.Forms.Platform.Android if (CanNavigateBack) { _drawerToggle.DrawerIndicatorEnabled = false; - using (var icon = new DrawerArrowDrawable(activity.SupportActionBar.ThemedContext)) + using (var icon = new DrawerArrowDrawable(context.GetThemedContext())) { icon.SetColorFilter(TintColor.ToAndroid(Color.White), PorterDuff.Mode.SrcAtop); icon.Progress = 1; @@ -288,7 +286,7 @@ namespace Xamarin.Forms.Platform.Android _drawerToggle.SyncState(); } - protected virtual async Task UpdateDrawerArrowFromBackButtonBehavior(Context context, Toolbar toolbar, DrawerLayout drawerLayout, BackButtonBehavior backButtonHandler, FormsAppCompatActivity activity) + protected virtual async Task UpdateDrawerArrowFromBackButtonBehavior(Context context, Toolbar toolbar, DrawerLayout drawerLayout, BackButtonBehavior backButtonHandler) { var behavior = backButtonHandler; var command = behavior.Command; @@ -307,7 +305,7 @@ namespace Xamarin.Forms.Platform.Android if (CanNavigateBack && icon == null) { - icon = new DrawerArrowDrawable(activity.SupportActionBar.ThemedContext); + icon = new DrawerArrowDrawable(context.GetThemedContext()); (icon as DrawerArrowDrawable).Progress = 1; } diff --git a/Xamarin.Forms.Platform.Android/Resources/Layout/FlyoutContent.axml b/Xamarin.Forms.Platform.Android/Resources/Layout/FlyoutContent.axml index 2a3b386..a6f4f5a 100644 --- a/Xamarin.Forms.Platform.Android/Resources/Layout/FlyoutContent.axml +++ b/Xamarin.Forms.Platform.Android/Resources/Layout/FlyoutContent.axml @@ -9,13 +9,13 @@ > diff --git a/Xamarin.Forms.Sandbox/ShellPage.xaml b/Xamarin.Forms.Sandbox/ShellPage.xaml new file mode 100644 index 0000000..e0cb676 --- /dev/null +++ b/Xamarin.Forms.Sandbox/ShellPage.xaml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/Xamarin.Forms.Sandbox/ShellPage.xaml.cs b/Xamarin.Forms.Sandbox/ShellPage.xaml.cs new file mode 100644 index 0000000..6979ea5 --- /dev/null +++ b/Xamarin.Forms.Sandbox/ShellPage.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.Sandbox +{ + [XamlCompilation(XamlCompilationOptions.Compile)] + public partial class ShellPage : Shell + { + public ShellPage() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Sandbox/Xamarin.Forms.Sandbox.csproj b/Xamarin.Forms.Sandbox/Xamarin.Forms.Sandbox.csproj index dca9fb7..cbae474 100644 --- a/Xamarin.Forms.Sandbox/Xamarin.Forms.Sandbox.csproj +++ b/Xamarin.Forms.Sandbox/Xamarin.Forms.Sandbox.csproj @@ -13,6 +13,12 @@ pdbonly true + + + + MSBuild:Compile + + -- 2.7.4