個人的なメモ

Tomohiro Suzuki @hiro128_777 のブログです。Microsoft MVP for Developer Technologies 2017- 本ブログと所属組織の公式見解は関係ございません。

DualScreen 対応の Xamarin.Forms アプリを作ってみる (2)

はじめに

こんにちは、@hiro128_777です。

とうとう iPhone 12 が発表されましたね。でも、Surface Duo も忘れないでください。Surface Duo 日本での発売はまだですが、すでに、エミュレーターで疑似体験することは可能です。

というわけで、10月27日(火)に、エミュレーターで Surface Duo 対応アプリを体験するオンラインハンズオンを開催いたします!

こちらは、簡単な Surface Duo のエミュレーターで動作するマルチスクリーン対応アプリを作って体験していただけるコンテンツになっていますので、ご興味がある方はぜひご参加下さい!

csharp-tokyo.connpass.com


なお、この記事は(2)ですので、最初から手順をお試しになる場合は下記の(1)からお願いします。
hiro128.hatenablog.jp



今回やること

TwoPaneViewを作成し、Surface Duo エミュレータ―上で、Master - Detail 構成のページを Dual Screen 表示できるようにします。

TwoPaneViewGridを継承しており、左右または上下に 2 つのビューを配置できるコンテナーです。

TwoPaneViewの詳細については下記公式ドキュメントをご参照ください。

docs.microsoft.com



Dual Screen に対応するには、以下の対応を行うだけであり、非常に簡単です。

  • Single Screen 用と、Dual Screen 用の "ガワ" の ContentPage を別々に用意し、それぞれのコンテンツの配置情報のみ記述する。

  • 具体的なコンテンツ(配置する UI)の XAML を用意する。

  • TwoPaneViewが 画面の表示領域のサイズによって、Single Screen 用または、Dual Screen 用 の画面を表示できるように、MinWideModeWidthMinTallModeHeightを設定します。


TwoPaneViewの Single Screen、Dual Screen の制御の詳細については下記公式ドキュメントをご参照ください。

docs.microsoft.com


これから作成するサンプルアプリの Single Screen、Dual Screen のときの構成を図に表すと以下のようになります。

Single Screen 時の構成

f:id:hiro128:20201018171513p:plain
 

Dual Screen 時の構成

f:id:hiro128:20201018171631p:plain
 

では、次にコードを書いていきましょう。 

TwoPaneView用のコンテンツページとコンテンツビューのファイル追加する

 

  1. View フォルダを追加します。
  2. 以下の4つのコンテンツページを追加します。
    1. MasterDetailPage.xaml コンテンツページ
    2. DetailsPage.xaml コンテンツページ
    3. Master.xaml コンテンツビュー
    4. Details.xaml コンテンツビュー


以下のようになればOKです。
f:id:hiro128:20201018162214p:plain


デフォルトの MainPage に設定されているMainPage.xamlを削除します。

f:id:hiro128:20201018165908p:plain
 

MasterDetailPage を MainPage として設定します。
 

App.xaml.cs
        public App()
        {
            InitializeComponent();
            MainPage = new NavigationPage(new MasterDetailPage());
        }

 

MasterDetailPage.xaml のマークアップとコードを記述する

 
Single Screen 時は Master Page のコンテンツのみ表示し、Dual Screen 時は 左に Master Page のコンテンツ、右に Detail Page のコンテンツを表示します。
 

MasterDetailPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    xmlns="http://xamarin.com/schemas/2014/forms"
            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
            xmlns:d="http://xamarin.com/schemas/2014/forms/design"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:dualScreen="clr-namespace:Xamarin.Forms.DualScreen;assembly=Xamarin.Forms.DualScreen"
            xmlns:local="clr-namespace:XFSurfaceDuoSample2020"
            mc:Ignorable="d"
            x:Class="XFSurfaceDuoSample2020.MasterDetailPage">
    <dualScreen:TwoPaneView MinWideModeWidth="4000" MinTallModeHeight="4000">
        <dualScreen:TwoPaneView.Pane1>
            <local:Master x:Name="masterPage"></local:Master>
        </dualScreen:TwoPaneView.Pane1>
        <dualScreen:TwoPaneView.Pane2>
            <local:Details x:Name="detailsPage"></local:Details>
        </dualScreen:TwoPaneView.Pane2>
    </dualScreen:TwoPaneView>
</ContentPage>

 

MasterDetailPage.xaml.cs
using System.ComponentModel;
using System.Linq;
using Xamarin.Forms;
using Xamarin.Forms.DualScreen;
using Xamarin.Forms.Xaml;

using XFSurfaceDuoSample2020.Models;
using XFSurfaceDuoSample2020.ViewModels;

namespace XFSurfaceDuoSample2020
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class MasterDetailPage
    {
        bool IsSpanned => DualScreenInfo.Current.SpanMode != TwoPaneViewMode.SinglePane;

        DetailsPage detailsPagePushed;

        public MasterDetailPage()
        {
            InitializeComponent();
            NavigationPage.SetHasNavigationBar(this, IsSpanned);
            detailsPagePushed = new DetailsPage();
        }

        async void SetupViews()
        {
            NavigationPage.SetHasNavigationBar(this, !IsSpanned);
            NavigationPage.SetHasNavigationBar(detailsPagePushed, !IsSpanned);
            NavigationPage.SetHasNavigationBar(detailsPage, !IsSpanned);

            if (!IsSpanned)
            {
                if (!Navigation.NavigationStack.Contains(detailsPagePushed))
                    await Navigation.PushAsync(detailsPagePushed);
            }
        }

        protected override void OnAppearing()
        {
            base.OnAppearing();
            DualScreenInfo.Current.PropertyChanged += OnFormsWindowPropertyChanged;
        }

        protected override void OnDisappearing()
        {
            base.OnDisappearing();
            DualScreenInfo.Current.PropertyChanged -= OnFormsWindowPropertyChanged;
        }

        void OnFormsWindowPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == nameof(DualScreenInfo.Current.SpanMode) ||
                e.PropertyName == nameof(DualScreenInfo.Current.IsLandscape))
            {
                SetupViews();
            }
        }

    }
}

 

DetailsPage.xaml のマークアップとコードを記述する

 
Single Screen 時は Detail Page を表示します。Dual Screen 時は使用されません。
 

DetailsPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:local="clr-namespace:XFSurfaceDuoSample2020"
             mc:Ignorable="d"
             x:Class="XFSurfaceDuoSample2020.DetailsPage"
             Title="Master Details"
             Padding="10,0,0,0"
             >
    <local:Details></local:Details>
</ContentPage>

 

DetailsPage.xaml.cs
using Xamarin.Forms.DualScreen;
using Xamarin.Forms.Xaml;

namespace XFSurfaceDuoSample2020
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class DetailsPage
    {
        bool IsSpanned => DualScreenInfo.Current.SpanMode != TwoPaneViewMode.SinglePane;

        public DetailsPage()
        {
            InitializeComponent();
        }

        protected override void OnAppearing()
        {
            base.OnAppearing();
            DualScreenInfo.Current.PropertyChanged += OnFormsWindowPropertyChanged;
        }

        protected override void OnDisappearing()
        {
            base.OnDisappearing();
            DualScreenInfo.Current.PropertyChanged -= OnFormsWindowPropertyChanged;
        }

        async void OnFormsWindowPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if(e.PropertyName == nameof(DualScreenInfo.Current.SpanMode) ||
                e.PropertyName == nameof(DualScreenInfo.Current.IsLandscape))
            {
                if (IsSpanned)
                    await Navigation.PopAsync();
            }
        }
    }
}

 
 

Master.xaml のマークアップとコードを記述する

 
Master Page のコンテンツです。
 

Master.xaml
<?xml version="1.0" encoding="utf-8" ?>
<StackLayout
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:d="http://xamarin.com/schemas/2014/forms/design"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    x:Class="XFSurfaceDuoSample2020.Master"
    >
    <StackLayout.Resources>
        <ResourceDictionary>
            <Style TargetType="Grid">
                <Setter Property="VisualStateManager.VisualStateGroups">
                    <VisualStateGroupList>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal" />
                            <VisualState x:Name="Selected">
                                <VisualState.Setters>
                                    <Setter
                                        Property="BackgroundColor"
                                        Value="LightSkyBlue"
                                        />
                                </VisualState.Setters>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateGroupList>
                </Setter>
            </Style>
        </ResourceDictionary>
    </StackLayout.Resources>
    <Picker
        Title="路線を選択して下さい"
        Margin="10"
        HeightRequest="60"
        FontSize="Large"
        HorizontalOptions="Fill"
        VerticalOptions="Fill"
        ItemsSource="{Binding LineItems}"
        SelectedItem="{Binding SelectedLineItem, Mode=TwoWay}"
        >
    </Picker>
    <!-- 確認用(後ほど消去する)ここから -->
    <Label FontSize="Large" TextColor="Coral" Text="Master Page のコンテンツ" HorizontalOptions="CenterAndExpand">
    </Label>
    <!-- 確認用(後ほど消去する)ここまで -->
    <CollectionView
        x:Name="Stations"
        ItemsSource="{Binding StationItems}"
        SelectionMode="Single"
        SelectedItem="{Binding SelectedStationItem, Mode=TwoWay}"
        SelectionChangedCommand="{Binding SelectStationCommand}"
        >
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <Grid
                    Padding="5"
                    >
                    <Frame Visual="Material" BorderColor="LightGray">
                        <StackLayout
                            Padding="0"
                            >
                            <Label FontSize="Title" Text="{Binding Name}">
                            </Label>
                        </StackLayout>
                    </Frame>
                </Grid>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</StackLayout>

 

Master.xaml.cs
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

using XFSurfaceDuoSample2020.ViewModels;

namespace XFSurfaceDuoSample2020
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class Master : StackLayout
    {
        public Master()
        {
            InitializeComponent();
        }
    }
}

 

Details.xaml のマークアップとコードを記述する

 
Detail Page のコンテンツです。
 

Details.xaml
<?xml version="1.0" encoding="UTF-8"?>
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d"
             x:Class="XFSurfaceDuoSample2020.Details"
             BackgroundColor="LightGray"
             Spacing="0"
             >
    <Grid
        BackgroundColor="White"
        RowSpacing="2" ColumnSpacing="2"
        Padding="8,8,8,0"
        VerticalOptions="Center"
        >
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Label
            BackgroundColor="White"
            HorizontalTextAlignment="End" VerticalTextAlignment="Center"
            FontSize="Title" Text="ナンバリング :"
            >
        </Label>
        <Label
            BackgroundColor="White"
            Grid.Row="1"
            HorizontalTextAlignment="End" VerticalTextAlignment="Center"
            FontSize="Title" Text="駅名 :"
            >
        </Label>
        <Label
            BackgroundColor="White"
            FontSize="Title"
            Grid.Column="1"
            Text="{Binding SelectedStationItem.ID}"
            VerticalTextAlignment="Center"
            >
        </Label>
        <Label
            BackgroundColor="White"
            FontSize="Title"
            Grid.Column="1" Grid.Row="1"
            Text="{Binding Name}"
            VerticalTextAlignment="Center"
            >
        </Label>
    </Grid>
    <StackLayout
        BackgroundColor="White"
        Padding="5">
        <!-- 確認用(後ほど消去する)ここから -->
        <Label FontSize="Large" TextColor="Coral" Text="Detail Page のコンテンツ" HorizontalOptions="CenterAndExpand">
        </Label>
        <!-- 確認用(後ほど消去する)ここまで -->
        <CollectionView
            Margin="3"
            BackgroundColor="LightGray"
            x:Name="Stations"
            ItemsSource="{Binding TimeTableRowItems}"
            SelectionMode="None"
            >
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <StackLayout
                        BackgroundColor="LightGray"
                        Padding="1">
                        <Grid
                            BackgroundColor="LightGray"
                            HorizontalOptions="Fill"
                            VerticalOptions="Center"
                            ColumnSpacing="2"
                            Padding="2,1,2,1"
                            >
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"></RowDefinition>
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"></ColumnDefinition>
                                <ColumnDefinition Width="10*"></ColumnDefinition>
                            </Grid.ColumnDefinitions>
                            <Label
                                BackgroundColor="LightSkyBlue"
                                Padding="8"
                                Grid.Column="0" Grid.Row="0"
                                Text="{Binding Hour}"
                                FontSize="Large"
                                HorizontalTextAlignment="Center" 
                                VerticalTextAlignment="Center"
                                >
                            </Label>
                            <Label
                                BackgroundColor="Beige"
                                Padding="8"
                                Grid.Column="1" Grid.Row="0"
                                Text="{Binding RowText}"
                                FontSize="Large"
                                HorizontalTextAlignment="Start"
                                VerticalTextAlignment="Center"
                                >
                            </Label>
                        </Grid>
                    </StackLayout>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </StackLayout>
</StackLayout>

 

Details.xaml.cs
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace XFSurfaceDuoSample2020
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class Details : StackLayout
    {
        public Details()
        {
            InitializeComponent();
        }
    }
}

 

デバッグ実行し、Single Screen、Dual Screen で表示されるか確認する。

 
Surface Duo エミュレータ―にデプロイすると、以下のように、Single Screen、Dual Screen で表示が変化します。
 

Single Screen での表示

f:id:hiro128:20201018214953p:plain
 

Dual Screen での表示

f:id:hiro128:20201018215010p:plain

 
  
Dual Screen で表示させるためには以下の動画の手順でアプリをスパンしてください。
なお、アプリのロジックはまだ実装していないので、画面が表示されるだけで、UI は反応しません。
 
Surface Duo Span 操作
youtu.be

 

続きは以下をご覧ください。
hiro128.hatenablog.jp