Responding to Orientation Changes in Xamarin Forms

Last week, I published the first in a series of articles on building cross-platform mobile apps with Visual Studio 2015 and Xamarin Forms. In it, I presented an RPN calculator app that works on Windows Phone, Android, and iOS.

One subject I didn’t address in that article was how to respond to orientation changes in a Xamarin Forms app – that is, when the device goes from portrait mode to landscape or vice versa. By default, the screen rotates so that the content remains right-side-up. You can “lock” the orientation to prevent the screen from rotating (more on that in a moment), but the more common case is that you want to change what’s on the screen to adapt to the new dimensions.

I built a second version of my calculator app that does just that. When the device rotates to landscape mode, two additional columns of buttons appear on the left side of the screen – buttons for sin, cos, tan, and so on:

Calculator-Group2

Most mobile operating systems fire some type of event when the device orientation changes. Xamarin Forms does not, at least not yet. (Xamarin Forms is a work in progress and it’s evolving rapidly. It hasn’t escaped the notice of the engineers in charge that the platform lacks a device-orientation event, so don’t be surprised if one is added soon.) In the meantime, the recommended way to respond to orientation changes is to override the page’s virtual OnSizeAllocated method and make any changes you wish to make inside the override. Here’s a simple example that writes to debug output when the orientation changes and indicates whether the device is in portrait mode or landscape mode:

protected override void OnSizeAllocated(double width, double height)
{
    base.OnSizeAllocated(width, height);
    Debug.WriteLine(width > height ? "Landscape" : "Portrait");
}

There are a few caveats to be aware of when overriding OnSizeAllocated:

  • It typically fires many times when the page is created
  • If you make changes to the layout inside the override – if, for example, you show or hide a bunch of controls or move controls around – it can fire many times each time the orientation changes
  • When you override this method, be sure to call the base class’s version of the same method from the override; otherwise, you may end up staring at a blank screen

The upshot of bullets 1 and 2 is that it’s smart to build logic into the override to prevent the layout changes you make from being applied multiple times following a single change in orientation.

Here’s the code I added to CalculatorPage.xaml.cs to make additional buttons appear in landscape mode and disappear in portrait mode:

protected override void OnSizeAllocated(double width, double height)
{
    base.OnSizeAllocated(width, height); // Important!

    if (width != _width || height != _height)
    {
        _width = width;
        _height = height;
        ShowExtraButtons(width > height);
    }
}

private void ShowExtraButtons(bool visible)
{
    foreach (View child in LayoutRoot.Children)
    {
        if (child is Button && (int)child.GetValue(Grid.ColumnProperty) < 2)
        {
            child.IsVisible = visible;
        }
    }
}

The ShowExtraButtons method enumerates the buttons in the Grid control named “LayoutRoot” and either shows or hides the ones in the left two columns by setting their IsVisible properties to true or false. (Incidentally, Microsoft XAML developers are accustomed to controlling visibility using the Visibility property; In Xamarin XAML, you use IsVisible instead. It’s a Boolean, so you won’t have to write those silly Boolean-to-Visibility value converters any more.) _width and _height are private fields I declared in the page to store the last width and height passed to OnSizeAllocated. They’re used to prevent ShowExtraButtons from being called more than once when OnSizeAllocated fires several times in a row with the same width and height parameters.

Where did the extra two columns of buttons come from? I modified CalculatorPage.xaml to declare them. Here’s the modified version. The new buttons are declared near the bottom, after the comment that reads “Buttons visible only in landscape mode:”

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XFormsRPNCalculator.CalculatorPage"
             xmlns:local="clr-namespace:XFormsRPNCalculator;assembly=XFormsRPNCalculator"
             Padding="16">

  <ContentPage.BackgroundColor>
    <OnPlatform x:TypeArguments="Color" iOS="#fff0f0f0" Android="Black" />
  </ContentPage.BackgroundColor>

  <ContentPage.Resources>
    <ResourceDictionary>
      <Style TargetType="Button">
        <Setter Property="BorderColor">
          <Setter.Value>
            <OnPlatform x:TypeArguments="Color" WinPhone="#FF404040" />
          </Setter.Value>
        </Setter>
        <Setter Property="BackgroundColor">
          <Setter.Value>
            <OnPlatform x:TypeArguments="Color" iOS="White" WinPhone="#FF303030" />
          </Setter.Value>
        </Setter>
      </Style>
    </ResourceDictionary>
  </ContentPage.Resources>

  <Grid x:Name="LayoutRoot">
    <Grid.RowDefinitions>
      <RowDefinition Height="1.6*" />
      <RowDefinition Height="*" />
      <RowDefinition Height="*" />
      <RowDefinition Height="*" />
      <RowDefinition Height="*" />
      <RowDefinition Height="*" />
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
      
    <Grid.RowSpacing>
        <OnPlatform x:TypeArguments="x:Double" iOS="8" WinPhone="-10" />
    </Grid.RowSpacing>

    <Grid.ColumnSpacing>
        <OnPlatform x:TypeArguments="x:Double" iOS="8" WinPhone="-10" />
    </Grid.ColumnSpacing>

    <ContentView Padding="8" Grid.Row="0" Grid.ColumnSpan="6">
      <Label x:Name="Output" Text="{Binding Output}" Grid.Column="0" XAlign="End">
        <Label.Font>
          <OnPlatform x:TypeArguments="Font" iOS="36" Android="48" WinPhone="56" />
        </Label.Font>
      </Label>
    </ContentView>

    <Button Text="±" Grid.Row="1" Grid.Column="2" Command="{Binding CalculatorCommand}" CommandParameter="±" />
    <Button Text="EXP" Grid.Row="1" Grid.Column="3" Command="{Binding CalculatorCommand}" CommandParameter="EXP" />
    <Button Text="STO" Grid.Row="1" Grid.Column="4" Command="{Binding CalculatorCommand}" CommandParameter="STO" />
    <Button Text="RCL" Grid.Row="1" Grid.Column="5" Command="{Binding CalculatorCommand}" CommandParameter="RCL" />

    <Button Text="ENTER" Grid.Row="2" Grid.ColumnSpan="2" Grid.Column="2" Command="{Binding CalculatorCommand}" CommandParameter="ENTER" />
    <Button Text="FIX" Grid.Row="2" Grid.Column="4" Command="{Binding CalculatorCommand}" CommandParameter="FIX" />
    <Button Text="CLX" Grid.Row="2" Grid.Column="5" Command="{Binding CalculatorCommand}" CommandParameter="CLX" />

    <Button Text="-" Grid.Row="3" Grid.Column="2" Command="{Binding CalculatorCommand}" CommandParameter="-" />
    <Button Text="7" Grid.Row="3" Grid.Column="3" Command="{Binding CalculatorCommand}" CommandParameter="7" />
    <Button Text="8" Grid.Row="3" Grid.Column="4" Command="{Binding CalculatorCommand}" CommandParameter="8" />
    <Button Text="9" Grid.Row="3" Grid.Column="5" Command="{Binding CalculatorCommand}" CommandParameter="9" />

    <Button Text="+" Grid.Row="4" Grid.Column="2" Command="{Binding CalculatorCommand}" CommandParameter="+" />
    <Button Text="4" Grid.Row="4" Grid.Column="3" Command="{Binding CalculatorCommand}" CommandParameter="4" />
    <Button Text="5" Grid.Row="4" Grid.Column="4" Command="{Binding CalculatorCommand}" CommandParameter="5" />
    <Button Text="6" Grid.Row="4" Grid.Column="5" Command="{Binding CalculatorCommand}" CommandParameter="6" />

    <Button Text="x" Grid.Row="5" Grid.Column="2" Command="{Binding CalculatorCommand}" CommandParameter="x" />
    <Button Text="1" Grid.Row="5" Grid.Column="3" Command="{Binding CalculatorCommand}" CommandParameter="1" />
    <Button Text="2" Grid.Row="5" Grid.Column="4" Command="{Binding CalculatorCommand}" CommandParameter="2" />
    <Button Text="3" Grid.Row="5" Grid.Column="5" Command="{Binding CalculatorCommand}" CommandParameter="3" />

    <Button Text="÷" Grid.Row="6" Grid.Column="2" Command="{Binding CalculatorCommand}" CommandParameter="÷" />
    <Button Text="0" Grid.Row="6" Grid.Column="3" Command="{Binding CalculatorCommand}" CommandParameter="0" />
    <Button Text="." Grid.Row="6" Grid.Column="4" Command="{Binding CalculatorCommand}" CommandParameter="." />
    <Button Text="DEL" Grid.Row="6" Grid.Column="5" Command="{Binding CalculatorCommand}" CommandParameter="DEL" />

    <!-- Buttons visible only in landscape mode -->
    <Button Text="sin" Grid.Row="1" Grid.Column="0" Command="{Binding CalculatorCommand}" CommandParameter="sin" IsVisible="False" />
    <Button Text="cos" Grid.Row="2" Grid.Column="0" Command="{Binding CalculatorCommand}" CommandParameter="cos" IsVisible="False" />
    <Button Text="tan" Grid.Row="3" Grid.Column="0" Command="{Binding CalculatorCommand}" CommandParameter="tan" IsVisible="False" />
    <Button Text="asin" Grid.Row="4" Grid.Column="0" Command="{Binding CalculatorCommand}" CommandParameter="asin" IsVisible="False" />
    <Button Text="acos" Grid.Row="5" Grid.Column="0" Command="{Binding CalculatorCommand}" CommandParameter="acos" IsVisible="False" />
    <Button Text="atan" Grid.Row="6" Grid.Column="0" Command="{Binding CalculatorCommand}" CommandParameter="atan" IsVisible="False" />

    <Button Text="1/x" Grid.Row="1" Grid.Column="1" Command="{Binding CalculatorCommand}" CommandParameter="1/x" IsVisible="False" />
    <Button Text="sqrt" Grid.Row="2" Grid.Column="1" Command="{Binding CalculatorCommand}" CommandParameter="sqrt" IsVisible="False" />
    <Button Text="x²" Grid.Row="3" Grid.Column="1" Command="{Binding CalculatorCommand}" CommandParameter="x²" IsVisible="False" />
    <Button Text="log" Grid.Row="4" Grid.Column="1" Command="{Binding CalculatorCommand}" CommandParameter="log" IsVisible="False" />
    <Button Text="ln" Grid.Row="5" Grid.Column="1" Command="{Binding CalculatorCommand}" CommandParameter="ln" IsVisible="False" />
    <Button Text="p" Grid.Row="6" Grid.Column="1" Command="{Binding CalculatorCommand}" CommandParameter="p" IsVisible="False" />
  </Grid>
</ContentPage>

Need Xamarin Forms Help?

Find the best tips tricks and lessons learned from leading developers

Learn More

The extra buttons are visible in landscape mode only. Because the width of the columns they’re in is “Auto” rather “*,” the columns collapse when the buttons disappear. An “Auto” column sizes itself to match the width of its widest element.

What If I Want to Lock the Orientation?

What if you’re writing a game and don’t want the screen to rotate when the device rotates? How do you lock the screen orientation in a Xamarin Forms app?

For an iOS app, you can open the project properties in Visual Studio, go to the “iOS Application” tab, and pick the orientations you wish to support. If you uncheck everything but ”Portrait,” the screen will remain locked in portrait mode:

image

For Android, you need to open MainActivity.cs in the Android project and add a ScreenOrientation property to the Activity attribute:

[Activity(Label = "MyAwesomePortableApp", Icon = "@drawable/icon", MainLauncher = true,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation,
    ScreenOrientation = ScreenOrientation.Portrait)]

Be aware that that due to a bug in Microsoft’s Android emulator, the screen will often rotate anyway. Presumably this bug will be fixed in the not-too-distant future.

For Windows Phone, the screen rotates when the device rotates due to the following statement in the MainPage constructor in MainPage.xaml.cs in the Windows Phone project:

SupportedOrientations = SupportedPageOrientation.PortraitOrLandscape;

To lock the orientation to portrait, modify this statement as follows:

SupportedOrientations = SupportedPageOrientation.Portrait;

As an alternative, you can delete this statement altogether and modify the corresponding SupportedOrientations attribute in MainPage.xaml.

Get the Source!

You can download version 2 of the calculator app from my OneDrive. Feel free to share it with your colleagues. And stay tuned for more articles about Xamarin Forms, because we’ve only scratched the surface of this awesome new platform.

Xamarin-Partner_thumb1_thumb_thumb

Need Xamarin Help?

Xamarin Consulting  Xamarin Training

Stay Informed

Sign up for the latest blogs, events, and insights.

We deliver solutions that accelerate the value of Azure.
Ready to experience the full power of Microsoft Azure?

Atmosera is thrilled to announce that we have been named GitHub AI Partner of the Year.

X