HorusStudio.Maui.Skeleton
3.0.0
Prefix Reserved
dotnet add package HorusStudio.Maui.Skeleton --version 3.0.0
NuGet\Install-Package HorusStudio.Maui.Skeleton -Version 3.0.0
<PackageReference Include="HorusStudio.Maui.Skeleton" Version="3.0.0" />
<PackageVersion Include="HorusStudio.Maui.Skeleton" Version="3.0.0" />
<PackageReference Include="HorusStudio.Maui.Skeleton" />
paket add HorusStudio.Maui.Skeleton --version 3.0.0
#r "nuget: HorusStudio.Maui.Skeleton, 3.0.0"
#:package HorusStudio.Maui.Skeleton@3.0.0
#addin nuget:?package=HorusStudio.Maui.Skeleton&version=3.0.0
#tool nuget:?package=HorusStudio.Maui.Skeleton&version=3.0.0
Skeleton for Xamarin and MAUI apps
<img src="https://raw.githubusercontent.com/HorusSoftwareUY/Xamarin.Forms.Skeleton/master/icon.png" width="128">
The Skeleton control is a popular approach to loading content in mobile apps that provides one or more visual placeholders while content is being loaded. This technique is particularly useful for improving user experience, as it reduces perceived load times and provides a more engaging experience.
Playground | Find the animation that fits your app
Six layouts, seven animations and every property we expose. Switch between them and keep the one that belongs in your .NET MAUI project. After installing the NuGet package, you can use your own colors and apply the animations to any layout.
<img src="https://raw.githubusercontent.com/HorusSoftwareUY/Xamarin.Forms.Skeleton/master/screenshots/Skeleton.gif" width="800">
Setup
.NET MAUI
- Available on NuGet: HorusStudio.Maui.Skeleton
| Platform | Version |
|---|---|
| .NET | 8, 9 and 10 |
| Android, iOS, macOS, Windows | all versions supported by .NET MAUI |
Version 3.0.0 and later require .NET 8 or newer. Apps on .NET 6 or 7 should stay on 2.0.0, which keeps working.
Xamarin.Forms (legacy)
- Available on NuGet: Xamarin.Forms.Skeleton
Xamarin.Forms reached end of support in May 2024. This package is frozen at 2.0.0 and will not receive further updates. New work happens on the .NET MAUI package above.
| Platform | Version |
|---|---|
| Xamarin.iOS | iOS 8+ |
| Xamarin.Android | API 16+ |
| Xamarin.Forms | >= 4.0.0.425677 |
Agent skill
Rather than learning the API, you can have a coding agent apply it for you. The skill and the two ways to use it are in skills/.
Usage
You must add this namespace to your xaml files.
For .NET MAUI:
xmlns:sk="clr-namespace:Maui.Skeleton;assembly=Maui.Skeleton"
For Xamarin.Forms:
xmlns:sk="clr-namespace:Xamarin.Forms.Skeleton;assembly=Xamarin.Forms.Skeleton"
Add the following properties to generate a loading animation:
<ListView RowHeight="90"
SeparatorVisibility="None"
SelectionMode="None"
VerticalOptions="FillAndExpand"
BackgroundColor="Transparent"
ItemTemplate="{StaticResource ItemTemplate}"
ItemsSource="{Binding Items}"
sk:Skeleton.IsParent="True"
sk:Skeleton.IsBusy="{Binding IsBusy}"
sk:Skeleton.Animation="{sk:DefaultAnimation Fade}" />
Add the following properties to set a loading animation with a specific background color:
<Frame BackgroundColor="Transparent"
HasShadow="False"
CornerRadius="5"
Padding="0"
HorizontalOptions="Start"
sk:Skeleton.IsBusy="{Binding IsBusy}"
sk:Skeleton.BackgroundColor="#c6c6c5">
<Label Text="{Binding Title}"
TextColor="#000000"
FontSize="20"
FontAttributes="Bold"
HorizontalOptions="Start"/>
</Frame>
Properties
IsBusy (Boolean)
- Indicates if the control is busy in a loading state.
- The default value is false.
IsBusy is not inherited. Every element that should react needs its own
sk:Skeleton.IsBusy="{Binding ...}". Setting it once on an outer layout does nothing for the
elements inside it.
IsParent (Boolean)
- Controls whether a container covers its own content or lets each child draw itself.
- The default value is false.
The name reads backwards from what it does. IsParent="True" does not mean "handle my
children" — it means "leave my children alone, they handle themselves".
| What happens | |
|---|---|
IsParent="False" (default) |
The container hides its content and paints its own BackgroundColor. You get one solid shape. |
IsParent="True" |
The content is left untouched. Every child that should show a placeholder needs its own IsBusy. |
Either way the container still paints its own colour and runs its own animation. IsParent only
governs whether it touches what is inside.
One solid shape — no IsParent, and the container needs a colour:
<Border sk:Skeleton.IsBusy="{Binding IsBusy}"
sk:Skeleton.BackgroundColor="#c6c6c5">
<Label Text="{Binding Title}" />
</Border>
Several shapes — IsParent="True", and each child declares itself:
<VerticalStackLayout sk:Skeleton.IsParent="True"
sk:Skeleton.IsBusy="{Binding IsBusy}">
<Border sk:Skeleton.IsBusy="{Binding IsBusy}"
sk:Skeleton.BackgroundColor="#c6c6c5">
<Label Text="{Binding Title}" />
</Border>
<Border sk:Skeleton.IsBusy="{Binding IsBusy}"
sk:Skeleton.BackgroundColor="#c6c6c5">
<Label Text="{Binding Subtitle}" />
</Border>
</VerticalStackLayout>
What changed in 3.0.0. Until 2.0.0 only a Layout, such as Grid or StackLayout, hid its
content. A view holding a single piece of content painted the placeholder and left its content
showing on top of it, which is why samples and apps used Skeleton.Hide="True" on each child to get
out of the way. Those views now behave like every other container, so that workaround is no longer
needed. It still works, it is just redundant.
The test is two conditions: a View implementing IContentView, whose presented content is a
View. Border, Frame, ContentView, ScrollView, RefreshView and SwipeView are the ones
that meet both. Implementing the interface is not enough on its own: a RadioButton does, but
with its default template it presents nothing, so nothing of it fades.
If content that used to stay visible now disappears, that container is the one deciding it: set
sk:Skeleton.IsParent="True" on it to get the old behaviour back.
Three mistakes this prevents, none of which reports an error:
IsParent="True"with a child that declares nothing does nothing at all to that child. If it holds real content, the content shows straight through the loading state. If you drew your own placeholder shape there, it stays on screen after loading finishes, on top of the real content.- No
IsParentand noBackgroundColorleaves an empty hole: the content is hidden and nothing is painted in its place. IsBusyon an outer layout alone renders no placeholders at all, because it is not inherited.
BackgroundColor (Color)
- Control background color when is busy.
- The default value is the xamarin forms default color.
Hide (Boolean)
- Indicates if the control is hide when is busy.
- The default value is false.
Animation (BaseAnimation)
- Control animation when is busy.
- Possible values: None, Fade, Beat, HorizontalShake, VerticalShake, Shimmer, Aurora, Tint and custom animation inheriting from BaseAnimation.
- The default value is null, which means no animation runs.
Animation settings
The interval and the parameter are not attached properties. They are set on the
DefaultAnimation markup extension, alongside Source:
sk:Skeleton.Animation="{sk:DefaultAnimation Source=Fade, Interval=600, Parameter=0.3}"
Source is the extension's content property, so {sk:DefaultAnimation Fade} is shorthand for
{sk:DefaultAnimation Source=Fade}.
| Setting | Meaning | Default |
|---|---|---|
Source |
Which built-in animation to use. None resolves to no animation at all. |
None |
Interval |
Duration in milliseconds of each half of a cycle, so a Fade at 600 takes 1.2s per pulse. |
500 |
Parameter |
What the animation interpolates towards. See the table below. | per animation |
| Source | What moves | Parameter means |
Default |
|---|---|---|---|
Fade |
opacity | opacity to fade to | 0.6 |
Beat |
scale | scale to grow to | 1.03 |
VerticalShake |
position | offset in units, up and down | 15 |
HorizontalShake |
position | offset in units, left and right | 10 |
Shimmer |
a band of light across the placeholder | not used, see below | — |
Aurora |
a wide field of colour drifting back and forth | not used, see below | — |
Tint |
the whole placeholder washing to a colour and back | not used, see below | — |
Shimmer, Aurora and Tint
These repaint the placeholder rather than animating a property of the view, so they behave a little
differently from the other four. Shimmer sends a band of light across and off the other side;
Aurora pans a much wider field of colour back and forth, so colour is always on screen; Tint
washes the whole placeholder to a colour and back, with nothing moving at all.
- It must be attached to the element that shows the placeholder colour. It does not carry down to
children the way
FadeandBeatdo. - It does not work on
Frame.Frameis deprecated in MAUI and its renderer does not repaint when the background is replaced, so the band never moves. UseBorder. Intervalis the whole movement, not half a cycle: one pass forShimmer, out and back forAuroraandTint. 1600 is a good value for any of them.- They ignore
Parameter, and takeDirectionandSweepColorsinstead.
<Border StrokeShape="RoundRectangle 5"
StrokeThickness="0"
sk:Skeleton.IsBusy="{Binding IsBusy}"
sk:Skeleton.BackgroundColor="#c6c6c5"
sk:Skeleton.Animation="{sk:DefaultAnimation Source=Shimmer, Interval='1600', Direction='Diagonal'}" />
| Setting | Values | Default |
|---|---|---|
Direction |
Horizontal, Vertical, Diagonal, DiagonalReverse. Not used by Tint. |
Horizontal |
SweepColors |
two or three #AARRGGBB colours, comma separated and quoted |
follows the placeholder |
Left alone, the band contrasts with the placeholder automatically: light over a dark placeholder,
dark over a light one. A placeholder bound with AppThemeBinding therefore shimmers correctly in
both themes with no extra work.
Demo
MAUI
https://github.com/HorusSoftwareUY/Xamarin.Forms.Skeleton/tree/master/SkeletonSample
Xamarin.Forms:
https://github.com/HorusSoftwareUY/Xamarin.Forms.Skeleton/tree/master/SkeletonExample
Changelog
What changed in each release, including the behaviour change in 3.0.0 and how to opt out of it, is in CHANGELOG.md.
Developed by
<a href="http://horus.com.uy" ><img src="https://cdn.prod.website-files.com/64a7016392b0b7da3a8604e3/6aa9893608391fb09df87302_horus-github.png" width="128"></a>
Contributions
Contributions are welcome! If you find a bug want a feature added please report it.
If you want to contribute code please file an issue, create a branch, and file a pull request.
License
MIT License - see LICENSE.txt
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories (1)
Showing the top 1 popular GitHub repositories that depend on HorusStudio.Maui.Skeleton:
| Repository | Stars |
|---|---|
|
danielmonettelli/dotnetmaui-mitawi-weather-app-challenge
Mitawi is an open-source weather forecast application developed with .NET MAUI. With its intuitive and user-friendly interface, Mitawi provides accurate and up-to-date weather information for your current location and anywhere around the globe.
|
- Multi-targets .NET 8, 9 and 10, so each app gets a binary built against its own MAUI version.
- Requires .NET 8 or newer. Apps on .NET 6 or 7 should stay on 2.0.0, which keeps working.
- Fixes rapid flickering on devices where the system has animations turned off. The skeleton now settles into a static placeholder instead of spinning the animation loop.
- Behaviour change: a view holding a single piece of content now fades that content while busy, like every Layout already did, and Skeleton.IsParent works on it. The test is two conditions: a View implementing IContentView, whose presented content is a View. Border, Frame, ContentView, ScrollView, RefreshView and SwipeView are the ones that meet both. Implementing the interface is not enough on its own: a RadioButton does, but with its default template it presents nothing, so nothing of it fades. Children previously marked with Skeleton.Hide as a workaround keep working; the attribute is now redundant. If content that used to stay visible through the loading state now disappears, set Skeleton.IsParent="True" on that container to get the old behaviour back.
- Animations are now driven from the UI thread, and a failing animation no longer leaves a view unable to animate again.
- New Shimmer animation: a band of light that sweeps across the placeholder, with a Direction of Horizontal, Vertical, Diagonal or DiagonalReverse and optional SweepColors. Left alone the band contrasts with the placeholder colour, so it works on light and dark themes without configuration. Shimmer paints the element's own background, so it has to be attached to the element showing the placeholder and does not work on the deprecated Frame; use Border.
- New Aurora animation: a wide field of colour that drifts back and forth across the placeholder, sharing Direction and SweepColors with Shimmer. Where Shimmer sends a band across and off the other side, Aurora keeps colour on screen and only shifts it.
- New Tint animation: the whole placeholder washes to a colour and back, with nothing moving. The quietest of the three, for loading states that should stay in the background. It takes the middle colour of SweepColors, so one palette works for all three.
- Fixes placeholders that were never removed. An element with no BackgroundColor of its own kept the grey after loading finished, because the restore wrote back the null that was saved on the way in and writing null does not repaint. A Border wrapping content was the common case; it now goes back to transparent.
- Fixes a Label losing its text permanently. The same null was written back to TextColor, so a Label that never declared one stayed transparent for the rest of the session. Its colour is now read from the native control, so it hides while loading and comes back afterwards, still following the platform's light and dark colours.
- A Button that declares no TextColor of its own is left readable while loading rather than hidden, because its native colour is one per state and restoring a single one would stop a disabled button looking disabled. Give it a TextColor to have its text hidden like any other.
- Still zero dependencies.