本章导读:如果说布局和控件是应用的骨架,那么图形和动画就是它的皮肤和表情。一个静态的界面就像一张照片——准确但缺乏生气;而一个有动画的界面就像一段视频——生动、流畅、有情感。本章将带你进入 XAML 图形与动画的奇妙世界,让你的应用从"能用"进化到"令人愉悦"。
在传统的 Web 开发中,我们习惯使用位图图片(PNG、JPG)来展示图形。但在现代 UI 框架中,矢量图形才是主角。矢量图形用数学公式描述形状,无论放大多少倍都保持清晰,而且文件体积通常更小。
Uno Platform 完整实现了 WinUI 的 Shapes 命名空间,提供了多种基础形状控件。
Rectangle(矩形) 是最常用的形状,常用于创建带圆角的背景:
<Rectangle Fill="LightBlue"
Stroke="Blue"
StrokeThickness="2"
RadiusX="8" RadiusY="8"
Width="200" Height="100" />
Ellipse(椭圆) 可以绘制圆形或椭圆形:
<!-- 圆形状态指示器 -->
<Ellipse Fill="Green" Width="12" Height="12" />
<!-- 椭圆形头像框 -->
<Ellipse Width="80" Height="80">
<Ellipse.Fill>
<ImageBrush ImageSource="avatar.jpg" Stretch="UniformToFill" />
</Ellipse.Fill>
</Ellipse>
Line(直线) 用于绘制线段:
<Line X1="0" Y1="0" X2="100" Y2="100"
Stroke="Black" StrokeThickness="2" />
Polygon(多边形) 用于绘制闭合的多边形:
<!-- 六边形 -->
<Polygon Points="50,0 100,25 100,75 50,100 0,75 0,25"
Fill="Purple" Stroke="DarkPurple" StrokeThickness="2" />
Polyline(折线) 用于绘制连续但不闭合的线段:
<!-- 简单的折线图 -->
<Polyline Points="0,100 30,80 60,90 90,40 120,50 150,20"
Stroke="Blue" StrokeThickness="3" />
第一性原理:为什么 Shape 是控件?
在 XAML 中,Shape 继承自 FrameworkElement,这意味着它是一个完整的控件,可以参与布局系统、响应事件、应用变换。这与其他框架将 Shape 视为简单的绘图原语不同。这种设计带来了极大的灵活性——你可以将 Shape 放在任何容器中,绑定它的属性,甚至在 Shape 上处理鼠标事件。
Path 是所有形状中最强大的,它可以绘制任意复杂的几何图形。Data 属性接受一个"路径标记语法"字符串,这种语法源自 SVG 标准。
<!-- 简单的三角形 -->
<Path Data="M 0,0 L 100,0 L 50,100 Z"
Fill="Red" />
<!-- 心形 -->
<Path Data="M 50,10 C 20,-10 0,20 50,60 C 100,20 80,-10 50,10"
Fill="Pink" Stroke="Red" StrokeThickness="2" />
<!-- 复杂的图标路径 -->
<Path Data="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
Fill="Blue" />
路径标记语法速览:
| 命令 | 含义 | 示例 |
|---|---|---|
| M | 移动到起点 | M 10,10 |
| L | 直线到终点 | L 100,100 |
| C | 三次贝塞尔曲线 | C 20,0 80,100 100,50 |
| Q | 二次贝塞尔曲线 | Q 50,0 100,100 |
| A | 弧线 | A 50,50 0 0 1 100,100 |
| Z | 闭合路径 | Z |
费曼技巧提问:什么时候用 Path 而不是其他 Shape? 简单的形状(矩形、圆形)用对应的 Shape 控件更直观。但当形状不符合这些基本类型时,Path 就是你的选择。大多数图标库(如 Material Icons、Font Awesome)都提供 SVG 路径数据,你可以直接复制到 Path 中使用。
如果你需要在多个地方使用相同的几何形状,可以将它定义为资源:
<Page.Resources>
<!-- 定义可复用的几何图形 -->
<PathGeometry x:Key="StarGeometry">
<PathGeometry.Figures>
<PathFigure StartPoint="50,0">
<PolyLineSegment Points="61,35 98,35 68,57 79,91 50,70 21,91 32,57 2,35 39,35" />
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</Page.Resources>
<!-- 在多个 Path 中复用 -->
<Path Data="{StaticResource StarGeometry}" Fill="Gold" />
<Path Data="{StaticResource StarGeometry}" Fill="Gray" Stretch="Uniform" Width="24" Height="24" />
画刷(Brush)决定了图形和控件如何被"填充"。XAML 提供了多种画刷类型,从简单的单色到复杂的渐变和图像。
最基础的画刷,填充单一颜色:
<!-- 使用颜色名称 -->
<Button Background="Red" />
<!-- 使用十六进制颜色值 -->
<Button Background="#FF0000" />
<!-- 使用 ARGB 格式(带透明度) -->
<Button Background="#80FF0000" />
<!-- 使用画刷对象 -->
<Button>
<Button.Background>
<SolidColorBrush Color="Red" Opacity="0.5" />
</Button.Background>
</Button>
线性渐变沿一条直线方向过渡颜色:
<Border Width="200" Height="100" CornerRadius="8">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#667eea" Offset="0" />
<GradientStop Color="#764ba2" Offset="1" />
</LinearGradientBrush>
</Border.Background>
</Border>
StartPoint 和 EndPoint 使用相对坐标(0-1),表示渐变的方向。(0,0) 是左上角,(1,1) 是右下角。
多色渐变可以添加多个 GradientStop:
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="Red" Offset="0" />
<GradientStop Color="Yellow" Offset="0.33" />
<GradientStop Color="Green" Offset="0.66" />
<GradientStop Color="Blue" Offset="1" />
</LinearGradientBrush>
径向渐变从一个中心点向外辐射:
<Ellipse Width="200" Height="200">
<Ellipse.Fill>
<RadialGradientBrush Center="0.5,0.5" RadiusX="0.5" RadiusY="0.5">
<GradientStop Color="White" Offset="0" />
<GradientStop Color="Black" Offset="1" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
用图像作为填充内容:
<Border Width="300" Height="200" CornerRadius="12">
<Border.Background>
<ImageBrush ImageSource="background.jpg"
Stretch="UniformToFill"
AlignmentX="Center"
AlignmentY="Center" />
</Border.Background>
</Border>
<!-- 文字填充图像 -->
<TextBlock Text="Hello" FontSize="72" FontWeight="Bold">
<TextBlock.Foreground>
<ImageBrush ImageSource="texture.jpg" />
</TextBlock.Foreground>
</TextBlock>
WinUI 3 提供了现代的毛玻璃效果画刷:
<Border Width="400" Height="300">
<Border.Background>
<AcrylicBrush TintColor="LightGray"
TintOpacity="0.5"
FallbackColor="Gray" />
</Border.Background>
</Border>
技术细节:AcrylicBrush 的性能考量 毛玻璃效果需要实时采样背景内容,在低端设备上可能影响性能。TintColor设置基底颜色,TintOpacity控制透明度,FallbackColor是在不支持毛玻璃的设备上使用的备用颜色。
变换(Transform)允许你在不影响布局的情况下改变元素的视觉呈现。这对于动画和特效至关重要。
RotateTransform:绕点旋转:
<Image Source="arrow.png" Width="50">
<Image.RenderTransform>
<RotateTransform Angle="45" CenterX="25" CenterY="25" />
</Image.RenderTransform>
</Image>
ScaleTransform:缩放:
<Button Content="放大" Width="100" Height="40">
<Button.RenderTransform>
<ScaleTransform ScaleX="1.2" ScaleY="1.2" />
</Button.RenderTransform>
</Button>
TranslateTransform:位移:
<Border Width="100" Height="100" Background="Blue">
<Border.RenderTransform>
<TranslateTransform X="50" Y="20" />
</Border.RenderTransform>
</Border>
SkewTransform:倾斜:
<TextBlock Text="斜体效果" FontSize="24">
<TextBlock.RenderTransform>
<SkewTransform AngleX="-15" />
</TextBlock.RenderTransform>
</TextBlock>
TransformGroup 允许组合多个变换:
<Button Content="组合变换">
<Button.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1.5" ScaleY="1.5" />
<RotateTransform Angle="30" />
</TransformGroup>
</Button.RenderTransform>
</Button>
重要区别:RenderTransform vs LayoutTransform XAML 中有两种变换:RenderTransform和LayoutTransform。大多数情况下使用
RenderTransform只影响视觉呈现,不影响布局测量。元素在布局中仍占据原来的位置和空间。LayoutTransform会影响布局,其他元素会为变换后的元素让出空间。RenderTransform,因为它性能更好,不会触发布局重计算。
动画是让界面"活起来"的关键。XAML 提供了强大的声明式动画系统。
Storyboard 是动画的容器,它管理一个或多个动画的时间线:
<Page.Resources>
<!-- 淡入动画 -->
<Storyboard x:Name="FadeInStoryboard">
<DoubleAnimation
Storyboard.TargetName="MyImage"
Storyboard.TargetProperty="Opacity"
From="0" To="1"
Duration="0:0:0.5" />
</Storyboard>
<!-- 淡出动画 -->
<Storyboard x:Name="FadeOutStoryboard">
<DoubleAnimation
Storyboard.TargetName="MyImage"
Storyboard.TargetProperty="Opacity"
From="1" To="0"
Duration="0:0:0.3" />
</Storyboard>
</Page.Resources>
<Image x:Name="MyImage" Source="photo.jpg" Opacity="0" />
在代码中启动动画:
private void ShowImage()
{
FadeInStoryboard.Begin();
}
private void HideImage()
{
FadeOutStoryboard.Begin();
}
DoubleAnimation:动画化 double 类型属性(Opacity、Width、Angle 等):
<DoubleAnimation
Storyboard.TargetName="MyElement"
Storyboard.TargetProperty="Width"
From="100" To="200"
Duration="0:0:1"
AutoReverse="True"
RepeatBehavior="Forever" />
ColorAnimation:动画化颜色:
<ColorAnimation
Storyboard.TargetName="MyBrush"
Storyboard.TargetProperty="Color"
From="Red" To="Blue"
Duration="0:0:2" />
PointAnimation:动画化点坐标:
<PointAnimation
Storyboard.TargetName="MyEllipse"
Storyboard.TargetProperty="Center"
From="0,0" To="100,100"
Duration="0:0:1" />
缓动函数让动画更自然,模拟现实世界的物理特性:
<DoubleAnimation From="0" To="300" Duration="0:0:2">
<DoubleAnimation.EasingFunction>
<BounceEase Bounces="3" Bounciness="2" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
常用缓动函数:
| 缓动函数 | 效果 | 适用场景 |
|---|---|---|
LinearEase | 匀速 | 进度条 |
QuadraticEase | 加速/减速 | 一般过渡 |
CubicEase | 更明显的加速/减速 | 强调动作 |
BounceEase | 弹跳 | 活泼的效果 |
ElasticEase | 弹簧 | 自然感觉 |
BackEase | 回退 | 超出再回来 |
关键帧动画允许在时间线上定义多个状态:
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="MyElement"
Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame Value="0" KeyTime="0:0:0" />
<LinearDoubleKeyFrame Value="1" KeyTime="0:0:0.5" />
<LinearDoubleKeyFrame Value="0.5" KeyTime="0:0:1" />
<LinearDoubleKeyFrame Value="1" KeyTime="0:0:1.5" />
</DoubleAnimationUsingKeyFrames>
不同类型的关键帧提供不同的插值方式:
<DoubleAnimationUsingKeyFrames>
<!-- 线性插值 -->
<LinearDoubleKeyFrame Value="100" KeyTime="0:0:0.5" />
<!-- 样条曲线(贝塞尔) -->
<SplineDoubleKeyFrame Value="200" KeyTime="0:0:1" KeySpline="0.5,0 0.5,1" />
<!-- 离散(瞬间跳变) -->
<DiscreteDoubleKeyFrame Value="300" KeyTime="0:0:1.5" />
<!-- 带缓动函数 -->
<EasingDoubleKeyFrame Value="400" KeyTime="0:0:2">
<EasingDoubleKeyFrame.EasingFunction>
<BounceEase />
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
对于需要 60fps 流畅度的复杂动画,传统 Storyboard 可能力不从心。WinUI 提供了 Composition API,它在独立的合成线程上运行,直接操作 GPU。
// 获取元素的 Visual
var visual = ElementCompositionPreview.GetElementVisual(MyElement);
// 创建 Compositor
var compositor = visual.Compositor;
// 创建动画
var animation = compositor.CreateScalarKeyFrameAnimation();
animation.InsertKeyFrame(0, 0);
animation.InsertKeyFrame(1, 360);
animation.Duration = TimeSpan.FromSeconds(2);
// 应用动画到 RotationAngle 属性
visual.StartAnimation("RotationAngle", animation);
Composition API 支持基于表达式的动画,可以让一个属性根据其他属性动态计算:
var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
// 创建一个"跟随鼠标"的表达式动画
var pointerPosition = compositor.CreatePointerPositionPropertySet();
var expression = compositor.CreateExpressionAnimation(
"Vector3(target.Position.X - 50, target.Position.Y - 50, 0)"
);
expression.SetReferenceParameter("target", pointerPosition);
var myVisual = ElementCompositionPreview.GetElementVisual(MyElement);
myVisual.StartAnimation("Offset", expression);
Composition API 提供了基于物理的自然动画:
// 弹簧动画
var springAnimation = compositor.CreateSpringScalarAnimation();
springAnimation.Period = TimeSpan.FromMilliseconds(50);
springAnimation.DampingRatio = 0.5f;
springAnimation.FinalValue = 100f;
visual.StartAnimation("Offset.X", springAnimation);
何时使用 Composition API:对于简单的 UI 状态过渡,传统 Storyboard 仍然是更简单的选择。
- 需要高性能、60fps 的动画
- 需要基于物理的自然动画
- 需要动画响应实时输入(如鼠标位置)
- 需要同时动画大量元素
Lottie 是 Airbnb 开发的一种基于 JSON 的矢量动画格式,设计师可以在 After Effects 中制作动画,导出为 Lottie 格式,然后在应用中播放。
首先安装 Uno.WinUI.Lottie NuGet 包。
xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
xmlns:lottie="using:Uno.UI.Lottie"
<AnimatedVisualPlayer x:Name="LottiePlayer"
AutoPlay="True"
Speed="1.0">
<lottie:LottieVisualSource UriSource="ms-appx:///Assets/success.json" />
</AnimatedVisualPlayer>
// 播放
await LottiePlayer.PlayAsync(0, 1, looped: false);
// 暂停
LottiePlayer.Pause();
// 停止
LottiePlayer.Stop();
// 设置播放速度(负数反向播放)
LottiePlayer.Speed = 0.5;
// 设置进度(0-1)
LottiePlayer.SetProgress(0.5);
<AnimatedVisualPlayer x:Name="LoadingPlayer">
<lottie:LottieVisualSource UriSource="ms-appx:///Assets/loading.json" />
</AnimatedVisualPlayer>
<Button Content="{x:Bind ViewModel.IsLoading, Converter={StaticResource BoolToVisibility}}" />
// 当加载状态改变时控制动画
public bool IsLoading
{
get => _isLoading;
set
{
if (SetProperty(ref _isLoading, value))
{
if (value)
{
LoadingPlayer.PlayAsync(0, 1, looped: true);
}
else
{
LoadingPlayer.Stop();
}
}
}
}
图形和动画是让应用从"能用"变成"令人愉悦"的关键因素。本章我们学习了 XAML 的矢量图形系统——从基础的矩形和椭圆到强大的 Path 控件,你可以创建任意复杂的形状。
画刷系统提供了丰富的填充方式:纯色、渐变、图像,以及现代的毛玻璃效果。变换让你能够旋转、缩放、位移元素,为动画奠定基础。
动画系统分为两个层次:传统 Storyboard 适合大多数 UI 状态过渡,Composition API 则为高性能场景提供了更强大的能力。Lottie 的集成让设计师制作的复杂动画可以轻松集成到应用中。
在下一章中,我们将从视觉层转向硬件层——学习如何访问设备硬件与原生 API,包括相机、传感器、地理位置等跨平台能力。
动手实验:
- 使用 Path 绘制一个自定义图标(如五角星或心形),并应用渐变填充。
- 创建一个按钮悬停动画:鼠标悬停时按钮放大 1.1 倍并微微旋转,离开时恢复。
- 实现一个加载动画:三个圆点依次上下跳动,使用 Storyboard 和关键帧动画。
- (进阶)使用 Composition API 创建一个跟随手指/鼠标移动的元素,带有自然的弹性效果。
还没有人回复