🔥 本文由 程序喵正在路上 原创,CSDN首发!
💖 系列专栏:Flutter学习
🌠 首发时间:2024年5月29日
🦋 欢迎关注🖱点赞👍收藏🌟留言🐾
目录
- 交错动画
- 自定义动画
- TweenAnimationBuilder自定义隐式动画
- AnimatedBuilder自定义显式动画
- Hero动画
- Hero动画的应用一
- Hero动画的应用二
- 配置Hero动画的执行时间
- Hero+photo_view实现类似微信朋友圈图片预览
- photo_view预览单张图片
- photo_view预览多张图片
交错动画
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 1));
_controller.addListener(() {
print(_controller.value);
});
}
void dispose() {
super.dispose();
_controller.dispose();
}
void _toggleAnimation() {
if (_controller.status == AnimationStatus.completed) {
_controller.reverse();
} else {
_controller.forward();
}
}
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: _toggleAnimation,
child: const Icon(Icons.refresh),
),
appBar: AppBar(
title: const Text('AnimatedIcon'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SlidingBox(
controller: _controller,
color: Colors.blue[200],
curve: const Interval(0, 0.2),
),
SlidingBox(
controller: _controller,
color: Colors.blue[400],
curve: const Interval(0.2, 0.4),
),
SlidingBox(
controller: _controller,
color: Colors.blue[600],
curve: const Interval(0.4, 0.6),
),
SlidingBox(
controller: _controller,
color: Colors.blue[800],
curve: const Interval(0.6, 0.8),
),
SlidingBox(
controller: _controller,
color: Colors.blue[900],
curve: const Interval(0.8, 1),
),
],
),
),
);
}
}
class SlidingBox extends StatelessWidget {
final AnimationController controller;
final Color? color;
final Curve curve;
const SlidingBox(
{super.key,
required this.controller,
required this.color,
required this.curve});
Widget build(BuildContext context) {
return SlideTransition(
position: Tween(begin: const Offset(-0.2, 1), end: const Offset(0.3, 0))
.chain(CurveTween(curve: Curves.bounceInOut))
.chain(CurveTween(curve: curve))
.animate(controller),
child: Container(
width: 220,
height: 60,
color: color,
),
);
}
}
自定义动画
TweenAnimationBuilder自定义隐式动画
每当 Tween 的 end 发生变化的时候就会触发动画。
大小变化的动画:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool flag = true;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('大小变化'),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.refresh),
onPressed: () {
setState(() {
flag = !flag;
});
},
),
body: Center(
child: TweenAnimationBuilder(
tween: Tween(begin: 100.0, end: flag ? 100.0 : 200.0),
duration: const Duration(seconds: 1),
builder: ((context, value, child) {
return Icon(
Icons.star,
color: Colors.red,
size: value,
);
}),
),
),
);
}
}
效果:点击浮动按钮,五角星的大小会变化。
透明度变化的动画:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool flag = true;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('透明度变化'),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.refresh),
onPressed: () {
setState(() {
flag = !flag;
});
},
),
body: Center(
child: TweenAnimationBuilder(
tween: Tween(begin: 0.0, end: flag ? 0.2 : 1.0),
duration: const Duration(seconds: 1),
builder: ((context, value, child) {
return Opacity(
opacity: value,
child: Container(color: Colors.blue, width: 200, height: 200),
);
}),
),
),
);
}
}
效果:点击浮动按钮,盒子的透明度会变化。
AnimatedBuilder自定义显式动画
透明度动画:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)..repeat(reverse: true); //.. 连缀操作
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('透明度动画'),
),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
return Opacity(
opacity: _controller.value, //从0到1变化
child: Container(
width: 200,
height: 200,
color: Colors.blue,
child: const Text('我是一个Text组件'),
),
);
},
),
),
);
}
}
效果:盒子的透明度会自动不停变化。
自定义变化范围:
上面代码中 opacity 的值我们也可以使用 Tween 来设置:
opacity: Tween(begin: 0.5, end: 1.0).animate(_controller).value, //从0.5到1变化
位置变化:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)..repeat(reverse: true); //.. 连缀操作
}
Widget build(BuildContext context) {
Animation y = Tween(begin: -120.0, end: 120.0)
.chain(CurveTween(curve: Curves.easeIn))
// .chain(CurveTween(curve: const Interval(0.2, 0.6)))
.animate(_controller);
return Scaffold(
appBar: AppBar(
title: const Text('位置变化'),
),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
return Container(
width: 200,
height: 200,
color: Colors.blue,
transform: Matrix4.translationValues(0, y.value, 0),
child: const Text('我是一个Text组件'),
);
},
),
),
);
}
}
效果:一个盒子在不停上下跳动。
child优化:
return Scaffold(
appBar: AppBar(
title: const Text('child优化'),
),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
return Container(
width: 200,
height: 200,
color: Colors.blue,
transform: Matrix4.translationValues(0, y.value, 0),
child: child,
);
},
child: const Text('我是一个Text组件'),
),
),
);
当我们将 Text 组件放在 builder 函数内部时,Text 组件会根据 _animation 的值进行重建。这意味着在每个动画帧上,Text 组件都会被重新构建,即使 Text 内容没有发生变化。这可能会导致不必要的重建和性能损失。
相比之下,将 Text 组件放在 builder 函数外部,则不会在每个动画帧上进行重建。Text 组件只会在初始渲染时创建一次,并且不会随着动画的进度而重建。这样可以减少重建次数,提高性能。
因此,将 Text 组件放在 builder 函数外部是一种更优化的做法,特别是当 Text 内容不会随动画进度而改变时。只有当动画进度对 Text 内容有影响时,才需要将 Text 组件放在 builder 函数内部,以确保 Text 能够根据动画的进度进行更新。
Hero动画
Hero动画的应用一
微信朋友圈点击小图片的时候会有一个动画效果到大图预览,这个动画效果就可以使用Hero 动画实现。
Hero 指的是可以在路由(页面)之间 “飞行” 的 widget,简单来说 Hero 动画就是在路由切换时,有一个共享的 widget 可以在新旧路由间切换。
我们回到之前写的自定义底部导航实现页面切换的代码:
我们将 home.dart 进行改进,并添加一个 hero.dart 用于演示 Hero 动画,具体代码如下:
main.dart
import 'package:flutter/material.dart';
import './routers/router.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
initialRoute: "/",
onGenerateRoute: onGenerateRoute,
);
}
}
home.dart
import 'package:flutter/material.dart';
import '../../res/listData.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<Widget> _getListData() {
var tempList = listData.map((value) {
return GestureDetector(
onTap: () {
Navigator.pushNamed(context, "/hero", arguments: {
"imageUrl": value['imageUrl'],
"author": value['author'],
});
},
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: const Color.fromRGBO(233, 233, 233, 0.9), width: 1),
),
child: Column(
children: [
Hero(
tag: value['imageUrl'], //唯一值
child: Image.network(value['imageUrl']),
),
const SizedBox(height: 12),
Text(
value['title'],
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 17),
),
],
),
),
);
});
return tempList.toList();
}
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 2, //一行的Widget数量
crossAxisSpacing: 10.0, //水平方向的子Widget之间的间距
mainAxisSpacing: 10.0, //垂直方向的子Widget之间的间距
padding: const EdgeInsets.all(10),
children: _getListData(),
);
}
}
hero.dart
import 'package:flutter/material.dart';
class HeroPage extends StatefulWidget {
final Map arguments;
const HeroPage({super.key, required this.arguments});
State<HeroPage> createState() => _HeroPageState();
}
class _HeroPageState extends State<HeroPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('详情页面'),
),
body: ListView(
children: [
Hero(
tag: widget.arguments['imageUrl'], //tag值要与home.dart中一致
child: Image.network(widget.arguments['imageUrl']),
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.fromLTRB(30, 5, 30, 0),
child: Text(
widget.arguments['author'],
style: const TextStyle(fontSize: 22),
),
),
],
),
);
}
}
router.dart
import 'package:flutter/cupertino.dart';
import '../pages/hero.dart';
import '../pages/tabs/category.dart';
import '../pages/tabs/home.dart';
import '../pages/tabs/message.dart';
import '../pages/tabs/setting.dart';
import '../pages/tabs/user.dart';
import '../pages/tabs.dart';
//1. 定义路由
final Map routes = {
"/": (context) => const Tabs(),
"/home": (context) => const HomePage(),
"/category": (context) => const CategoryPage(),
"/setting": (context) => const SettingPage(),
"/message": (context) => const MessagePage(),
"/user": (context) => const UserPage(),
"/hero": (context, {arguments}) => HeroPage(arguments: arguments)
};
//2. 配置onGenerateRoute,固定写法
var onGenerateRoute = (settings) {
// 统一处理
final String? name = settings.name;
final Function? pageContentBuilder = routes[name];
if (pageContentBuilder != null) {
if (settings.arguments != null) {
final Route route = CupertinoPageRoute(
builder: (context) =>
pageContentBuilder(context, arguments: settings.arguments));
return route;
} else {
final Route route =
CupertinoPageRoute(builder: (context) => pageContentBuilder(context));
return route;
}
}
return null;
};
点击图片会进入对应的详情页面:
Hero动画的应用二
将 hero.dart 换成这个:
import 'package:flutter/material.dart';
class HeroPage extends StatefulWidget {
final Map arguments;
const HeroPage({super.key, required this.arguments});
State<HeroPage> createState() => _HeroPageState();
}
class _HeroPageState extends State<HeroPage> {
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Hero(
tag: widget.arguments['imageUrl'],
child: Scaffold(
//加Scaffold是为了点击屏幕任意位置都可以返回
backgroundColor: Colors.black,
body: Center(
child: AspectRatio(
aspectRatio: 16 / 9,
child: Image.network(
widget.arguments['imageUrl'],
fit: BoxFit.cover,
),
),
),
),
),
);
}
}
配置Hero动画的执行时间
-
引入 scheduler.dart
import 'package:flutter/scheduler.dart';
-
设置动画时间
void initState() { super.initState(); timeDilation = 1.0; //设置动画时间 }
Hero+photo_view实现类似微信朋友圈图片预览
- photo_view 插件支持预览图片,可放大、缩小、滑动图片
- photo_view 官方地址:https://pub-web.flutter-io.cn/packages/photo_view
photo_view预览单张图片
-
配置依赖
dependencies: photo_view: ^0.15.0
-
引入
import 'package:photo_view/photo_view.dart';
-
单张图片的预览
改进 hero.dart:
import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; class HeroPage extends StatefulWidget { final Map arguments; const HeroPage({super.key, required this.arguments}); State<HeroPage> createState() => _HeroPageState(); } class _HeroPageState extends State<HeroPage> { Widget build(BuildContext context) { return GestureDetector( onTap: () { Navigator.pop(context); }, child: Hero( tag: widget.arguments['imageUrl'], child: Scaffold( //加Scaffold是为了点击屏幕任意位置都可以返回 backgroundColor: Colors.black, body: Center( child: AspectRatio( aspectRatio: 16 / 9, child: PhotoView( imageProvider: NetworkImage(widget.arguments['imageUrl']), )), ), ), ), ); } }
photo_view预览多张图片
-
配置依赖
dependencies: photo_view: ^0.15.0
-
引入
import 'package:photo_view/photo_view_gallery.dart';
-
多张图片的预览
改造 listData,加个属性:
List listData = [ { "id": 0, "title": 'Candy Shop', "author": 'Mohamed Chahin', "imageUrl": 'https://www.itying.com/images/flutter/1.png', }, { "id": 1, "title": 'Childhood in a picture', "author": 'Google', "imageUrl": 'https://www.itying.com/images/flutter/2.png', }, { "id": 2, "title": 'Alibaba Shop', "author": 'Alibaba', "imageUrl": 'https://www.itying.com/images/flutter/3.png', }, { "id": 3, "title": 'Candy Shop', "author": 'Mohamed Chahin', "imageUrl": 'https://www.itying.com/images/flutter/4.png', }, { "id": 4, "title": 'Tornado', "author": 'Mohamed Chahin', "imageUrl": 'https://www.itying.com/images/flutter/5.png', }, { "id": 5, "title": 'Undo', "author": 'Mohamed Chahin', "imageUrl": 'https://www.itying.com/images/flutter/6.png', }, { "id": 6, "title": 'white-dragon', "author": 'Mohamed Chahin', "imageUrl": 'https://www.itying.com/images/flutter/7.png', } ];
home.dart 中需要传入 hero.dart 需要的参数:
hero.dart:import 'package:flutter/material.dart'; import 'package:photo_view/photo_view_gallery.dart'; class HeroPage extends StatefulWidget { final Map arguments; const HeroPage({super.key, required this.arguments}); State<HeroPage> createState() => _HeroPageState(); } class _HeroPageState extends State<HeroPage> { late List listData = []; late int initialPage; void initState() { super.initState(); listData = widget.arguments['listData']; initialPage = widget.arguments['initialPage']; } Widget build(BuildContext context) { return GestureDetector( onTap: () { Navigator.pop(context); }, child: Hero( tag: widget.arguments['imageUrl'], child: Scaffold( //加Scaffold是为了点击屏幕任意位置都可以返回 backgroundColor: Colors.black, body: Center( child: PhotoViewGallery.builder( itemCount: listData.length, pageController: PageController(initialPage: initialPage), //点击后显示点击的图片 builder: ((context, index) { return PhotoViewGalleryPageOptions( imageProvider: NetworkImage(listData[index]["imageUrl"])); }), ), ), ), ), ); } }
可以实现点击哪张图片就预览哪张图片,同时可以左右滑动切换图片。