# 第九章:图形与动画:WinUI 的表现力
> **本章导读**:如果说布局和控件是应用的骨架,那么图形和动画就是它的皮肤和表情。一个静态的界面就像一张照片——准确但缺乏生气;而一个有动画的界面就像一段视频——生动、流畅、有情感。本章将带你进入 XAML 图形与动画的奇妙世界,让你的应用从"能用"进化到"令人愉悦"。
---
## 🔷 9.1 矢量图形:构建界面的几何原子
在传统的 Web 开发中,我们习惯使用位图图片(PNG、JPG)来展示图形。但在现代 UI 框架中,**矢量图形**才是主角。矢量图形用数学公式描述形状,无论放大多少倍都保持清晰,而且文件体积通常更小。
### 📐 9.1.1 基础形状控件
Uno Platform 完整实现了 WinUI 的 `Shapes` 命名空间,提供了多种基础形状控件。
**Rectangle(矩形)** 是最常用的形状,常用于创建带圆角的背景:
```xml
<Rectangle Fill="LightBlue"
Stroke="Blue"
StrokeThickness="2"
RadiusX="8" RadiusY="8"
Width="200" Height="100" />
```
**Ellipse(椭圆)** 可以绘制圆形或椭圆形:
```xml
<!-- 圆形状态指示器 -->
<Ellipse Fill="Green" Width="12" Height="12" />
<!-- 椭圆形头像框 -->
<Ellipse Width="80" Height="80">
<Ellipse.Fill>
<ImageBrush ImageSource="avatar.jpg" Stretch="UniformToFill" />
</Ellipse.Fill>
</Ellipse>
```
**Line(直线)** 用于绘制线段:
```xml
<Line X1="0" Y1="0" X2="100" Y2="100"
Stroke="Black" StrokeThickness="2" />
```
**Polygon(多边形)** 用于绘制闭合的多边形:
```xml
<!-- 六边形 -->
<Polygon Points="50,0 100,25 100,75 50,100 0,75 0,25"
Fill="Purple" Stroke="DarkPurple" StrokeThickness="2" />
```
**Polyline(折线)** 用于绘制连续但不闭合的线段:
```xml
<!-- 简单的折线图 -->
<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 上处理鼠标事件。
### 🛤️ 9.1.2 Path:终极绘图工具
`Path` 是所有形状中最强大的,它可以绘制任意复杂的几何图形。`Data` 属性接受一个"路径标记语法"字符串,这种语法源自 SVG 标准。
```xml
<!-- 简单的三角形 -->
<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 中使用。
### 🧩 9.1.3 Geometry:可复用的几何定义
如果你需要在多个地方使用相同的几何形状,可以将它定义为资源:
```xml
<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" />
```
---
## 🎨 9.2 画刷系统:色彩的无限可能
画刷(Brush)决定了图形和控件如何被"填充"。XAML 提供了多种画刷类型,从简单的单色到复杂的渐变和图像。
### 🎯 9.2.1 SolidColorBrush:纯色填充
最基础的画刷,填充单一颜色:
```xml
<!-- 使用颜色名称 -->
<Button Background="Red" />
<!-- 使用十六进制颜色值 -->
<Button Background="#FF0000" />
<!-- 使用 ARGB 格式(带透明度) -->
<Button Background="#80FF0000" />
<!-- 使用画刷对象 -->
<Button>
<Button.Background>
<SolidColorBrush Color="Red" Opacity="0.5" />
</Button.Background>
</Button>
```
### 🌈 9.2.2 LinearGradientBrush:线性渐变
线性渐变沿一条直线方向过渡颜色:
```xml
<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`:
```xml
<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>
```
### 🌀 9.2.3 RadialGradientBrush:径向渐变
径向渐变从一个中心点向外辐射:
```xml
<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>
```
### 🖼️ 9.2.4 ImageBrush:图像填充
用图像作为填充内容:
```xml
<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>
```
### 🎭 9.2.5 AcrylicBrush:毛玻璃效果
WinUI 3 提供了现代的毛玻璃效果画刷:
```xml
<Border Width="400" Height="300">
<Border.Background>
<AcrylicBrush TintColor="LightGray"
TintOpacity="0.5"
FallbackColor="Gray" />
</Border.Background>
</Border>
```
> **技术细节**:AcrylicBrush 的性能考量
>
> 毛玻璃效果需要实时采样背景内容,在低端设备上可能影响性能。`TintColor` 设置基底颜色,`TintOpacity` 控制透明度,`FallbackColor` 是在不支持毛玻璃的设备上使用的备用颜色。
---
## 🔄 9.3 变换:打破静态布局
变换(Transform)允许你在不影响布局的情况下改变元素的视觉呈现。这对于动画和特效至关重要。
### 🔄 9.3.1 常用变换类型
**RotateTransform**:绕点旋转:
```xml
<Image Source="arrow.png" Width="50">
<Image.RenderTransform>
<RotateTransform Angle="45" CenterX="25" CenterY="25" />
</Image.RenderTransform>
</Image>
```
**ScaleTransform**:缩放:
```xml
<Button Content="放大" Width="100" Height="40">
<Button.RenderTransform>
<ScaleTransform ScaleX="1.2" ScaleY="1.2" />
</Button.RenderTransform>
</Button>
```
**TranslateTransform**:位移:
```xml
<Border Width="100" Height="100" Background="Blue">
<Border.RenderTransform>
<TranslateTransform X="50" Y="20" />
</Border.RenderTransform>
</Border>
```
**SkewTransform**:倾斜:
```xml
<TextBlock Text="斜体效果" FontSize="24">
<TextBlock.RenderTransform>
<SkewTransform AngleX="-15" />
</TextBlock.RenderTransform>
</TextBlock>
```
### 🔀 9.3.2 组合变换
`TransformGroup` 允许组合多个变换:
```xml
<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`,因为它性能更好,不会触发布局重计算。
---
## 🎬 9.4 传统动画:Storyboard 与时间轴
动画是让界面"活起来"的关键。XAML 提供了强大的声明式动画系统。
### 🎞️ 9.4.1 Storyboard 基础
`Storyboard` 是动画的容器,它管理一个或多个动画的时间线:
```xml
<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" />
```
在代码中启动动画:
```csharp
private void ShowImage()
{
FadeInStoryboard.Begin();
}
private void HideImage()
{
FadeOutStoryboard.Begin();
}
```
### 📊 9.4.2 动画类型
**DoubleAnimation**:动画化 double 类型属性(Opacity、Width、Angle 等):
```xml
<DoubleAnimation
Storyboard.TargetName="MyElement"
Storyboard.TargetProperty="Width"
From="100" To="200"
Duration="0:0:1"
AutoReverse="True"
RepeatBehavior="Forever" />
```
**ColorAnimation**:动画化颜色:
```xml
<ColorAnimation
Storyboard.TargetName="MyBrush"
Storyboard.TargetProperty="Color"
From="Red" To="Blue"
Duration="0:0:2" />
```
**PointAnimation**:动画化点坐标:
```xml
<PointAnimation
Storyboard.TargetName="MyEllipse"
Storyboard.TargetProperty="Center"
From="0,0" To="100,100"
Duration="0:0:1" />
```
### ⏱️ 9.4.3 缓动函数(Easing Functions)
缓动函数让动画更自然,模拟现实世界的物理特性:
```xml
<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` | 回退 | 超出再回来 |
### 🔄 9.4.4 关键帧动画
关键帧动画允许在时间线上定义多个状态:
```xml
<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>
```
不同类型的关键帧提供不同的插值方式:
```xml
<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>
```
---
## ⚡ 9.5 Composition API:高性能视觉层
对于需要 60fps 流畅度的复杂动画,传统 Storyboard 可能力不从心。WinUI 提供了 **Composition API**,它在独立的合成线程上运行,直接操作 GPU。
### 🎯 9.5.1 Composition 基础
```csharp
// 获取元素的 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);
```
### 🌊 9.5.2 表达式动画
Composition API 支持基于表达式的动画,可以让一个属性根据其他属性动态计算:
```csharp
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);
```
### 🔧 9.5.3 自然动画
Composition API 提供了基于物理的自然动画:
```csharp
// 弹簧动画
var springAnimation = compositor.CreateSpringScalarAnimation();
springAnimation.Period = TimeSpan.FromMilliseconds(50);
springAnimation.DampingRatio = 0.5f;
springAnimation.FinalValue = 100f;
visual.StartAnimation("Offset.X", springAnimation);
```
> **何时使用 Composition API**:
>
> - 需要高性能、60fps 的动画
> - 需要基于物理的自然动画
> - 需要动画响应实时输入(如鼠标位置)
> - 需要同时动画大量元素
>
> 对于简单的 UI 状态过渡,传统 Storyboard 仍然是更简单的选择。
---
## 🎭 9.6 Lottie:矢量动画的魔法
**Lottie** 是 Airbnb 开发的一种基于 JSON 的矢量动画格式,设计师可以在 After Effects 中制作动画,导出为 Lottie 格式,然后在应用中播放。
### 📦 9.6.1 安装和配置
首先安装 `Uno.WinUI.Lottie` NuGet 包。
### ▶️ 9.6.2 播放 Lottie 动画
```xml
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>
```
### 🎮 9.6.3 控制播放
```csharp
// 播放
await LottiePlayer.PlayAsync(0, 1, looped: false);
// 暂停
LottiePlayer.Pause();
// 停止
LottiePlayer.Stop();
// 设置播放速度(负数反向播放)
LottiePlayer.Speed = 0.5;
// 设置进度(0-1)
LottiePlayer.SetProgress(0.5);
```
### 🔗 9.6.4 与数据绑定结合
```xml
<AnimatedVisualPlayer x:Name="LoadingPlayer">
<lottie:LottieVisualSource UriSource="ms-appx:///Assets/loading.json" />
</AnimatedVisualPlayer>
<Button Content="{x:Bind ViewModel.IsLoading, Converter={StaticResource BoolToVisibility}}" />
```
```csharp
// 当加载状态改变时控制动画
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**,包括相机、传感器、地理位置等跨平台能力。
---
> **动手实验**:
> 1. 使用 Path 绘制一个自定义图标(如五角星或心形),并应用渐变填充。
> 2. 创建一个按钮悬停动画:鼠标悬停时按钮放大 1.1 倍并微微旋转,离开时恢复。
> 3. 实现一个加载动画:三个圆点依次上下跳动,使用 Storyboard 和关键帧动画。
> 4. (进阶)使用 Composition API 创建一个跟随手指/鼠标移动的元素,带有自然的弹性效果。
登录后可参与表态
讨论回复
0 条回复还没有人回复,快来发表你的看法吧!