在MAUI应用开发里,当页面需要请求网络数据或者加载本地资源时,直接展示空白内容会让用户产生等待焦虑,Shimmer加载效果和骨架屏可以有效解决这个问题,通过动态闪烁的占位结构模拟内容加载过程,提升交互体验。

Shimmer效果实现原理
Shimmer效果的核心是通过渐变遮罩和位移动画实现闪烁感,整体逻辑分为三步:首先绘制占位内容的静态骨架结构,然后在骨架上方叠加一个线性渐变的半透明遮罩,最后让遮罩沿着水平方向循环移动,就形成了常见的闪烁加载效果。
自定义Shimmer控件实现
我们可以封装一个可复用的Shimmer容器控件,方便后续在多个页面中调用,控件需要支持子内容嵌套、动画速度调整和渐变颜色配置。
控件代码实现
首先创建ShimmerLayout类,继承自ContentView,实现动画逻辑:
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Shapes;
using System.Threading.Tasks;
namespace MauiShimmerDemo.Controls
{
public class ShimmerLayout : ContentView
{
// 动画持续时间,默认1.5秒
public double AnimationDuration { get; set; } = 1500;
// 渐变起始颜色
public Color StartColor { get; set; } = Color.FromArgb("#E0E0E0");
// 渐变中间高亮颜色
public Color HighlightColor { get; set; } = Color.FromArgb("#F5F5F5");
// 渐变结束颜色
public Color EndColor { get; set; } = Color.FromArgb("#E0E0E0");
private bool _isAnimating = false;
private RectangleGeometry _maskGeometry;
private LinearGradientBrush _gradientBrush;
protected override void OnHandlerChanged()
{
base.OnHandlerChanged();
InitShimmerMask();
}
private void InitShimmerMask()
{
// 创建线性渐变画刷
_gradientBrush = new LinearGradientBrush
{
StartPoint = new Point(0, 0.5),
EndPoint = new Point(1, 0.5),
GradientStops = new GradientStopCollection
{
new GradientStop(StartColor, 0),
new GradientStop(HighlightColor, 0.5),
new GradientStop(EndColor, 1)
}
};
// 创建遮罩矩形,初始位置在控件左侧外部
var maskRect = new RectangleGeometry(new Rect(-this.Width, 0, this.Width * 2, this.Height));
_maskGeometry = maskRect;
// 创建遮罩层
var maskLayer = new Rectangle
{
Fill = _gradientBrush,
Clip = _maskGeometry,
Opacity = 0.8
};
// 将遮罩层叠加到内容上方
var grid = new Grid();
if (Content != null)
{
grid.Children.Add(Content);
}
grid.Children.Add(maskLayer);
Content = grid;
}
public async Task StartAnimation()
{
if (_isAnimating || this.Width == 0) return;
_isAnimating = true;
while (_isAnimating)
{
// 重置遮罩位置到左侧
_maskGeometry.Rect = new Rect(-this.Width, 0, this.Width * 2, this.Height);
// 执行位移动画
await _maskGeometry.RectProperty.AnimateTo(
new Rect(this.Width, 0, this.Width * 2, this.Height),
(uint)AnimationDuration,
Easing.Linear
);
}
}
public void StopAnimation()
{
_isAnimating = false;
}
}
}
骨架屏页面搭建
有了ShimmerLayout控件后,我们可以搭建一个常见的列表页骨架屏,模拟列表项加载的占位效果。
页面XAML布局
在ContentPage中添加骨架屏结构和真实内容结构,通过绑定控制显示隐藏:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:MauiShimmerDemo.Controls"
x:Class="MauiShimmerDemo.MainPage"
Title="Shimmer骨架屏示例">
<ContentPage.Resources>
<!-- 骨架项模板 -->
<DataTemplate x:Key="ShimmerItemTemplate">
<Grid Padding="10" HeightRequest="80">
<!-- 头像占位 -->
<Frame WidthRequest="60" HeightRequest="60" CornerRadius="30" BackgroundColor="#E0E0E0" HorizontalOptions="Start" VerticalOptions="Center"/>
<!-- 文本占位 -->
<StackLayout Margin="70,0,0,0" VerticalOptions="Center">
<Frame HeightRequest="20" BackgroundColor="#E0E0E0" CornerRadius="4" WidthRequest="200"/>
<Frame HeightRequest="16" BackgroundColor="#E0E0E0" CornerRadius="4" WidthRequest="150" Margin="0,10,0,0"/>
</StackLayout>
</Grid>
</DataTemplate>
</ContentPage.Resources>
<Grid>
<!-- 骨架屏区域 -->
<controls:ShimmerLayout x:Name="ShimmerContainer" IsVisible="{Binding IsLoading}">
<ListView ItemTemplate="{StaticResource ShimmerItemTemplate}" ItemsSource="{Binding ShimmerItems}">
<ListView.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>占位</x:String>
<x:String>占位</x:String>
<x:String>占位</x:String>
<x:String>占位</x:String>
<x:String>占位</x:String>
</x:Array>
</ListView.ItemsSource>
</ListView>
</controls:ShimmerLayout>
<!-- 真实内容区域 -->
<ListView ItemsSource="{Binding RealItems}" IsVisible="{Binding IsLoading, Converter={StaticResource InverseBoolConverter}}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Padding="10" HeightRequest="80">
<Image Source="{Binding Avatar}" WidthRequest="60" HeightRequest="60" HorizontalOptions="Start" VerticalOptions="Center"/>
<StackLayout Margin="70,0,0,0" VerticalOptions="Center">
<Label Text="{Binding Name}" FontSize="16" FontAttributes="Bold"/>
<Label Text="{Binding Desc}" FontSize="14" TextColor="Gray" Margin="0,10,0,0"/>
</StackLayout>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</ContentPage>
页面后台逻辑
在页面后台代码中控制加载状态,模拟数据请求过程:
using Microsoft.Maui.Controls;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
namespace MauiShimmerDemo
{
public partial class MainPage : ContentPage
{
public ObservableCollection ShimmerItems { get; set; }
public ObservableCollection RealItems { get; set; }
public bool IsLoading { get; set; } = true;
public MainPage()
{
InitializeComponent();
ShimmerItems = new ObservableCollection();
RealItems = new ObservableCollection();
// 初始化占位数据
for (int i = 0; i < 5; i++)
{
ShimmerItems.Add("占位");
}
BindingContext = this;
}
protected override async void OnAppearing()
{
base.OnAppearing();
// 启动Shimmer动画
await ShimmerContainer.StartAnimation();
// 模拟2秒数据请求
await Task.Delay(2000);
// 加载真实数据
LoadRealData();
// 停止动画,切换显示真实内容
ShimmerContainer.StopAnimation();
IsLoading = false;
OnPropertyChanged(nameof(IsLoading));
}
private void LoadRealData()
{
RealItems.Add(new UserModel { Avatar = "https://ipipp.com/avatar1.png", Name = "张三", Desc = "这是第一条用户描述信息" });
RealItems.Add(new UserModel { Avatar = "https://ipipp.com/avatar2.png", Name = "李四", Desc = "这是第二条用户描述信息" });
RealItems.Add(new UserModel { Avatar = "https://ipipp.com/avatar3.png", Name = "王五", Desc = "这是第三条用户描述信息" });
}
}
public class UserModel
{
public string Avatar { get; set; }
public string Name { get; set; }
public string Desc { get; set; }
}
}
注意事项
- Shimmer动画的渐变颜色需要和骨架占位颜色保持协调,避免闪烁效果过于突兀
- 页面销毁时需要手动调用StopAnimation方法停止动画,避免后台无效循环消耗性能
- 如果页面有横竖屏切换场景,需要在布局尺寸变化时重新初始化遮罩的几何尺寸,保证动画效果正常
- 骨架屏的占位结构需要和真实内容的布局结构保持一致,避免内容加载完成后出现明显的布局跳动
MAUIShimmer加载效果骨架屏ContentPage修改时间:2026-06-18 08:48:43