欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
目录
- [1. animated_builder.dart](#1. animated_builder.dart)
-
- [1.1. 技术要点](#1.1. 技术要点)
- [1.2. 程序实现](#1.2. 程序实现)
- [2. curved_animation.dart](#2. curved_animation.dart)
-
- [2.1. 技术要点](#2.1. 技术要点)
- [2.2. 程序实现](#2.2. 程序实现)
- [3. focus_image.dart](#3. focus_image.dart)
-
- [3.1. 技术要点](#3.1. 技术要点)
- [3.2. 程序实现](#3.2. 程序实现)
- [4. page_route_builder.dart](#4. page_route_builder.dart)
-
- [4.1. 技术要点](#4.1. 技术要点)
- [4.2. 程序实现](#4.2. 程序实现)
- [5. 效果演示](#5. 效果演示)
本节我们针对常用的动画效果做一个补充,方便日后项目中使用。
1. animated_builder.dart
1.1. 技术要点
实现了一个使用 AnimatedBuilder 的 Flutter 示例页面,核心功能是:点击按钮时,按钮的背景色会在紫色(Colors.deepPurple)和橙色(Colors.deepOrange)之间平滑过渡动画,动画时长为 800 毫秒。
- 基础类定义
- 这是一个有状态的 Widget(StatefulWidget),因为需要处理动画状态的变化
- routeName 是路由名称,用于导航跳转时标识这个页面。
- 状态类核心逻辑
- SingleTickerProviderStateMixin:提供动画帧回调(vsync),确保动画只在页面可见时运行,节省性能
- AnimationController:动画控制器,控制动画的播放、暂停、反向等
- ColorTween:颜色补间动画,定义从起始色到结束色的过渡
- initState:初始化动画控制器和颜色动画
- dispose:销毁控制器,这是 Flutter 动画开发的必做操作,否则会内存泄漏
- 构建 UI 部分
- AnimatedBuilder:Flutter 中高效的动画封装 Widget,核心优势是只重建需要动画的部分
animation:绑定要监听的动画对象,动画值变化时会触发 builder 重建
builder:动画值变化时执行的构建函数,这里只重建按钮的样式,不重建整个页面
child:预构建的静态子 Widget(这里是文字),不会随动画重建,提升性能 - 按钮点击逻辑:根据动画当前状态(controller.status)切换播放方向
AnimationStatus.completed:动画正向播放完成(已到橙色)
controller.forward():正向播放(紫→橙)
controller.reverse():反向播放(橙→紫)
运行效果
页面初始化后,按钮默认是紫色(beginColor)
第一次点击按钮:按钮背景色从紫色平滑过渡到橙色(800 毫秒)
再次点击按钮:按钮背景色从橙色平滑过渡回紫色(800 毫秒)
重复点击会在两种颜色之间循环切换
总结
AnimatedBuilder 的核心价值:只重建动画相关的 Widget 部分,而非整个页面,提升性能;支持传入静态 child 进一步优化性能。
动画开发必做步骤:使用 AnimationController 必须配合 SingleTickerProviderStateMixin,且在 dispose 中销毁控制器,防止内存泄漏。
动画控制逻辑:通过 controller.forward()/reverse() 控制动画方向,通过 AnimationStatus 判断当前动画状态。
1.2. 程序实现
bash
import 'package:flutter/material.dart';
class AnimatedBuilderDemo extends StatefulWidget {
const AnimatedBuilderDemo({super.key});
static const String routeName = 'basics/animated_builder';
@override
State<AnimatedBuilderDemo> createState() => _AnimatedBuilderDemoState();
}
class _AnimatedBuilderDemoState extends State<AnimatedBuilderDemo>
with SingleTickerProviderStateMixin {
static const Color beginColor = Colors.deepPurple;
static const Color endColor = Colors.deepOrange;
Duration duration = const Duration(milliseconds: 800);
late AnimationController controller;
late Animation<Color?> animation;
@override
void initState() {
super.initState();
controller = AnimationController(vsync: this, duration: duration);
animation =
ColorTween(begin: beginColor, end: endColor).animate(controller);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('AnimatedBuilder'),
),
body: Center(
// AnimatedBuilder handles listening to a given animation and calling the builder
// whenever the value of the animation change. This can be useful when a Widget
// tree contains some animated and non-animated elements, as only the subtree
// created by the builder needs to be re-built when the animation changes.
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: animation.value,
),
child: child,
onPressed: () {
switch (controller.status) {
case AnimationStatus.completed:
controller.reverse();
default:
controller.forward();
}
},
);
},
// AnimatedBuilder can also accept a pre-built child Widget which is useful
// if there is a non-animated Widget contained within the animated widget.
// This can improve performance since this widget doesn't need to be rebuilt
// when the animation changes.
child: const Text(
'Change Color',
style: TextStyle(color: Colors.white),
),
),
),
);
}
}
2. curved_animation.dart
2.1. 技术要点
代码整体功能
这段代码实现了一个曲线动画演示页面,核心功能是:
提供下拉选择框,可分别选择正向动画曲线和反向动画曲线。
点击「Animate」按钮后,Flutter Logo 会同时执行旋转动画和水平平移动画
动画完成后自动反向播放,且正 / 反向动画可以使用不同的缓动曲线(如弹跳、弹性、立方曲线等)
-
基础结构与数据定义
CurveChoice 是一个数据模型类,用于封装「动画曲线」和「曲线名称」,方便下拉框展示和选择
页面继承 StatefulWidget 是因为需要动态切换曲线、更新动画状态
-
状态类核心成员
关键概念:
Curve:Flutter 中的动画曲线,决定动画的速率变化规律(比如匀速、先慢后快、弹跳、弹性等)
CurvedAnimation:将普通的 AnimationController 包装成带曲线的动画,让动画按指定曲线执行
SingleTickerProviderStateMixin:提供动画帧回调,保证动画性能
-
初始化动画逻辑
核心要点:
CurvedAnimation 是「包装器」:它不直接产生动画值,而是修改 parent(AnimationController)的动画值变化速率
curve:正向播放(forward)时使用的曲线
reverseCurve:反向播放(reverse)时使用的曲线(如果不设置,反向会用 curve 的反转)
addListener(() { setState(() {}); }):监听动画值变化,触发 UI 重建(这是手动监听动画的方式,也可以用 AnimatedBuilder 优化)
addStatusListener:监听动画状态,完成后自动反向播放
-
构建 UI 部分
关键 Widget 解析:
DropdownButton:下拉选择框,用于切换正 / 反向动画曲线
Transform.rotate:旋转变换,angle 参数接收弧度值(2*math.pi = 360 度)
FractionalTranslation:百分比平移动画,Offset(1,0) 表示向右平移 1 倍自身宽度
动态修改曲线:curvedAnimation.curve = newCurve 可以实时修改动画曲线,无需重建控制器
-
资源释放
这是 Flutter 动画开发的必做操作,AnimationController 持有资源,必须手动销毁
运行效果
页面初始化后,默认选中「Bounce In」曲线
点击「Animate」按钮:
上方 Logo 开始 360 度旋转,下方 Logo 从左向右平移
动画速率遵循选中的正向曲线(比如 Bounce In 会有「弹跳进入」的效果)
动画 4 秒后完成(_duration),自动反向播放(旋转回 0 度、平移回左侧)
反向播放的速率遵循选中的反向曲线
可随时通过下拉框切换正 / 反向曲线,新曲线会在下次动画生效
总结
CurvedAnimation 核心作用:为基础动画(AnimationController)添加「缓动曲线」,控制动画的速率变化(匀速、弹跳、弹性等),支持正 / 反向使用不同曲线。
动画复用:一个 CurvedAnimation 可以被多个 Tween 动画(旋转、平移)共享,实现多属性同步动画。
关键注意点:AnimationController 必须在 dispose 中销毁;动态修改曲线无需重建控制器,直接赋值即可。
2.2. 程序实现
bash
import 'dart:math' as math;
import 'package:flutter/material.dart';
class CurvedAnimationDemo extends StatefulWidget {
const CurvedAnimationDemo({super.key});
static const String routeName = 'misc/curved_animation';
@override
State<CurvedAnimationDemo> createState() => _CurvedAnimationDemoState();
}
class CurveChoice {
final Curve curve;
final String name;
const CurveChoice({required this.curve, required this.name});
}
class _CurvedAnimationDemoState extends State<CurvedAnimationDemo>
with SingleTickerProviderStateMixin {
late final AnimationController controller;
late final Animation<double> animationRotation;
late final Animation<Offset> animationTranslation;
static const _duration = Duration(seconds: 4);
List<CurveChoice> curves = const [
CurveChoice(curve: Curves.bounceIn, name: 'Bounce In'),
CurveChoice(curve: Curves.bounceOut, name: 'Bounce Out'),
CurveChoice(curve: Curves.easeInCubic, name: 'Ease In Cubic'),
CurveChoice(curve: Curves.easeOutCubic, name: 'Ease Out Cubic'),
CurveChoice(curve: Curves.easeInExpo, name: 'Ease In Expo'),
CurveChoice(curve: Curves.easeOutExpo, name: 'Ease Out Expo'),
CurveChoice(curve: Curves.elasticIn, name: 'Elastic In'),
CurveChoice(curve: Curves.elasticOut, name: 'Elastic Out'),
CurveChoice(curve: Curves.easeInQuart, name: 'Ease In Quart'),
CurveChoice(curve: Curves.easeOutQuart, name: 'Ease Out Quart'),
CurveChoice(curve: Curves.easeInCirc, name: 'Ease In Circle'),
CurveChoice(curve: Curves.easeOutCirc, name: 'Ease Out Circle'),
];
late CurveChoice selectedForwardCurve, selectedReverseCurve;
late final CurvedAnimation curvedAnimation;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: _duration,
vsync: this,
);
selectedForwardCurve = curves[0];
selectedReverseCurve = curves[0];
curvedAnimation = CurvedAnimation(
parent: controller,
curve: selectedForwardCurve.curve,
reverseCurve: selectedReverseCurve.curve,
);
animationRotation = Tween<double>(
begin: 0,
end: 2 * math.pi,
).animate(curvedAnimation)
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
}
});
animationTranslation = Tween<Offset>(
begin: const Offset(-1, 0),
end: const Offset(1, 0),
).animate(curvedAnimation)
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Curved Animation'),
),
body: Column(
children: [
const SizedBox(height: 20.0),
Text(
'Select Curve for forward motion',
style: Theme.of(context).textTheme.titleLarge,
),
DropdownButton<CurveChoice>(
items: curves.map((curve) {
return DropdownMenuItem<CurveChoice>(
value: curve, child: Text(curve.name));
}).toList(),
onChanged: (newCurve) {
if (newCurve != null) {
setState(() {
selectedForwardCurve = newCurve;
curvedAnimation.curve = selectedForwardCurve.curve;
});
}
},
value: selectedForwardCurve,
),
const SizedBox(height: 15.0),
Text(
'Select Curve for reverse motion',
style: Theme.of(context).textTheme.titleLarge,
),
DropdownButton<CurveChoice>(
items: curves.map((curve) {
return DropdownMenuItem<CurveChoice>(
value: curve, child: Text(curve.name));
}).toList(),
onChanged: (newCurve) {
if (newCurve != null) {
setState(() {
selectedReverseCurve = newCurve;
curvedAnimation.reverseCurve = selectedReverseCurve.curve;
});
}
},
value: selectedReverseCurve,
),
const SizedBox(height: 35.0),
Transform.rotate(
angle: animationRotation.value,
child: const Center(
child: FlutterLogo(
size: 100,
),
),
),
const SizedBox(height: 35.0),
FractionalTranslation(
translation: animationTranslation.value,
child: const FlutterLogo(
size: 100,
),
),
const SizedBox(height: 25.0),
ElevatedButton(
onPressed: () {
controller.forward();
},
child: const Text('Animate'),
),
],
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
3. focus_image.dart
3.1. 技术要点
实现了一个「图片网格预览 + 点击放大查看」的功能,核心亮点是:
展示 4 列共 40 张图片的网格布局(前 20 张和后 20 张分别用不同图片)
点击任意图片时,图片会从原位置平滑过渡放大到全屏展示
点击放大后的图片,会反向过渡回到原网格位置
过渡动画使用自定义的 PositionedTransition,实现「焦点缩放」的视觉效果
-
入口页面与网格布局
关键知识点:
GridView.builder:懒加载网格布局,只构建可见区域的子项,性能更优
SliverGridDelegateWithFixedCrossAxisCount:固定列数的网格代理,crossAxisCount: 4 表示 4 列布局
SmallCard:封装了单个图片卡片的样式和点击逻辑
-
单个图片卡片(SmallCard)
关键知识点:
InkWell:带水波纹效果的点击组件,替代 GestureDetector 更符合 Material 设计
Image.asset:加载本地图片资源,fit: BoxFit.cover 保证图片填满容器且不变形
点击时调用 _createRoute 创建自定义过渡的路由
-
核心:自定义页面过渡动画
3.1 创建自定义路由(_createRoute)
关键知识点:
PageRouteBuilder:自定义页面路由的核心类,可自定义页面内容和过渡动画
transitionsBuilder:过渡动画的构建函数,参数说明:
animation:正向过渡动画(进入新页面)
secondaryAnimation:反向过渡动画(返回原页面)
child:新页面的 Widget(这里是 _SecondPage)
CurveTween(curve: Curves.ease):添加缓动曲线,让动画更自然
PositionedTransition:基于 RelativeRect 的位置过渡组件,控制 Widget 的位置和大小
3.2 坐标计算(_createTween)
MediaQuery.of(context).size:获取设备屏幕的宽高
context.findRenderObject():获取当前 Widget(SmallCard)的渲染对象,包含布局信息
box.localToGlobal(Offset.zero):将 Widget 的本地坐标(相对父容器)转换为屏幕绝对坐标
& box.size:组合坐标和大小,得到 Widget 在屏幕上的矩形区域
RelativeRect.fromSize:将绝对矩形转换为相对屏幕的 RelativeRect(格式:left, top, right, bottom)
RelativeRectTween:创建补间动画,定义从「原位置」到「全屏」的过渡
-
放大后的图片页面(_SecondPage)
AspectRatio(aspectRatio: 1):强制图片按 1:1 比例展示,避免拉伸
Navigator.of(context).pop():返回上一页时,PageRouteBuilder 会自动反向执行过渡动画(从全屏缩回到原位置)
黑色背景:增强图片的视觉聚焦效果
运行效果
进入页面后,展示 4 列共 40 张图片的网格布局
点击任意图片:
该图片会从原网格位置平滑放大,同时移动到屏幕中心
动画过程中图片的位置和大小连续变化,视觉上像是「从网格中飞出来放大」
点击放大后的图片:
图片会平滑缩小并回到原网格位置,同时关闭放大页面
总结
核心技术点:PageRouteBuilder 自定义过渡动画 + PositionedTransition 位置过渡 + RelativeRectTween 坐标补间,实现「焦点缩放」的视觉效果。
坐标计算逻辑:通过渲染对象获取 Widget 绝对位置,转换为相对屏幕的坐标,作为动画的起始值。
反向动画特性:Flutter 路由过渡动画默认支持反向执行,无需重复编写反向逻辑,只需调用 pop() 即可。
3.2. 程序实现
bash
import 'package:flutter/material.dart';
class FocusImageDemo extends StatelessWidget {
const FocusImageDemo({super.key});
static String routeName = 'misc/focus_image';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Focus Image')),
body: const Grid(),
);
}
}
class Grid extends StatelessWidget {
const Grid({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: GridView.builder(
itemCount: 40,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4),
itemBuilder: (context, index) {
return (index >= 20)
? const SmallCard(
imageAssetName: 'assets/eat_cape_town_sm.jpg',
)
: const SmallCard(
imageAssetName: 'assets/eat_new_orleans_sm.jpg',
);
},
),
);
}
}
Route _createRoute(BuildContext parentContext, String image) {
return PageRouteBuilder<void>(
pageBuilder: (context, animation, secondaryAnimation) {
return _SecondPage(image);
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
var rectAnimation = _createTween(parentContext)
.chain(CurveTween(curve: Curves.ease))
.animate(animation);
return Stack(
children: [
PositionedTransition(rect: rectAnimation, child: child),
],
);
},
);
}
Tween<RelativeRect> _createTween(BuildContext context) {
var windowSize = MediaQuery.of(context).size;
var box = context.findRenderObject() as RenderBox;
var rect = box.localToGlobal(Offset.zero) & box.size;
var relativeRect = RelativeRect.fromSize(rect, windowSize);
return RelativeRectTween(
begin: relativeRect,
end: RelativeRect.fill,
);
}
class SmallCard extends StatelessWidget {
const SmallCard({required this.imageAssetName, super.key});
final String imageAssetName;
@override
Widget build(BuildContext context) {
return Card(
child: Material(
child: InkWell(
onTap: () {
var nav = Navigator.of(context);
nav.push<void>(_createRoute(context, imageAssetName));
},
child: Image.asset(
imageAssetName,
fit: BoxFit.cover,
),
),
),
);
}
}
class _SecondPage extends StatelessWidget {
final String imageAssetName;
const _SecondPage(this.imageAssetName);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Material(
child: InkWell(
onTap: () => Navigator.of(context).pop(),
child: AspectRatio(
aspectRatio: 1,
child: Image.asset(
imageAssetName,
fit: BoxFit.cover,
),
),
),
),
),
);
}
}
4. page_route_builder.dart
4.1. 技术要点
实现了两个页面的跳转功能,核心亮点是:
页面 1(Page 1)有一个「Go!」按钮,点击后跳转到页面 2(Page 2)
跳转时使用自定义的滑动过渡动画:页面 2 从屏幕底部(Offset(0.0, 1.0))向上滑入屏幕,动画带有 Curves.ease 缓动效果
返回页面 1 时,动画会自动反向执行(页面 2 向下滑出屏幕)
-
入口页面(PageRouteBuilderDemo)
这是无状态 Widget(StatelessWidget),因为页面逻辑简单,无状态变化
Navigator.of(context).push(_createRoute()):通过 push 方法跳转到自定义路由返回的页面
push 中的 表示跳转时不传递返回值
-
核心:自定义路由(_createRoute)
点击「Go!」后,animation 会从 0 → 1 执行
curveTween 先将 animation 的值按 Curves.ease 曲线转换
tween 再将转换后的值映射为 Offset(从 (0,1) → (0,0))
SlideTransition 根据 Offset 控制 _Page2() 从底部滑入屏幕
-
目标页面(_Page2)
类名前的 _ 表示私有类,只能在当前文件中使用
Theme.of(context).textTheme.headlineMedium:使用系统主题的文字样式,符合 Material 设计规范
返回页面 1 时,Flutter 会自动反向执行 transitionsBuilder 中的动画(animation 从 1 → 0),页面 2 会向下滑出屏幕
运行效果
初始页面显示「Page 1」和「Go!」按钮
点击「Go!」按钮:
页面 2 从屏幕底部开始向上滑动
动画速率遵循 Curves.ease(先慢→快→慢)
最终页面 2 完全显示在屏幕中
点击页面 2 的返回按钮(AppBar 左侧箭头):
页面 2 向下滑动,从屏幕底部消失
回到页面 1,动画反向执行,无需额外代码
总结
PageRouteBuilder 核心作用:替代 Flutter 默认的页面跳转动画,完全自定义页面的进入 / 退出过渡效果,支持任意动画类型(滑动、缩放、渐变等)。
关键组合:PageRouteBuilder + transitionsBuilder + Tween + Transition组件(如 SlideTransition)是自定义页面动画的标准写法。
反向动画特性:Flutter 会自动处理反向动画(返回页面时),只需定义正向动画即可,无需重复编写反向逻辑。
4.2. 程序实现
bash
import 'package:flutter/material.dart';
class PageRouteBuilderDemo extends StatelessWidget {
const PageRouteBuilderDemo({super.key});
static const String routeName = 'basics/page_route_builder';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Page 1'),
),
body: Center(
child: ElevatedButton(
child: const Text('Go!'),
onPressed: () {
Navigator.of(context).push<void>(_createRoute());
},
),
),
);
}
}
Route _createRoute() {
return PageRouteBuilder<SlideTransition>(
pageBuilder: (context, animation, secondaryAnimation) => _Page2(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
var tween =
Tween<Offset>(begin: const Offset(0.0, 1.0), end: Offset.zero);
var curveTween = CurveTween(curve: Curves.ease);
return SlideTransition(
position: animation.drive(curveTween).drive(tween),
child: child,
);
},
);
}
class _Page2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Page 2'),
),
body: Center(
child:
Text('Page 2!', style: Theme.of(context).textTheme.headlineMedium),
),
);
}
}
5. 效果演示
动画效果