Software7

Personal Developer Notebook

Shared XAML Layouts for Windows Universal Apps

 

There are many different devices out there with different form factors and different screens pixels densities.

In order to ease the development with different pixel densities one works with logical pixels. Windows Runtime automatically scales the UI based on the actual device pixel density.

Together with layout primitives like Grid, StackPanel, GridView and so on one can create often very efficiently great looking adaptive UIs especially if you even can limit the layout orientation to either portrait or landscape.

Starting with the Windows 8.1 Runtime it’s possible to write Universal apps targeting phones, tablets and desktop at once (without the need for PCLs or Linked Files etc.). The template in Visual Studio sets two different XAML files for Phone and Windows 8.1 projects, to be able to create the best user experience for different kinds of devices, but sometimes it makes a lot of sense to use one shared XAML file.

Requirements

Before we start, a description of the use case in brief:

  • One page app
  • Universal (phone, tablet)
  • Support of portrait and landscape orientation
  • One header area with a main title
  • The main area is divided into two stripes of equal height
  • Each stripe should have a rotated subtitle on the left
  • Main title and subtitles are variable (think of database fields or localizations into different languages)
  • Both subtitles should have the same font size
  • The font size for the main title should be 42pt and the subtitle 20pt (if possible)
  • The subtitles should be centered and use 80% of the height of a stripe at maximum
  • The subtitles should use the same baseline
  • If the device is rotated the layout must adapt using these rules
  • The title areas should be double as large as the font size

Some of the requirements are contradictory, e.g. use a font size of 20pt and 80% of the stripe height at maximum with different screen sizes and orientation.

Therefore:

  • If there is not enough space for a title, the font size is reduced
  • Is the font size of one subtitle is decreased, the font size of the other title must also be adapted

We don’t want to calculate the whole layout by ourselves, so to be able to still use the goodness of the XAML layout primitives is also a requirement.

The following illustration of the desired user interface should give you a better understanding of the requirements.

Layout

Illustration of the intended layout

Essential Workflow

Regardless of which specific implementation one chooses (e.g. layout calculation in the MVVM’s ViewModel or an implementation as Behavior), the essential workflow is the same:

  • Use the current main title and calculate the width of the TextBlock using a font size of 42pt
  • If it uses more than 80% of the available horizontal space decrease the font size in 10-percent steps until it fits
  • Calculate the header height
  • Calculate the height of the stripes
  • Determine the TextBlock widths of both subtitles based on an initial font size of 20pt
  • If the width of one of them is larger than 80% of the height of the containing stripe, decrease the font size in 10-percent steps
  • Based on the font size calculate the width of the subtitle area

Screencasts

I assume that you already have a basic understanding of XAML. Nevertheless, I show all the steps for this project in the screencasts.

Whether your work with two separate XML files or a shared one depends on the actual use case. The following screencast shows how to share one XAML Layout. Before watching the screencasts it makes perhaps some sense to read a little bit more about the requirements below the screencasts section.

Screencast #1 shows:

  • Configure the usage of one shared XAML
  • Create a dummy UI in XAML with the areas
  • Create a ViewModel using the MVVM pattern
  • Use Resharper to create the properties
  • Change the setters with a regular expression at once (to fire PropertyChangedEvents)

Screencast #2 shows how to use converter classes for the binding:

Screencast #3 finally shows:

  • The core algorithm for the adaptive layout
  • A brief test

Source Code

MainPageViewModel.cs

using System;
using System.Collections.Generic;
using System.Text;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using UniLayout.Common;

namespace UniLayout.ViewModels
{
    class MainPageViewModel : BindableBase
    {
        private const int BaseFontSizeMainTitle = 42;
        private const int BaseFontSizeSubTitle = 20;

        private string _mainTitle = "Possibly Very Long Title";
        private string _upperTitle = "Upper Long Title";
        private string _lowerTitle = "Lower Area";

        private int _fontSizeMainTitle;
        private int _fontSizeSubTitle;

        private int _mainTitleHeight;
        private int _headerHeight;
        private int _subTitleHeight;
        private int _upperTitleWidth;
        private int _upperTitleHeight;
        private int _lowerTitleWidth;
        private int _lowerTitleHeight;

        public string MainTitle
        {
            get { return _mainTitle; }
            set { SetProperty(ref _mainTitle, value); }
        }

        public string UpperTitle
        {
            get { return _upperTitle; }
            set { SetProperty(ref _upperTitle, value); }
        }

        public string LowerTitle
        {
            get { return _lowerTitle; }
            set { SetProperty(ref _lowerTitle, value); }
        }

        public int FontSizeMainTitle
        {
            get { return _fontSizeMainTitle; }
            set { SetProperty(ref _fontSizeMainTitle, value); }
        }

        public int FontSizeSubTitle
        {
            get { return _fontSizeSubTitle; }
            set { SetProperty(ref _fontSizeSubTitle, value); }
        }

        public int MainTitleHeight
        {
            get { return _mainTitleHeight; }
            set { SetProperty(ref _mainTitleHeight, value); }
        }

        public int HeaderHeight
        {
            get { return _headerHeight; }
            set { SetProperty(ref _headerHeight, value); }
        }

        public int SubTitleHeight
        {
            get { return _subTitleHeight; }
            set { SetProperty(ref _subTitleHeight, value); }
        }

        public int UpperTitleWidth
        {
            get { return _upperTitleWidth; }
            set { SetProperty(ref _upperTitleWidth, value); }
        }

        public int UpperTitleHeight
        {
            get { return _upperTitleHeight; }
            set { SetProperty(ref _upperTitleHeight, value); }
        }

        public int LowerTitleWidth
        {
            get { return _lowerTitleWidth; }
            set { SetProperty(ref _lowerTitleWidth, value); }
        }

        public int LowerTitleHeight
        {
            get { return _lowerTitleHeight; }
            set { SetProperty(ref _lowerTitleHeight, value); }
        }

        public void Update()
        {
            double width = Window.Current.Bounds.Width;
            int fsMainTitle = BaseFontSizeMainTitle;
            int fsSubTitle = BaseFontSizeSubTitle;

            int h, w;
            TextBlockSizeCalc(_mainTitle, fsMainTitle, out w, out h);

            while (w > width*.8)
            {
                fsMainTitle = (int) Math.Floor(fsMainTitle*.9);
                TextBlockSizeCalc(_mainTitle, fsMainTitle, out w, out h);
            }

            FontSizeMainTitle = fsMainTitle;
            MainTitleHeight = h*2;

            double height = Window.Current.Bounds.Height;
            double lowerAreaHeight = (height - MainTitleHeight)/2;

            int w1, h1;
            int w2, h2;

            TextBlockSizeCalc(_upperTitle, fsSubTitle, out w1, out h1);
            TextBlockSizeCalc(_lowerTitle, fsSubTitle, out w2, out h2);
            w = Math.Max(w1, w2);
            while (w > lowerAreaHeight*.8)
            {
                fsSubTitle = (int) Math.Floor(fsSubTitle*.9);
                TextBlockSizeCalc(_upperTitle, fsSubTitle, out w1, out h1);
                TextBlockSizeCalc(_lowerTitle, fsSubTitle, out w2, out h2);
                w = Math.Max(w1, w2);
            }

            FontSizeSubTitle = fsSubTitle;
            UpperTitleWidth = w1;
            UpperTitleHeight = h1;
            LowerTitleWidth = w2;
            LowerTitleHeight = h2;
            SubTitleHeight = Math.Max(h1, h2)*2;
        }

        private readonly TextBlock _offscreenTextBlock = new TextBlock();
        private readonly Size _availableSize = new Size(0, 0);
        private readonly Rect _finalRect = new Rect(0, 0, 0, 0);

        private void TextBlockSizeCalc(string txt, int fontSize, out int width, out int height)
        {
            _offscreenTextBlock.Text = txt;
            _offscreenTextBlock.FontSize = fontSize;
            _offscreenTextBlock.Measure(_availableSize);
            _offscreenTextBlock.Arrange(_finalRect);
            width = (int) Math.Ceiling(_offscreenTextBlock.ActualWidth);
            height = (int) Math.Ceiling(_offscreenTextBlock.ActualHeight);
        }
    }
}

IntToGridLengthConverter.cs

using System;
using System.Collections.Generic;
using System.Text;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Data;

namespace UniLayout.Converter
{
    class IntToGridLengthConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            int v = (int) value;
            return new GridLength(v);
        }

        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            throw new NotImplementedException();
        }
    }
}

WidthToRotatedMarginConverter.cs

using System;
using System.Collections.Generic;
using System.Text;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Data;

namespace UniLayout.Converter
{
    class WidthToRotatedMarginConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            int width = (int) value;
            return new Thickness(-width/2.0, 0, -width/2.0, 0);
        }

        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            throw new NotImplementedException();
        }
    }
}

MainPage.xaml

<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:UniLayout"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:ViewModels="using:UniLayout.ViewModels"
    xmlns:converter="using:UniLayout.Converter"
    x:Class="UniLayout.MainPage"
    mc:Ignorable="d">

    <Page.DataContext>
        <ViewModels:MainPageViewModel/>
    </Page.DataContext>

    <Page.Resources>
         <converter:IntToGridLengthConverter x:Key="IntToGridLengthConverter"/>
         <converter:WidthToRotatedMarginConverter x:Key="WidthToRotatedMarginConverter"/>
    </Page.Resources>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

        <Grid.RowDefinitions>
            <RowDefinition Height="{Binding Path=MainTitleHeight, Converter={StaticResource IntToGridLengthConverter}}"/>
            <RowDefinition Height="1*"/>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="{Binding Path=SubTitleHeight, Converter={StaticResource IntToGridLengthConverter}}"/>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>

        <Border Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Background="DarkSlateGray">
            <TextBlock Text="{Binding Path=MainTitle}"
                       HorizontalAlignment="Center"
                       VerticalAlignment="Center"
                       FontSize="{Binding FontSizeMainTitle}"
                       TextAlignment="Center"/>
        </Border>

        <Border Grid.Row="1" Grid.Column="0"  Background="DarkKhaki">
            <TextBlock
                Text="{Binding Path=UpperTitle}"
                FontSize="{Binding FontSizeSubTitle}" Width="{Binding UpperTitleWidth}" Height="{Binding UpperTitleHeight}"
                Margin="{Binding Path=UpperTitleWidth, Converter={StaticResource WidthToRotatedMarginConverter}}"
                TextAlignment="Center"
                RenderTransformOrigin="0.5, 0.5">
                <TextBlock.RenderTransform>
                    <RotateTransform Angle="-90"/>
                </TextBlock.RenderTransform>
            </TextBlock>
        </Border>

        <Border Grid.Row="2" Grid.Column="0"  Background="DarkOrange">
            <TextBlock
                Text="{Binding Path=LowerTitle}"
                FontSize="{Binding FontSizeSubTitle}" Width="{Binding Path=LowerTitleWidth}" Height="{Binding Path=LowerTitleHeight}"
                Margin="{Binding Path=LowerTitleWidth, Converter={StaticResource WidthToRotatedMarginConverter}}"
                TextAlignment="Center"
                RenderTransformOrigin="0.5, 0.5">
                <TextBlock.RenderTransform>
                    <RotateTransform Angle="-90"/>
                </TextBlock.RenderTransform>
            </TextBlock>
        </Border>

        <Border Grid.Row="1" Grid.Column="1"  Background="CadetBlue"/>

        <Border Grid.Row="2" Grid.Column="1"  Background="AntiqueWhite"/>

    </Grid>
</Page>