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.
="1.0"="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)},
{ new ScreenInfo(720,1280, eScreenSizes.Small)},
{ new ScreenInfo(828,1792, eScreenSizes.Medium)},
{ new ScreenInfo(1284,2778, eScreenSizes.Large)},
{ new ScreenInfo(1440,3200, eScreenSizes.ExtraLarge)},
{ new ScreenInfo(2732,2048, eScreenSizes.ExtraLarge)},
};
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.
public class OnScreenSize : IMarkupExtension
{
private static List<ScreenInfo> _screenSizes = new List<ScreenInfo>
{
{ new ScreenInfo(480,800, eScreenSizes.ExtraSmall)},
{ new ScreenInfo(720,1280, eScreenSizes.Small)},
{ new ScreenInfo(828,1792, eScreenSizes.Medium)},
{ new ScreenInfo(1284,2778, eScreenSizes.Large)},
{ new ScreenInfo(1440,3200, eScreenSizes.ExtraLarge)},
{ new ScreenInfo(2732,2048, eScreenSizes.ExtraLarge)},
};
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()
{
}
private static eScreenSizes? deviceScreenSize;
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
or ExtraLarge
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