Skip to content

Instantly share code, notes, and snippets.

@rc1021
Last active January 10, 2018 05:29
Show Gist options
  • Save rc1021/12064c95c7a744a8cff1c1c47d1cee99 to your computer and use it in GitHub Desktop.
Save rc1021/12064c95c7a744a8cff1c1c47d1cee99 to your computer and use it in GitHub Desktop.
Xamarin Android TabbedPage Bar 置底作法

歡迎加入我的 LINE ID: easter1021

Xamarin Android TabbedPage Bar 置底

在網路上參考幾份文件,最終選擇使用 Sergey Metlov 的範例,以下內容是我實作時遇到的問題以及詳解。

此篇文章執行的作業系統為 Windows 7 學習時間 10 分鐘

Screenshot

Alt iOSAlt Android

圖片來源: https://asyncawait.wordpress.com/2016/06/16/bottom-menu-for-xamarin-forms-android/

索引

  1. 建立專案
  2. 安裝 NuGet
  3. 建構程式碼
  4. 執行 F5
  5. 修正 _bottomBar 高度問題
  6. 參考

建立專案

  1. 檔案(F) -> 新增(N) -> 專案(P) -> Cross-Platform -> Xamarin.Forms Portable
  2. 輸入專案名稱 DemoBottomMenu -> 確定 (base on .NET Framework 4.5.2)
  3. 在方案總管根目錄 建立方案(B) 完成專案初次建置

安裝 NuGet

  1. 在方案總管的 DemoBottomMenu.Droid 點擊右鍵選單,選擇 管理 NuGet 套件
  2. 選擇 瀏覽 標籤 -> 搜尋 BottomNavigationBar 作者 pocheshire -> 選擇 1.1.1 版本 -> 安裝

建構程式碼

建立 BaseContentPage.cs

Page 類別裡有2個 protected 方法,分別為 OnAppearingOnDisappearing,他們主要功能是當前Page物件在切換時被呼叫的方法。但因為我們覆寫了 MainPage(Android APP) 的 Renderer 類別,此舉使得這2個方法不會被執行,因此我們需要使用 `BaseContentPage' 主動呼叫這2個方法。

  1. 在方案總管的 DemoBottomMenu 點擊右鍵選單,選擇 加入(D) -> 新增項目(W)
  2. 選擇 Cross-Platform -> Forms Page -> 輸入名稱 BaseContentPage.cs

修改 BaseContentPage.cs 代碼內容如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;
using System.Text;
using Xamarin.Forms;

namespace DemoBottomMenu
{
    public class BaseContentPage : ContentPage
    {
        public void SendAppearing()
        {
            OnAppearing();
        }

        public void SendDisappearing()
        {
            OnDisappearing();
        }
    }
}

建立 ContactPage.xaml

  1. 在方案總管的 DemoBottomMenu 點擊右鍵選單,選擇 加入(D) -> 新增項目(W)
  2. 選擇 Cross-Platform -> Forms Xaml Page -> 輸入名稱 ContactPage.xaml

修改 ContactPage.xaml 代碼內容如下:

<?xml version="1.0" encoding="utf-8" ?>
<local:BaseContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DemoBottomMenu"
             x:Class="DemoBottomMenu.ContactPage"
             Title="聯絡我們">
  <Label Text="contact..." VerticalOptions="Center" HorizontalOptions="Center" />
</local:BaseContentPage>

修改 ContactPage.xaml.cs 代碼內容如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Xamarin.Forms;

namespace DemoBottomMenu
{
    public partial class ContactPage : BaseContentPage
    {
        public ContactPage()
        {
            InitializeComponent();
        }
    }
}

建立 AboutPage.xaml

  1. 在方案總管的 DemoBottomMenu 點擊右鍵選單,選擇 加入(D) -> 新增項目(W)
  2. 選擇 Cross-Platform -> Forms Xaml Page -> 輸入名稱 AboutPage.xaml

修改 AboutPage.xaml 代碼內容如下:

<?xml version="1.0" encoding="utf-8" ?>
<local:BaseContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DemoBottomMenu"
             x:Class="DemoBottomMenu.AboutPage"
             Title="關於我">
  <Label Text="about" VerticalOptions="Center" HorizontalOptions="Center" />
</local:BaseContentPage>

修改 AboutPage.xaml.cs 代碼內容如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Xamarin.Forms;

namespace DemoBottomMenu
{
    public partial class AboutPage : BaseContentPage
    {
        public AboutPage()
        {
            InitializeComponent();
        }
    }
}

修正 App.xaml.cs

App.xaml.csMainPage 修改為:

// MainPage = new DemoBottomMenu.MainPage();
MainPage = new NavigationPage(new MainPage());

修正 MainPage.xaml 和 MainPage.xaml.cs

修改 MainPage.xaml 代碼內容如下:

<?xml version="1.0" encoding="utf-8" ?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DemoBottomMenu"
             x:Class="DemoBottomMenu.MainPage">
    <local:AboutPage />
    <local:ContactPage />
</TabbedPage>

MainPage.xaml.cs 原本繼承 ContenPage 改為 TabbedPage:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace DemoBottomMenu
{
    public partial class MainPage : TabbedPage
    {
        public MainPage()
        {
            InitializeComponent();
        }
 
        protected override void OnCurrentPageChanged()
        {
            base.OnCurrentPageChanged();
 
            Title = CurrentPage?.Title;
        }
    }
}

在 DemoBottomMenu.Droid 建立 MainPageRenderer.cs

Renderer 將覆寫 MainPage 在 Android APP 的輸出樣式。

  1. 在方案總管的 DemoBottomMenu.Droid 點擊右鍵選單,選擇 加入(D) -> 新增資料夾(D) -> 重新命名為 Renderers
  2. 在剛才加入的資料夾 Renderers 點擊右鍵選單,選擇 加入(D) -> 新增項目(W)
  3. 選擇 程式碼 -> 程式碼檔 -> 輸入名稱 MainPageRenderer.cs

修改 MainPageRenderer.cs 代碼內容如下:

using System.Collections.Generic;
using System.Linq;
using Android.Views;
using BottomNavigationBar;
using BottomNavigationBar.Listeners;
using DemoBottomMenu.Droid.Renderers;
using DemoBottomMenu;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Color = Android.Graphics.Color;

[assembly: ExportRenderer(typeof(MainPage), typeof(MainPageRenderer))]

namespace DemoBottomMenu.Droid.Renderers
{
    internal class MainPageRenderer : VisualElementRenderer<MainPage>, IOnTabClickListener
    {
        private BottomBar _bottomBar;

        private Page _currentPage;

        private int _lastSelectedTabIndex = -1;

        public MainPageRenderer()
        {
            // 不主動加入子頁面
            AutoPackage = false;
        }

        public void OnTabSelected(int position)
        {
            LoadPageContent(position);
        }

        public void OnTabReSelected(int position)
        {
        }

        protected override void OnElementChanged(ElementChangedEventArgs<MainPage> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement != null)
            {
                ClearElement(e.OldElement);
            }

            if (e.NewElement != null)
            {
                InitializeElement(e.NewElement);
            }
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                ClearElement(Element);
            }

            base.Dispose(disposing);
        }

        protected override void OnLayout(bool changed, int l, int t, int r, int b)
        {
            if (Element == null)
            {
                return;
            }

            int width = r - l;
            int height = b - t;

            _bottomBar.Measure(
                MeasureSpec.MakeMeasureSpec(width, MeasureSpecMode.Exactly),
                MeasureSpec.MakeMeasureSpec(height, MeasureSpecMode.AtMost));

            // 為了正確佈局底部的tabbed,我們需要重新測量尺寸
            _bottomBar.Measure(
                MeasureSpec.MakeMeasureSpec(width, MeasureSpecMode.Exactly),
                MeasureSpec.MakeMeasureSpec(_bottomBar.ItemContainer.MeasuredHeight, MeasureSpecMode.Exactly));

            int barHeight = _bottomBar.ItemContainer.MeasuredHeight;

            _bottomBar.Layout(0, b - barHeight, width, b);

            float density = Android.Content.Res.Resources.System.DisplayMetrics.Density;

            double contentWidthConstraint = width / density;
            double contentHeightConstraint = (height - barHeight) / density;

            if (_currentPage != null)
            {
                var renderer = Platform.GetRenderer(_currentPage);

                renderer.Element.Measure(contentWidthConstraint, contentHeightConstraint);
                renderer.Element.Layout(new Rectangle(0, 0, contentWidthConstraint, contentHeightConstraint));

                renderer.UpdateLayout();
            }
        }

        private void InitializeElement(MainPage element)
        {
            PopulateChildren(element);
        }

        private void PopulateChildren(MainPage element)
        {
            // 作者說明 bottomBar 無法直接使用,因此要刪除重建
            _bottomBar?.RemoveFromParent();

            _bottomBar = CreateBottomBar(element.Children);
            AddView(_bottomBar);

            LoadPageContent(0);
        }

        private void ClearElement(MainPage element)
        {
            if (_currentPage != null)
            {
                IVisualElementRenderer renderer = Platform.GetRenderer(_currentPage);

                if (renderer != null)
                {
                    renderer.ViewGroup.RemoveFromParent();
                    renderer.ViewGroup.Dispose();
                    renderer.Dispose();

                    _currentPage = null;
                }

                if (_bottomBar != null)
                {
                    _bottomBar.RemoveFromParent();
                    _bottomBar.Dispose();
                    _bottomBar = null;
                }
            }
        }

        private BottomBar CreateBottomBar(IEnumerable<Page> pageIntents)
        {
            var bar = new BottomBar(Context);

            // TODO: Configure the bottom bar here according to your needs
            // 程式在建立底部選單時,你可以此這裡寫入你需要的內容

            bar.SetOnTabClickListener(this);
            bar.UseFixedMode();

            PopulateBottomBarItems(bar, pageIntents);

            bar.ItemContainer.SetBackgroundColor(Color.LightGray);

            return bar;
        }

        private void PopulateBottomBarItems(BottomBar bar, IEnumerable<Page> pages)
        {
            var barItems = pages.Select(x => new BottomBarTab(Context.Resources.GetDrawable(x.Icon), x.Title));

            bar.SetItems(barItems.ToArray());
        }

        private void LoadPageContent(int position)
        {
            ShowPage(position);
        }

        private void ShowPage(int position)
        {
            if (position != _lastSelectedTabIndex)
            {
                Element.CurrentPage = Element.Children[position];

                if (Element.CurrentPage != null)
                {
                    LoadPageContent(Element.CurrentPage);
                }
            }

            _lastSelectedTabIndex = position;
        }

        private void LoadPageContent(Page page)
        {
            UnloadCurrentPage();

            _currentPage = page;

            LoadCurrentPage();

            Element.CurrentPage = _currentPage;
        }

        private void LoadCurrentPage()
        {
            var renderer = Platform.GetRenderer(_currentPage);

            if (renderer == null)
            {
                renderer = Platform.CreateRenderer(_currentPage);
                Platform.SetRenderer(_currentPage, renderer);

                AddView(renderer.ViewGroup);
            }
            else
            {
                // 手動呼叫 OnAppearing()
                var basePage = _currentPage as BaseContentPage;
                basePage?.SendAppearing();
            }

            renderer.ViewGroup.Visibility = ViewStates.Visible;
        }

        private void UnloadCurrentPage()
        {
            if (_currentPage != null)
            {
                // 手動呼叫 OnDisappearing()
                var basePage = _currentPage as BaseContentPage;
                basePage?.SendDisappearing();

                var renderer = Platform.GetRenderer(_currentPage);

                if (renderer != null)
                {
                    renderer.ViewGroup.Visibility = ViewStates.Invisible;
                }
            }
        }
    }
}

執行 F5

  1. 在方案總管的 DemoBottomMenu.Droid 點擊右鍵選單,選擇 設定為啟始專案(A)
  2. F5執行

修正 _bottomBar 高度問題

Sergey Metlov 的範例中 App.xaml.csMainPage 是1個 NavigationPage 物件,但如果將 MainPage 換成 MainPage 物件,則顯示的 _bottomBar 位置就會錯誤

App.xaml.cs

// 原始範例改為以下內容,則 _bottomBar 位置就會錯誤 
// MainPage = new NavigationPage( new MainPage() );
MainPage = new MainPage();

因此我在實作此範例時做了一些小修正避免這問題。

MainPageRenderer.cs

b - barHeight 改為 height - barHeight

// _bottomBar.Layout(0, b - barHeight, width, b);
_bottomBar.Layout(0, height - barHeight, width, b);

這樣就沒問題了,但原因是什麼我還不太確定,如果改錯說錯也請多給我一些指教。 LINE ID: easter1021

參考

  1. Bottom menu for Xamarin Forms (Android) Source Code
  2. Android BottomBar v.2.0: A custom view component that mimics the new Material Design Bottom Navigation pattern.
  3. BottomNavigationBar: Bottom Navigation Bar for Xamarin
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment