Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Xamarin

Adaptive Layouts for Different Device Sizes in Xamarin Apps

5.00/5 (2 votes)
6 Jan 2021CPOL2 min read 8.5K  
In this post, you will learn how to make Apps have a fluid and adaptive UI on different device screen sizes.

Image 1

Ok, let's admit, Xamarin.forms has no built-in mechanisms for making our Apps look great and adapt based on different screen sizes.

In this post, I will show you the approach I use to make my Apps to have a fluid and adaptive UI on different device screen sizes.

The Problem

The most recurrent problem is related to label font sizes. Font sizes in Xamarin are fixed. No matter if you have a device with small screen size like a Samsung Galaxy S, or a big one like an Apple iPad Pro 12.9, Xamarin forces you to use the same font size.

We have the same problem when dealing other Views (UI controls).

Obviously, you can use Bindable Properties to set values on different font sizes based on the device size, but besides not being performant and by making our code become messy, it is not an elegant solution.

The Solution

Inspired by this article written by Charlin Agramonte, and by the OnPlatform Markup extension, I decided to create my own markup extension called “OnScreenSize“.

XML
<markups:OnScreenSize
                      DefaultSize="Micro"
                      ExtraSmall="11"
                      Small="14"
                      Medium="16"
                      Large="19"
                      ExtraLarge="24" />

By using my markup, we are able to define different values for different screen sizes without the need to have a bunch of styling tags (or having as little as possible).

It works not only for Labels but also for any kind of Xamarin forms UI elements like Grid, Image, Entry, Frame, Picker, and so forth.

XML
<?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:markups="clr-namespace:TheNextLoop.Markups;assembly=TheNextLoop.Markups"
             x:Class="OnScreenSize.Samples.MainPage" Padding="0, 20, 0, 0" >

    <Grid Margin="8, 0, 8, 0">
        <Grid.RowDefinitions>
            <RowDefinition Height="{markups:OnScreenSize DefaultSize='60', 
             ExtraSmall='7', Small='8', Medium='60', Large='10', ExtraLarge='13'}" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        
            <Label Text="List of Animals" Grid.Row="0" TextColor="Black" 
             HorizontalOptions="CenterAndExpand" FontSize="Body" 
             VerticalOptions="CenterAndExpand"/>
          
            <CollectionView   Grid.Row="1" 
                              ItemsSource="{Binding Animals}" 
                              IsGrouped="true">
                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <Grid Padding="10">
                            <Grid.RowDefinitions>
                                <RowDefinition Height=
                                "{markups:OnScreenSize DefaultSize='Auto', 
                                Medium='30', ExtraLarge='Auto'}" />
                                <RowDefinition Height="{markups:OnScreenSize 
                                DefaultSize='Auto', Medium='30', ExtraLarge='Auto'}" />
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto" />
                                <ColumnDefinition Width="Auto" />
                            </Grid.ColumnDefinitions>

                            <Image Grid.RowSpan="2" 
                                   Source="{Binding ImageUrl}" 
                                   Aspect="AspectFill"
                                   HeightRequest="{markups:OnScreenSize DefaultSize='30', 
                                   Medium='60', ExtraLarge='120'}" 
                                   WidthRequest="{markups:OnScreenSize DefaultSize='30', 
                                   Medium='60', ExtraLarge='120'}" />

                            <Label Grid.Column="1"
                                   FontSize="{markups:OnScreenSize DefaultSize='12', 
                                   Medium='20',  ExtraLarge='40'}"
                                   Text="{Binding Name}" 
                                   FontAttributes="Bold" />

                            <Label Grid.Row="1"
                                   Grid.Column="1"
                                   FontSize="{markups:OnScreenSize DefaultSize='9', 
                                   Medium='18', ExtraLarge='35'}"
                                   Text="{Binding Location}"
                                   FontAttributes="Italic" 
                                   VerticalOptions="End" />
                        </Grid>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
                <CollectionView.GroupHeaderTemplate>
                    <DataTemplate>
                        <Label Text="{Binding Name}"
                               FontSize="{markups:OnScreenSize DefaultSize='20', 
                               Medium='Large',  ExtraLarge='45'}"
                               BackgroundColor="LightGray"
                               FontAttributes="Bold" />
                    </DataTemplate>
                </CollectionView.GroupHeaderTemplate>
                <CollectionView.GroupFooterTemplate>
                    <DataTemplate>
                        <Label Text="{Binding Count, StringFormat='Total animals: {0:D}'}"
                               FontSize="{markups:OnScreenSize DefaultSize='12', 
                               Medium='15', Large='10', ExtraLarge='30'}"
                               FontAttributes="Bold"
                               Margin="0,0,0,10" />
                    </DataTemplate>
                </CollectionView.GroupFooterTemplate>
            </CollectionView>
    </Grid>
 
</ContentPage>

How It Works Under the Hood

In order to make it easy to measure the screen sizes, I decided to use the DeviceDisplay class from Xamarin Essentials.

The OnScreenSize Markup has many properties where you can set values according to the screen size:

  • ExtraSmall
  • Small
  • Medium
  • Large
  • ExtraLarge

It also has a defaultSize property which serves to indicate which of the above properties should be used in case the screen size of the device is not available in the _screenSizes list, below:

C#
private static List<ScreenInfo> _screenSizes = new List<ScreenInfo>
      {
          { new ScreenInfo(480,800, eScreenSizes.ExtraSmall)},   //Samsung Galaxy S,
          { new ScreenInfo(720,1280, eScreenSizes.Small)},       //Nesus S
          { new ScreenInfo(828,1792, eScreenSizes.Medium)},      //iphone 11
          { new ScreenInfo(1284,2778, eScreenSizes.Large)},      //Apple iPhone 12 Pro Max
          { new ScreenInfo(1440,3200, eScreenSizes.ExtraLarge)}, //Samsung Galaxy S20+
          { new ScreenInfo(2732,2048, eScreenSizes.ExtraLarge)}, //Apple iPad Pro 12.9
      };

Below is the full source code of OnScreenSize class.

The magic to get the correct value for the current screen size happens on the GetValue() method. It attempts to get the Screen size that match the list _screenSizes and once it finds a match, it gets the value on the corresponding property for that Screen Size.

C#
/// <summary>
 /// Markup Xaml para definir valores  dependendo do tamanho da tela do celular do usuario.
 /// </summary>
 public class OnScreenSize : IMarkupExtension
 {
     private static List<ScreenInfo> _screenSizes = new List<ScreenInfo>
     {
         { new ScreenInfo(480,800, eScreenSizes.ExtraSmall)},    //Samsung Galaxy S,
         { new ScreenInfo(720,1280, eScreenSizes.Small)},        //Nesus S
         { new ScreenInfo(828,1792, eScreenSizes.Medium)},       //iphone 11
         { new ScreenInfo(1284,2778, eScreenSizes.Large)},       //Apple iPhone 12 Pro Max
         { new ScreenInfo(1440,3200, eScreenSizes.ExtraLarge)},  //Samsung Galaxy S20+
         { new ScreenInfo(2732,2048, eScreenSizes.ExtraLarge)},  //Apple iPad Pro 12.9
     };

     private Dictionary<eScreenSizes, object> _values =
                                      new Dictionary<eScreenSizes, object>() {
         { eScreenSizes.ExtraSmall, null},
         { eScreenSizes.Small, null},
         { eScreenSizes.Medium,  null},
         { eScreenSizes.Large,  null},
         { eScreenSizes.ExtraLarge,  null},
     };

     public OnScreenSize()
     {
     }

     /// <summary>
     /// Screen-size do device.
     /// </summary>
     private static eScreenSizes? deviceScreenSize;

     /// <summary>
     /// Tamanho-padrao na tela que deve ser assumido quando não for
     /// possivel determinar o tamanho dela com base na lista <see cref="_screenSizes"/>
     /// </summary>
     public object DefaultSize { get; set; }

     public object ExtraSmall
     {
         get
         {
             return _values[eScreenSizes.ExtraSmall];
         }
         set
         {
             _values[eScreenSizes.ExtraSmall] = value;
         }
     }

     public object Small
     {
         get
         {
             return _values[eScreenSizes.Small];
         }
         set
         {
             _values[eScreenSizes.Small] = value;
         }
     }
     public object Medium
     {
         get
         {
             return _values[eScreenSizes.Medium];
         }
         set
         {
             _values[eScreenSizes.Medium] = value;
         }
     }

     public object Large
     {
         get
         {
             return _values[eScreenSizes.Large];
         }
         set
         {
             _values[eScreenSizes.Large] = value;
         }
     }

     public object ExtraLarge
     {
         get
         {
             return _values[eScreenSizes.ExtraLarge];
         }
         set
         {
             _values[eScreenSizes.ExtraLarge] = value;
         }
     }

     public object ProvideValue(IServiceProvider serviceProvider)
     {
         var valueProvider = serviceProvider?.GetService<IProvideValueTarget>() ??
                             throw new ArgumentException();

         BindableProperty bp;
         PropertyInfo pi = null;
         Type propertyType = null;

         if (valueProvider.TargetObject is Setter setter)
         {
             bp = setter.Property;
         }
         else
         {
             bp = valueProvider.TargetProperty as BindableProperty;
             pi = valueProvider.TargetProperty as PropertyInfo;
         }

         propertyType = bp?.ReturnType ?? pi?.PropertyType ??
                        throw new InvalidOperationException
                        ("Não foi posivel determinar a propriedade para fornecer o valor.");

         var value = GetValue(serviceProvider);

         return value.ConvertTo(propertyType, bp);
     }

     private object GetValue(IServiceProvider serviceProvider)
     {
         var screenSize = GetScreenSize();
         if (screenSize != eScreenSizes.NotSet)
         {
             if (_values[screenSize] != null)
             {
                 return _values[screenSize];
             }
         }

         if (DefaultSize == null)
         {
             throw new XamlParseException("OnScreenExtension requires a DefaultSize set.");
         }
         else
         {
             return DefaultSize;
         }
     }

     private eScreenSizes GetScreenSize()
     {
         if (TryGetScreenSize(out var screenSize))
         {
             return screenSize;
         }

         return  eScreenSizes.NotSet;
     }

     private static bool TryGetScreenSize(out eScreenSizes screenSize)
     {
         if (deviceScreenSize != null)
         {
             if (deviceScreenSize.Value == eScreenSizes.NotSet)
             {
                 screenSize = deviceScreenSize.Value;
                 return false;
             }
             else
             {
                 screenSize = deviceScreenSize.Value;
                 return true;
             }
         }

         var device = DeviceDisplay.MainDisplayInfo;

         var deviceWidth = device.Width;
         var deviceHeight = device.Height;

         if (Xamarin.Essentials.DeviceInfo.Idiom == Xamarin.Essentials.DeviceIdiom.Tablet)
         {
             deviceWidth = Math.Max(device.Width, device.Height);
             deviceHeight = Math.Min(device.Width, device.Height);
         }

         foreach (var sizeInfo in _screenSizes)
         {
             if (deviceWidth <= sizeInfo.Width &&
                 deviceHeight <= sizeInfo.Height)
             {
                 deviceScreenSize = sizeInfo.ScreenSize;
                 screenSize = deviceScreenSize.Value;
                 return true;
             }
         }

         deviceScreenSize = eScreenSizes.NotSet;
         screenSize = deviceScreenSize.Value;
         return false;
     }
 }

Future Improvements

  1. Add more Screen Sizes to the _screenSizes list.
  2. Implement a way to auto classify the screen size. So that, based on the screen size, the code can classify as Small, ExtraSmall, Medium, Large or ExtraLarge automatically without the need to maintain the _screenSizes list.
  3. Improve type conversions.

That's it everyone, you can get a sample App and the full source code here.

History

  • 6th January, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)