In case you’ve been wondering how to achieve different XAML layouts for different device orientation with Xamarin.Forms (just Forms from now), here is a solution. The thing is that there is currently no support for this. There is support for different layout when it comes Phone vs Tablet (OnIdiom<T>) or when it comes to different OS (OnPlatform<T>) but the thing is that these guys are static. They do their work once and that’s it. They won’t react to changes and it makes sense, since underlying OS or device type don’t change during runtime.
So something different is need. But before I even start with the XAML there is an important part that has to be done – getting the current orientation, or better, rotation at the Portable Class Library level. It is a twofold solution.
Here is code that exists in PCL project and isn’t OS specific.
public enum Rotation { Rotation0, Rotation90, Rotation180, Rotation270 } public static class Orientation { private static Rotation rotation; public static EventHandler RotationChanged; public static Rotation Rotation { get { return rotation; } set { if (Rotation != value) { rotation = value; OnRotationChanged(); } } } public static bool IsLandscape { get { return Rotation == Rotation.Rotation90 || Rotation == Rotation.Rotation270; } } private static void OnRotationChanged() { if (RotationChanged != null) RotationChanged(null, EventArgs.Empty); } }
And here is the OS specific part where you set the orientation.
Android version:
public class MainActivity : AndroidActivity { protected override void OnCreate (Bundle bundle) { UpdateCurrentRotation(); base.OnCreate (bundle); Xamarin.Forms.Forms.Init (this, bundle); SetPage (App.GetMainPage ()); } public override void OnConfigurationChanged(Android.Content.Res.Configuration newConfig) { base.OnConfigurationChanged(newConfig); UpdateCurrentRotation(); } private void UpdateCurrentRotation() { switch (WindowManager.DefaultDisplay.Rotation) { case SurfaceOrientation.Rotation0: Orientation.Rotation = Rotation.Rotation0; break; case SurfaceOrientation.Rotation90: Orientation.Rotation = Rotation.Rotation90; break; case SurfaceOrientation.Rotation180: Orientation.Rotation = Rotation.Rotation180; break; case SurfaceOrientation.Rotation270: Orientation.Rotation = Rotation.Rotation270; break; } } }
Windows Phone version (note that you have to set SupportedOrientations="PortraitOrLandscape" in your MainPage.xaml to enable rotation of the page):
public partial class MainPage : PhoneApplicationPage { public MainPage() { InitializeComponent(); UpdateRotation(); Forms.Init(); Content = App16.App.GetMainPage().ConvertPageToUIElement(this); } private void UpdateRotation() { if ((Orientation & PageOrientation.PortraitUp)
== PageOrientation.PortraitUp) App16.Orientation.Rotation = Rotation.Rotation0; else if ((Orientation & PageOrientation.PortraitDown)
== PageOrientation.PortraitDown) App16.Orientation.Rotation = Rotation.Rotation180; else if ((Orientation & PageOrientation.LandscapeLeft)
== PageOrientation.LandscapeLeft) App16.Orientation.Rotation = Rotation.Rotation90; else App16.Orientation.Rotation = Rotation.Rotation270; } protected override void OnOrientationChanged
(OrientationChangedEventArgs e) { UpdateRotation(); base.OnOrientationChanged(e); } }
Sadly I have no iOS device at this time, but it should be pretty easy to support it as well.
At the beginning of the page/activity and each time orientation changes the current rotation is sent to Orientation class in PCL.
At this point we have the rotation information and event RotationChanges that notifies about changes in rotation. Now this information can be used to achieve the dynamic layout changes.
For that purpose I’ll use a Grid derived class that can be nicely used with XAML.
public class Rotational: Grid { public View Landscape { get; set; } public View Portrait { get; set; } public string LandscapePage { get; set; } public string PortraitPage { get; set; } private bool isPortrait; protected override void OnParentSet() { base.OnParentSet(); VisualElement view = ParentView; while (!(view is Page)) { view = view.ParentView; } Page page = (Page)view; page.Disappearing += page_Disappearing; page.Appearing += page_Appearing; } void page_Appearing(object sender, EventArgs e) { Orientation.RotationChanged += CurrentOrientation_Changed; Update(); } private void CurrentOrientation_Changed(object sender, EventArgs e) { Update(); } void page_Disappearing(object sender, EventArgs e) { Orientation.RotationChanged -= CurrentOrientation_Changed; } private void Update() { if (Children.Count == 0 || Orientation.IsLandscape && isPortrait || !Orientation.IsLandscape && !isPortrait) { Children.Clear(); if (Orientation.IsLandscape) { isPortrait = false; if (Landscape != null) { Children.Add(Landscape); } else { View child = FindPageElement(LandscapePage); if (child != null) Children.Add(child); } } else { isPortrait = true; if (Portrait != null) { Children.Add(Portrait); } else { View child = FindPageElement(PortraitPage); if (child != null) Children.Add(child); } } } } private View FindPageElement(string pageName) { var query = from t in GetType().GetTypeInfo().Assembly.DefinedTypes where t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(ContentPage)) && t.Name == pageName select t; var pageTypeInfo = query.SingleOrDefault(); if (pageTypeInfo != null) { ContentPage page = (ContentPage)Activator.CreateInstance
(pageTypeInfo.AsType()); View view = page.Content; page.Content = null; return view; } else return null; } }
The class is named Rotational and defines four properties: Portrait, PortraitPage, Landscape and LandscapePage. Portrait and Landscape are of type View and you can store View derived classes that would appear based on orientation. Here is an example:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:v="clr-namespace:App16;assembly=App16" x:Class="App16.TestPage"> <v:Rotational> <v:Rotational.Landscape> <StackLayout Orientation="Vertical"> <Label Text="Landscape" HorizontalOptions="Center" /> </StackLayout> </v:Rotational.Landscape> <v:Rotational.Portrait> <StackLayout Orientation="Vertical"> <Label Text="Portrait" HorizontalOptions="Center" /> </StackLayout> </v:Rotational.Portrait> </v:Rotational> </ContentPage>
Note that namespace declaration is required (xmlns:v="clr-namespace:App16;assembly=App16"). The above definition will show a label saying Portrait or Landscape, depending on device rotation and will change dynamically. The drawbacks of this approach are that both configurations are in-memory and the page might become a bit bloated.
Hence the second pair of properties: PortraitPage and LandscapePage. These are of string type and allow to enter the page class names the content of which will be used dynamically. Here is an example:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:v="clr-namespace:App16;assembly=App16" x:Class="App16.TestPage"> <v:Rotational PortraitPage="PortraitTestPage"
LandscapePage="LandscapeTestPage" /> </ContentPage>
Additional two pages are required (PortraitTestPage.xaml and LandscapeTestPage.xaml):
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="App16.PortraitTestPage"> <Label Text="Portrait test" VerticalOptions="Center"
HorizontalOptions="Center" /> </ContentPage> ... <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="App16.LandscapeTestPage"> <Label Text="Landscape Test" VerticalOptions="Center"
HorizontalOptions="Center" /> </ContentPage>
The pages are found using reflection and based on their names (no namespace is used).
This method is more memory efficient since it loads required content on demand and discards it afterwards. On the other side it is slower due to the loading of the page.
So, here you have it. What do you think? Is there a better way?
The sample is attached.
Rotational.zip (77.2KB)Update 1: Instead of dynamically loading ContentPage and then using its content, ContentView should be used (it is XAML supported). That'd be a change in FindPageElement method to:
private View FindContentView(string contentViewName) { var query = from t in GetType().GetTypeInfo().Assembly.DefinedTypes where t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(ContentView)) && t.Name == contentViewName select t; var contentViewTypeInfo = query.SingleOrDefault(); if (contentViewTypeInfo != null) { ContentView page = (ContentView)Activator.CreateInstance
(contentViewTypeInfo.AsType()); return view; } else return null; }