Adaptive Layouts for Different Device Sizes in Xamarin Apps





5.00/5 (2 votes)
In this post, you will learn how to make Apps have a fluid and adaptive UI on different device screen sizes.
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
“.
<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 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:
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.
/// <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
- Add more Screen Sizes to the
_screenSizes
list. - 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
orExtraLarge
automatically without the need to maintain the_screenSizes
list. - 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