Xamarin.Forms 如何在推送时隐藏标签栏?

How to hide Tab Bar on push in Xamarin.Forms?

过去几天,我在 iOS 的 Xamarin.Forms 中与 TabbedPage 打交道。我找到了一些类似的解决方案: https://forums.xamarin.com/discussion/20901/hide-tab-bar-on-push

但是,其中 none 个效果很好。我还尝试子类化 TabbedRenderer 并将 TabBar 高度设置为 0。它有效,但如果我在 NavigationPage.Pushed 事件处理程序中隐藏 TabBar,则会有一些延迟,例如 TableView 在 space 上有空白底部。

如果我尝试在 PushViewController/PopViewController 方法中覆盖 NavigationRenderer 和 hide/show 选项卡栏,它有时会失败。例如,如果我快速来回导航,方法 PopViewController 不会被调用,NavigationStack 被破坏并且标签栏不会被恢复。

我认为唯一好的解决方案是让这个 属性 工作:UIViewController.HidesBottomBarWhenPushed。但是,我不知道该怎么做,因为 setting/overriding 它在渲染器中不起作用。

有没有人设法成功显示和隐藏 TabBar?

我尝试过的:

  • 创建 ContentPage 的子类并在其中创建 BindableProperty(如 HidesBottomBarWhenPushed)。我在 PageRenderer 中设置了 ViewController.hidesBottomBarWhenPushed 但它不起作用,尽管我可以获得此 属性 的值。

  • 在 PageRenderer 的初始构造函数中设置 this.hidesBottomBarWhenPushed ,仍然没有运气。

我想一定是hidesBottomBarWhenPushed有问题,我们不能通过这种方式隐藏tabbar。作为临时且简单的解决方法,我更改了 PageRenderer

TabBarController.TabBar 的 Visible
class PageiOS : PageRenderer
{
    public override void ViewWillAppear(bool animated)
    {
        base.ViewWillAppear(animated);
        if (this.NavigationController != null && this.TabBarController != null)
        {
            bool isRootVC = this.NavigationController.ViewControllers.Length == 1;
            ParentViewController.TabBarController.TabBar.Hidden = !isRootVC;
        }
    }
}

它的表现就像你上面说的,有一些延迟和底部空白 space。我禁用了 push 和 pop 的动画,问题就消失了。

测试:

我设法实施了一个解决方案,解决了隐藏 TabBar 后空白 space 的问题。您可以在 this article 中阅读有关它的更多详细信息。

为了解决这个问题,我们只需要布局所有 ChildViewControllers。这是我的自定义 TabbedPage 及其 TabbedPageRenderer 的示例实现。

HideableTabbedPage.cs:

using System;
using Xamarin.Forms;

namespace HideTabBar.Controls
{
    public class HideableTabbedPage : TabbedPage
    {
        public static readonly BindableProperty IsHiddenProperty =
            BindableProperty.Create(nameof(IsHidden), typeof(bool), typeof(HideableTabbedPage), false);

        public bool IsHidden
        {
            get { return (bool)GetValue(IsHiddenProperty); }
            set { SetValue(IsHiddenProperty, value); }
        }
    }
}

HideableTabbedPageRenderer.cs:

using System;
using System.ComponentModel;
using System.Threading.Tasks;
using HideTabBar.Controls;
using HideTabBar.iOS.CustomRenderer;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer(typeof(HideableTabbedPage), typeof(HideableTabbedPageRenderer))]
namespace HideTabBar.iOS.CustomRenderer
{
    public class HideableTabbedPageRenderer : TabbedRenderer
    {
        private bool disposed;
        private const int TabBarHeight = 49;

        protected override void OnElementChanged(VisualElementChangedEventArgs e)
        {
            base.OnElementChanged(e);

            if (e.OldElement == null)
            {
                this.Tabbed.PropertyChanged += Tabbed_PropertyChanged;
            }
        }

        private void Tabbed_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == HideableTabbedPage.IsHiddenProperty.PropertyName)
            {
                this.OnTabBarHidden((this.Element as HideableTabbedPage).IsHidden);
            }
        }

        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
            this.disposed = true;
        }

        private async void OnTabBarHidden(bool isHidden)
        {
            if (this.disposed || this.Element == null || this.TabBar == null)
            {
                return;
            }

            await this.SetTabBarVisibility(isHidden);
        }

        private async Task SetTabBarVisibility(bool hide)
        {
            this.TabBar.Opaque = false;
            if (hide)
            {
                this.TabBar.Alpha = 0;
            }

            this.UpdateFrame(hide);

            // Show / Hide TabBar
            this.TabBar.Hidden = hide;
            this.RestoreFonts();

            // Animate appearing 
            if (!hide)
            {
                await UIView.AnimateAsync(0.2f, () => this.TabBar.Alpha = 1);
            }
            this.TabBar.Opaque = true;

            this.ResizeViewControllers();
            this.RestoreFonts();
        }

        private void UpdateFrame(bool isHidden)
        {
            var tabFrame = this.TabBar.Frame;
            tabFrame.Height = isHidden ? 0 : TabBarHeight;
            this.TabBar.Frame = tabFrame;
        }

        private void RestoreFonts()
        {
            // Workaround to restore custom fonts:

            foreach (var item in this.TabBar.Items)
            {
                var text = item.Title;
                item.Title = "";
                item.Title = text;
            }
        }

        private void ResizeViewControllers()
        {
            foreach (var child in this.ChildViewControllers)
            {
                child.View.SetNeedsLayout();
                child.View.SetNeedsDisplay();
            }
        }
    }
}  

最终结果:

当我需要在屏幕上绘制标签栏之前隐藏标签栏时,我遇到了一个问题。

Wojciech Kulik 的解决方案帮助了我,但它在导航到标签页时开始闪烁。

下面的代码解决了我的问题。希望对您有所帮助。 把它放在你的 TabbedRenderer derived class

public override void ViewWillLayoutSubviews()
{
    OnTabBarHidden(true); // Hide before the page appear
}

有一个解决方案不需要任何渲染并且适用于 Android 和 iOS。

TabbedPage 包裹在 NavigationPage 中,这样您的应用程序结构就变成了

  • 导航页面(根)
    • 点击页面
      • 导航页面
        • ContentPage(带标签栏)
    • ContentPage(没有标签栏)

在 TabbedPage 上,您必须隐藏 'root' NavigationPage 的导航栏,否则您有 2 个导航栏。

<TabbedPage
    ...
    HasNavigationBar="False"> 

如果使用 'root' NavigationPage 推送页面,则标签栏被隐藏并且底部没有空白 space。

查看我的示例: https://github.com/Jfcobuss/HideTabbarExample/tree/master/HideTabbarExample

我确实使用 HidesBottomBarWhenPushed 属性 以更“原生的方式”获得了预期的结果。如果您查看 iOS 的导航是如何实现的,您会注意到,在将新的视图控制器推入 CreateViewControllerForPage 方法内的导航堆栈 it gets wrapped in a parent view controller 之前。它让我有机会拦截尚未推送到导航堆栈的已创建视图控制器并设置 HidesBottomBarWhenPushed 属性。为此,我创建了一个空的 class 派生自 ContentPage:

public class NoTabBarPage : ContentPage { }

所以现在从 NoTabBarPage 派生的每个页面都不会有底部标签栏。

还有一个让一切正常工作的渲染器:

[assembly: ExportRenderer(typeof(NoTabBarPage), typeof(NoTabBarPageRenderer))]
namespace HideTabBar.iOS.Renderers
{
    public class NoTabBarPageRenderer : PageRenderer
    {
        public override void DidMoveToParentViewController(UIViewController parent)
        {
            base.DidMoveToParentViewController(parent);

            parent.HidesBottomBarWhenPushed = true;
        }

        public override void ViewDidLayoutSubviews()
        {
            base.ViewDidLayoutSubviews();

            if (View != null && View.Superview != null && View.Frame.Height != View.Superview.Frame.Height)
            {
                View.Frame = View.Superview.Frame;
                Element.Layout(View.Frame.ToRectangle());
            }
        }
    }
}

像往常一样,Xamarin.Forms 不能完美无缺,结果是,子视图控制器没有延伸到父视图控制器的框架。而且我必须在 ViewDidLayoutSubviews.

中手动修复

另外请注意,在带有隐藏标签栏的视图控制器也没有标签栏之后,每个新的视图控制器都会被推送到导航堆栈。所以在我的例子中,每个“详细信息”页面都来自 NoTabBarPage.