Seit wenigen Wochen ist das neue Smartphone von Microsoft mit Android und dem Namen Surface Duo auch außerhalb den USA erhältlich. In einem Blog-Beitrag habe ich das Gerät mit den zwei Screens bereits vorgestellt. Nun dachte ich mir, dass es an der Zeit ist einmal einen Blick auf die Entwicklung für das Surface Duo zu werfen. Als Beispiel-Anwendung soll eine einfache Liste dienen, welche bei der Auswahl eines Eintrags ein entsprechendes Detail anzeigt. Das besondere dabei ist, dass wir die beiden Screens des Surface Duos verwenden wollen, aber gleichzeitig auch lauffähig sind, wenn keine zwei Screens zur Verfügung steht.
Die Ausgangslage bildet eine normale Xamarin.Forms App. Zunächst aktualisieren wir alle NuGet-Pakete auf die aktuellste Version. Da das Surface Duo im Vordergrund steht, wollen wir uns auch nur die Android-App anschauen und daher besteht unsere Xamarin.Forms App auch nur aus der .NET Standard Library und dem Android Projekt.
Nun müssen wir noch weitere NuGet-Pakete installieren, damit wir das volle Potenzial des Surface Duos in unsere Xamarin.Forms App ausschöpfen können. Beginnen wir mit dem Paket Xamarin.Forms.DualScreen. Dieses Paket muss in beide Projekte integriert werden. Außerdem brauchen wir noch das Paket Xamarin.DuoSdk, welches wir jedoch nur dem Android-Projekt hinzufügen. Nun öffnen wir die Klasse MainActivity
im Android-Projekt und ergänzen den Init
-Aufruf des DualScreenServices
.
Xamarin.Forms.DualScreen.DualScreenService.Init(this);
Damit sind die Vorbereitungen soweit abgeschlossen und wir können uns jetzt um die Daten-Klasse kümmern. Dafür erstellen wir einen neuen Ordner Models in unserem Projekt und darin dann eine Klasse mit den Namen MyListItem
. Diese Klasse besitzt zwei Properties, nämlich Title und Detail. Wir erstellen noch den Konstruktor, welcher eine Zahl entgegen nimmt und entsprechend Titel und Detail mit Inhalt füllt.
public class MyListItem
{
public string Title { get; }
public string Detail { get; }
public MyListItem(int num)
{
Title = $"Titel {num}";
Detail = $"Das ist das Detail mit der Nummer {num}.";
}
}
Nun erstellen wir einen weiteren Ordner in unserem Projekt mit den Namen Views. Darin erstellen wir eine neue ContentPage mit den Namen MainPage. Hier ergänzen wir zunächst den Title, setzen ein Padding und aktivieren das Material Design.
<?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:dualscreen="clr-namespace:Xamarin.Forms.DualScreen;assembly=Xamarin.Forms.DualScreen"
x:Class="ListDetailSample.Views.MainPage"
Title="Surface Duo: List Detail"
Padding="16"
Visual="Material">
</ContentPage>
Da wir ja beim Surface Duo zwei Screens zur Verfügung haben, wollen wir die TwoPaneView
verwenden, welche es uns ermöglicht für jeden Screen ein Layout zu definieren.
<?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:dualscreen="clr-namespace:Xamarin.Forms.DualScreen;assembly=Xamarin.Forms.DualScreen"
x:Class="ListDetailSample.Views.MainPage"
Title="Surface Duo: List Detail"
Padding="16"
Visual="Material">
<dualscreen:TwoPaneView MinWideModeWidth="4000"
MinTallModeHeight="4000">
<dualscreen:TwoPaneView.Pane1>
</dualscreen:TwoPaneView.Pane1>
<dualscreen:TwoPaneView.Pane2>
</dualscreen:TwoPaneView.Pane2>
</dualscreen:TwoPaneView>
</ContentPage>
Nun fehlt uns aktuell noch der Content, welchen wir auf den Screens anzeigen wollen. Dafür erstellen wir in unserem Views-Ordner eine neue ContentView mit dem Namen ListView.
<?xml version="1.0" encoding="UTF-8"?>
<CollectionView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ListDetailSample.Views.ListView"
SelectionMode="Single">
<CollectionView.ItemsLayout>
<LinearItemsLayout ItemSpacing="12"
Orientation="Vertical" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame BorderColor="LightGray">
<StackLayout Padding="5">
<Label Text="{Binding Title}"
FontSize="Title" />
</StackLayout>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
Nun wechseln wir in die Code-Behind Datei und tauschen auch hier ContentView durch CollectionView aus. Außerdem setzen wir im Konstruktor auch noch die ItemsSource. Hier nutzen wir die Methode Range
der Klasse Enumerable
und erzeugen uns so 50 Elemente von unserem MyListItem
.
public partial class ListView : CollectionView
{
public ListView()
{
InitializeComponent();
ItemsSource = Enumerable.Range(1, 50)
.Select(x => new MyListItem(x));
}
}
Damit wechseln wir zurück auf unsere MainPage
und ergänzen nun im Pane1
unsere eigene ListView
.
<?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:dualscreen="clr-namespace:Xamarin.Forms.DualScreen;assembly=Xamarin.Forms.DualScreen"
xmlns:views="clr-namespace:ListDetailSample.Views"
x:Class="ListDetailSample.Views.MainPage"
Title="Surface Duo: List Detail"
Padding="16"
Visual="Material">
<dualscreen:TwoPaneView MinWideModeWidth="4000"
MinTallModeHeight="4000">
<dualscreen:TwoPaneView.Pane1>
<views:ListView />
</dualscreen:TwoPaneView.Pane1>
<dualscreen:TwoPaneView.Pane2>
</dualscreen:TwoPaneView.Pane2>
</dualscreen:TwoPaneView>
</ContentPage>
Damit können wir uns jetzt um das Detail kümmern. Hierfür erstellen wir eine neue ContentView im unseren Views-Ordner mit dem Namen DetailView.
Auch hier tauschen wir das Root-Objekt aus. Wir verwenden statt ContentView hier Grid und ergänzen die RowDefinitions
. Außerdem fügen wir noch den Content
hinzu, indem wir den Title und das Detail entsprechend ausgeben.
<?xml version="1.0" encoding="UTF-8"?>
<Grid xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ListDetailSample.Views.DetailView"
RowDefinitions="Auto,*"
Padding="16">
<Label Grid.Row="0">
<Label.FormattedText>
<FormattedString>
<Span Text="Title: "
FontAttributes="Bold" />
<Span Text="{Binding Title}" />
</FormattedString>
</Label.FormattedText>
</Label>
<Label Grid.Row="1">
<Label.FormattedText>
<FormattedString>
<Span Text="Details: "
FontAttributes="Bold" />
<Span Text="{Binding Detail}" />
</FormattedString>
</Label.FormattedText>
</Label>
</Grid>
Anschließend wechseln wir in die Code-Behind Datei und tauschen auch hier ContentView mit Grid aus. Nun wechseln wir wieder zurück in die MainPage
und ergänzen hier Pane2
mit unserer DetailView
.
<?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:dualscreen="clr-namespace:Xamarin.Forms.DualScreen;assembly=Xamarin.Forms.DualScreen"
xmlns:views="clr-namespace:ListDetailSample.Views"
x:Class="ListDetailSample.Views.MainPage"
Title="Surface Duo: List Detail"
Padding="16"
Visual="Material">
<dualscreen:TwoPaneView MinWideModeWidth="4000"
MinTallModeHeight="4000">
<dualscreen:TwoPaneView.Pane1>
<views:ListView />
</dualscreen:TwoPaneView.Pane1>
<dualscreen:TwoPaneView.Pane2>
<views:DetailView />
</dualscreen:TwoPaneView.Pane2>
</dualscreen:TwoPaneView>
</ContentPage>
Wir öffnen nun die App.xaml.cs
Datei und tauschen die MainPage
durch unsere neue MainPage
aus und in diesem Zusammenhang können wir dann auch die Default-MainPage löschen.
MainPage = new NavigationPage(new MainPage());
Nun können wir auch zum ersten Mal die App ausprobieren. Sofern die App nur auf einem Screen ausgeführt wird, sehen wir die Liste mit unseren Demo-Daten und wenn wir nun die App auf zwei Screens aufspannen, dann sehen wir links die Liste und rechts das Detail, welches sich jedoch aktuell noch nicht aktualisiert. Darum wollen wir uns jetzt kümmern.
Wir öffnen wieder unsere MainPage und vergeben unserer ListView
und unserer DetailView
jeweils einen Namen, so dass wir über die Code Behind Datei darauf zugreifen können.
<?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:dualscreen="clr-namespace:Xamarin.Forms.DualScreen;assembly=Xamarin.Forms.DualScreen"
xmlns:views="clr-namespace:ListDetailSample.Views"
x:Class="ListDetailSample.Views.MainPage"
Title="Surface Duo: List Detail"
Padding="16"
Visual="Material">
<dualscreen:TwoPaneView MinWideModeWidth="4000"
MinTallModeHeight="4000">
<dualscreen:TwoPaneView.Pane1>
<views:ListView x:Name="ListViewPage" />
</dualscreen:TwoPaneView.Pane1>
<dualscreen:TwoPaneView.Pane2>
<views:DetailView x:Name="DetailViewPage" />
</dualscreen:TwoPaneView.Pane2>
</dualscreen:TwoPaneView>
</ContentPage>
In der Code Behind Datei erweitern wir den Konstruktor und registrieren das SelectionChanged
-Event.
public MainPage()
{
InitializeComponent();
ListViewPage.SelectionChanged += OnSelectionChanged;
}
In dem Event überprüfen wir, ob ein Element ausgewählt wurde und rufen dann eine Methode SetBindingContext
auf.
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.CurrentSelection == null || e.CurrentSelection.Count == 0)
return;
SetBindingContext();
}
In der SetBindingContext
holen wir uns das SelectedItem
unserer Liste und setzen den BindingContext
unserer DetailView.
private void SetBindingContext()
{
var selectedItem = ListViewPage.SelectedItem
?? (ListViewPage.ItemsSource as IEnumerable<MyListItem>).FirstOrDefault();
DetailViewPage.BindingContext = selectedItem;
}
Wenn wir die App nun erneut ausprobieren, stellen wir fest, dass im Single Screen noch nicht viel passiert, aber wenn wir die App nun über beide Screens aufspannen, dann aktualisiert sich jetzt auch das Detail entsprechend. Aber wenn die App auf einem Gerät mit nur einem Screen ausgeführt wird, ist die App noch nicht wirklich funktional. Daher kehren wir zurück zu unserem Code, um auch hier dieses Problem zu lösen.
Um nun die App auch im Single Screen betreiben zu können, erstellen wir eine neue ContentPage im Order Views und nennen diese DetailPage. Wir setzen einen Title
, das entsprechende Padding
und auch wieder Material
als Visual
. Anschließend fügen wir als Content einfach unsere DetailView hinzu.
<?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:views="clr-namespace:ListDetailSample.Views"
x:Class="ListDetailSample.Views.DetailPage"
Title="Detail"
Padding="16"
Visual="Material">
<ContentPage.Content>
<views:DetailView />
</ContentPage.Content>
</ContentPage>
Der Trick ist nun, dass wir überprüfen, ob wir auf einem einzelnen Screen laufen und dann entsprechend auf die DetailPage navigieren. Dazu öffnen wir wieder unsere Code-Behind Datei der MainPage und passen diese entsprechend an. Zunächst fügen wir zwei Variablen hinzu. Die eine Variable überprüft, ob unsere App aktuell über zwei Screens aufgespannt wurde und die andere beinhaltet eine Referenz auf unsere neue DetailPage.
private readonly DetailPage _detailPage;
private bool IsSpanned
=> DualScreenInfo.Current.SpanMode != TwoPaneViewMode.SinglePane;
Im Konstruktor setzen wir nun die Variable _detailPage
.
public MainPage()
{
InitializeComponent();
ListViewPage.SelectionChanged += OnSelectionChanged;
_detailPage = new DetailPage();
}
Nun implementieren wir die beiden Methoden OnAppearing
und OnDisappearing
. Hier registrieren wir das PropertyChanged
-Event, um auf Änderung am Screen zu reagieren.
protected override void OnAppearing()
{
base.OnAppearing();
if (!IsSpanned)
ListViewPage.SelectedItem = null;
DualScreenInfo.Current.PropertyChanged
+= OnDualScreenInfoPropertyChanged;
}
protected override void OnDisappearing()
{
base.OnDisappearing();
DualScreenInfo.Current.PropertyChanged
-= OnDualScreenInfoPropertyChanged;
}
In dem EventHandler
überprüfen wir nun, ob sich der SpanMode
oder IsLandscape
geändert hat. Wenn dies der Fall ist, so rufen wir eine Methode SetupViews
auf.
private void OnDualScreenInfoPropertyChanged(object sender,
PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(DualScreenInfo.Current.SpanMode)
|| e.PropertyName == nameof(DualScreenInfo.Current.IsLandscape))
{
SetupViews();
}
}
Die Methode SetupViews
übernimmt nun die ganze Magie, denn wenn die App nicht über zwei Screens aufgespannt ist, dann navigieren wir einfach auf unsere neue DetailPage.
private async void SetupViews()
{
if (IsSpanned && !DualScreenInfo.Current.IsLandscape)
SetBindingContext();
if (DetailViewPage.BindingContext == null)
return;
if (!IsSpanned)
{
if (!Navigation.NavigationStack.Contains(_detailPage))
await Navigation.PushAsync(_detailPage);
}
}
In der SetBindingContext
-Methode ergänzen wir noch das Setzen des BindingContext
für unsere DetailPage.
private void SetBindingContext()
{
var selectedItem = ListViewPage.SelectedItem
?? (ListViewPage.ItemsSource as IEnumerable<MyListItem>).FirstOrDefault();
DetailViewPage.BindingContext = selectedItem;
_detailPage.BindingContext = selectedItem;
}
Und in der Methode OnSelectionChanged
noch den Aufruf von SetupViews
.
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.CurrentSelection == null || e.CurrentSelection.Count == 0)
return;
SetBindingContext();
SetupViews();
}
Damit können wir die App nun ein weiteres Mal testen und wir sehen nun, dass die App sowohl mit einem Screen als auch mit zwei Screens nutzbar ist.
In diesem Beitrag habe ich euch nun gezeigt, wie wir eine erste App entwickelt haben, welche Gebrauch von den zwei Screens eines Surface Duos macht, aber auch weiterhin auf Geräten mit nur einem Screen lauffähig ist. Ich habe die gleiche App auch in einem meiner letzten Videos programmiert.
Den gesamten Code findet ihr hier auf GitHub. Aber natürlich ist dies nicht die einzige Option, um eine App für das Surface Duo zu entwickeln. Daher seid doch auch beim nächsten Beitrag zu diesem Thema wieder mit dabei.