Edited at

Xamarin.FormsでのMasterDetailとバックボタン

More than 1 year has passed since last update.

de:code2017アプリとかで気になってた事があって

MasterDetail組む時にホーム画面への配慮した方が良いかも。っていう問題。

iOSだと意識しなくて良いのだけどAndroidだとバックボタンがあるのでホーム画面に配慮する必要があると思っています。

例えばGoogle MapであったりGmailアプリであったりで、ハンバーガーメニューで他の画面へ移動してからバックスペースを押すと最初の画面へ戻ると思います。

こうなっていないアプリって結構めんどくさくて

例えば設定画面を開いて設定を変更して元の画面に戻る、みたいなケースを想定した時に

ホーム画面を考慮していないアプリの場合

ホーム画面 -> メニューを開く -> 設定画面へ移動 -> 設定操作 -> メニューを開く -> ホームへ戻る

というオペレーションになるのだけど

ホーム画面 -> メニューを開く -> 設定画面へ移動 -> 設定操作 -> バックボタン

っていう操作をしてアプリが閉じちゃう、みたいな事になったりならなかったり。

Xamarin.FormsでCustomRendererを使わずにMasterDetailをホーム画面を意識した感じにはいくつか方法はあるけど、MasterDetailっぽい動作を維持しつつ(開いてるメニューが閉じてDetailが変わる感じ)実現しようとすると、DetailにNavigationPageを二つ重ねる感じで実現できます。

VS2017のXamarinテンプレートのMasterDetailを今回の説明方法で修正したイメージはこんな感じです。

https://sleepyandhungry1984.tumblr.com/post/162319590522/xamarinforms-masterdetail

MasterDetail

Master -- ContentPage
Detai -- NavigationPage(outer)
       ----NavigationPage(inner)
           ----ContentPage(Home)

みたいな感じで構成します。

そして重要な点はinnerナビゲーションの下に追加するページはNavigationPage.SetHasNavigationBarでナビゲーションバーを表示しないようにしておきます。

これでメニューから例えばAboutページを選択した場合にはInnerにPushAsyncして

NavigationPage(outer)

-------NavigationPage(inner)
-------------ContentPage(Home)
-------------ContentPage(About)

という感じにします。

こうするとハンバーガーメニューは残ったままDetailが切り替わります。

でAndroidのバックボタンを押すとAboutがpopしてHomeに戻ると。

Aboutが開いている状態でメニューからHomeを選択した場合にはPopToRootしてあげれば良くて、Aboutが開いている状態で別のメニューを選んだ場合には InnerにPushAsyncした後にAboutをRemovePageする感じでOK。

で、ページ遷移させようとした場合には

NavigationPage(outer)

-------NavigationPage(inner)
-------------ContentPage(Home)
-------ContentPage(Newitem)

という感じで、outerにPushAsyncすることでNavigationBarのハンバーガメニューが ←矢印に変わってナビゲーションする感じです。

メンドクサイし実装が複雑化するしMVVMフレームワークとかだとできなかったりする場合もあるし、Xamarin.Formsでこの辺にコストをかけるのか?っていう疑問もあるんですけど、でも出来てると良いかも?

そんなお話でした。

コードをちょっと張り付けておきます。

#region


using System;
using System.Threading;
using MasterDetail.Views;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

#endregion

[assembly: XamlCompilation(XamlCompilationOptions.Compile)]

namespace MasterDetail
{
// XamlになれるとC#コードでも普通に画面書けることに気が付いた。
// Intellisenseとコード補完最大限に効くので結構書きやすい。
// 物凄く読みにくいけどね🍣

public partial class App : Application
{
private readonly NavigationPage _innerNavPage;
private readonly MasterDetailPage _masterDetailPage;
private readonly NavigationPage _outerNavPage;

public App()
{
InitializeComponent();

// browse page
var browsePage = new ItemsPage
{
Title = "Browse"
};
NavigationPage.SetHasNavigationBar(browsePage, false);

// aboutPage
var aboutPage = new AboutPage
{
Title = "About"
};
NavigationPage.SetHasNavigationBar(aboutPage, false);

// InnerNavigationPage
_innerNavPage = new NavigationPage(browsePage)
{
Title = browsePage.Title
};
_innerNavPage.PropertyChanged += (sender, args) =>
{
if (args.PropertyName == NavigationPage.CurrentPageProperty.PropertyName)
_innerNavPage.Title = _innerNavPage.CurrentPage.Title;
};

// OuterNavigationPage
_outerNavPage = new NavigationPage(_innerNavPage);

// MenuPage
Page menuPage = null;
menuPage = new ContentPage
{
Title = "Menu",
Content = new StackLayout
{
Children =
{
new ContentView
{
Padding = 20,
Content = new Label {Text = "Browse"},
GestureRecognizers =
{
new TapGestureRecognizer
{
Command = new Command(() => { MessagingCenter.Send(menuPage, "Menu", "Browse"); })
}
}
},
new ContentView
{
Padding = 20,
Content = new Label {Text = "About"},
GestureRecognizers =
{
new TapGestureRecognizer
{
Command = new Command(() => { MessagingCenter.Send(menuPage, "Menu", "About"); })
}
}
}
}
}
};

_masterDetailPage = new MasterDetailPage();
_masterDetailPage.Master = menuPage;
_masterDetailPage.Detail = _outerNavPage;

MainPage = _masterDetailPage;

MessagingCenter.Subscribe(this, "Navigation",
new Action<ItemsPage, Page>((s, p) => { _outerNavPage.PushAsync(p); }));

MessagingCenter.Subscribe(this, "Menu", new Action<Page, string>((p, s) =>
{
if (s == "Browse")
{
if (!(_innerNavPage.CurrentPage is ItemsPage))
_innerNavPage.PopToRootAsync();
}
else if (s == "About")
{
if (_innerNavPage.CurrentPage is AboutPage)
return;

if (_innerNavPage.CurrentPage is ItemsPage)
{
_innerNavPage.PushAsync(aboutPage);
}
else
{
var currentPage = _innerNavPage.CurrentPage;
_innerNavPage.PushAsync(aboutPage)
.ContinueWith(task =>
{
SynchronizationContext.Current.Post(state => { _innerNavPage.Navigation.RemovePage(currentPage); }, null);
});
}
}
_masterDetailPage.IsPresented = false;
}));
}
}
}