歡迎加入我的 LINE ID: easter1021
在網路上參考幾份文件,最終選擇使用 Sergey Metlov 的範例,以下內容是我實作時遇到的問題以及詳解。
此篇文章執行的作業系統為
Windows 7
學習時間 10 分鐘
圖片來源: https://asyncawait.wordpress.com/2016/06/16/bottom-menu-for-xamarin-forms-android/
- 建立專案
- 安裝 NuGet
- 建構程式碼
- 執行 F5
- 修正 _bottomBar 高度問題
- 參考
檔案(F)
->新增(N)
->專案(P)
-> Cross-Platform -> Xamarin.Forms Portable- 輸入專案名稱
DemoBottomMenu
-> 確定 (base on .NET Framework 4.5.2) - 在方案總管根目錄
建立方案(B)
完成專案初次建置
- 在方案總管的
DemoBottomMenu.Droid
點擊右鍵選單,選擇管理 NuGet 套件
- 選擇
瀏覽
標籤 -> 搜尋BottomNavigationBar
作者 pocheshire -> 選擇1.1.1
版本 -> 安裝
在 Page
類別裡有2個 protected 方法,分別為 OnAppearing
和 OnDisappearing
,他們主要功能是當前Page物件在切換時被呼叫的方法。但因為我們覆寫了 MainPage
(Android APP) 的 Renderer
類別,此舉使得這2個方法不會被執行,因此我們需要使用 `BaseContentPage' 主動呼叫這2個方法。
- 在方案總管的
DemoBottomMenu
點擊右鍵選單,選擇加入(D)
->新增項目(W)
- 選擇 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();
}
}
}
- 在方案總管的
DemoBottomMenu
點擊右鍵選單,選擇加入(D)
->新增項目(W)
- 選擇 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();
}
}
}
- 在方案總管的
DemoBottomMenu
點擊右鍵選單,選擇加入(D)
->新增項目(W)
- 選擇 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
的 MainPage
修改為:
// MainPage = new DemoBottomMenu.MainPage();
MainPage = new NavigationPage(new MainPage());
修改 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;
}
}
}
此 Renderer
將覆寫 MainPage
在 Android APP 的輸出樣式。
- 在方案總管的
DemoBottomMenu.Droid
點擊右鍵選單,選擇加入(D)
->新增資料夾(D)
-> 重新命名為Renderers
- 在剛才加入的資料夾
Renderers
點擊右鍵選單,選擇加入(D)
->新增項目(W)
- 選擇 程式碼 -> 程式碼檔 -> 輸入名稱
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;
}
}
}
}
}
- 在方案總管的
DemoBottomMenu.Droid
點擊右鍵選單,選擇設定為啟始專案(A)
F5
執行
在 Sergey Metlov 的範例中
App.xaml.cs
的MainPage
是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
- Bottom menu for Xamarin Forms (Android) Source Code
- Android BottomBar v.2.0: A custom view component that mimics the new Material Design Bottom Navigation pattern.
- BottomNavigationBar: Bottom Navigation Bar for Xamarin