de:code2017アプリとかで気になってた事があって
MasterDetail組む時にホーム画面への配慮した方が良いかも。っていう問題。
iOSだと意識しなくて良いのだけどAndroidだとバックボタンがあるのでホーム画面に配慮する必要があると思っています。
例えばGoogle MapであったりGmailアプリであったりで、ハンバーガーメニューで他の画面へ移動してからバックスペースを押すと最初の画面へ戻ると思います。
こうなっていないアプリって結構めんどくさくて
例えば設定画面を開いて設定を変更して元の画面に戻る、みたいなケースを想定した時に
ホーム画面を考慮していないアプリの場合
ホーム画面 -> メニューを開く -> 設定画面へ移動 -> 設定操作 -> メニューを開く -> ホームへ戻る
というオペレーションになるのだけど
ホーム画面 -> メニューを開く -> 設定画面へ移動 -> 設定操作 -> バックボタン
っていう操作をしてアプリが閉じちゃう、みたいな事になったりならなかったり。
Xamarin.FormsでCustomRendererを使わずにMasterDetailをホーム画面を意識した感じにはいくつか方法はあるけど、MasterDetailっぽい動作を維持しつつ(開いてるメニューが閉じてDetailが変わる感じ)実現しようとすると、DetailにNavigationPageを二つ重ねる感じで実現できます。
VS2017のXamarinテンプレートの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;
}));
}
}
}