前言
当前案例 Flutter SDK版本:3.13.2
显式动画
Tween({this.begin,this.end}) 两个构造参数,分别是 开始值 和 结束值,根据这两个值,提供了控制动画的方法,以下是常用的;
- controller.forward() : 向前,执行 begin 到 end 的动画,执行结束后,处于end状态;
- controller.reverse() : 反向,当动画已经完成,进行还原动画;
- controller.reset() : 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画;
使用方式一
使用 addListener() 和 setState();
import 'package:flutter/material.dart';
class TweenAnimation extends StatefulWidget {
const TweenAnimation({super.key});
@override
State<TweenAnimation> createState() => _TweenAnimationState();
}
/// 使用 addListener() 和 setState()
class _TweenAnimationState extends State<TweenAnimation>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
animation = Tween<double>(begin: 50, end: 100).animate(controller)
..addListener(() {
setState(() {}); // 更新UI
})..addStatusListener((status) {
debugPrint('status:$status'); // 监听动画执行状态
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'显式动画',
style: TextStyle(fontSize: 20),
)),
body: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: animation.value,
height: animation.value,
child: const FlutterLogo(),
),
ElevatedButton(
onPressed: () {
if (controller.isCompleted) {
controller.reverse();
} else {
controller.forward();
}
// controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
// controller.reverse(); // 反向,当动画已经完成,进行还原动画
// controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
},
child: const Text('缩放'),
)
],
)
],
),
),
);
}
}
使用方式二
AnimatedWidget,解决痛点:不需要再使用 addListener() 和 setState();
import 'package:flutter/material.dart';
class TweenAnimation extends StatefulWidget {
const TweenAnimation({super.key});
@override
State<TweenAnimation> createState() => _TweenAnimationState();
}
/// 测试 AnimatedWidget
class _TweenAnimationState extends State<TweenAnimation>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
animation = Tween<double>(begin: 50, end: 100).animate(controller)
..addStatusListener((status) {
debugPrint('status:$status'); // 监听动画执行状态
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'显式动画',
style: TextStyle(fontSize: 20),
)),
body: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedLogo(animation: animation),
ElevatedButton(
onPressed: () {
if (controller.isCompleted) {
controller.reverse();
} else {
controller.forward();
}
// controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
// controller.reverse(); // 反向,当动画已经完成,进行还原动画
// controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
},
child: const Text('缩放'),
)
],
)
],
),
),
);
}
}
/// 使用 AnimatedWidget,创建显式动画
/// 解决痛点:不需要再使用 addListener() 和 setState()
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({super.key, required Animation<double> animation})
: super(listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
width: animation.value,
height: animation.value,
child: const FlutterLogo(),
),
);
}
}
使用方式三
使用 内置的显式动画 widget;
后缀是 Transition 的组件,几乎都是 显式动画 widget;
import 'package:flutter/material.dart';
class TweenAnimation extends StatefulWidget {
const TweenAnimation({super.key});
@override
State<TweenAnimation> createState() => _TweenAnimationState();
}
/// 使用 内置的显式动画Widget
class _TweenAnimationState extends State<TweenAnimation>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this);
animation = Tween<double>(begin: 0.1, end: 1.0).animate(controller)
..addListener(() {
setState(() {}); // 更新UI
})
..addStatusListener((status) {
debugPrint('status:$status'); // 监听动画执行状态
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'显式动画',
style: TextStyle(fontSize: 20),
)),
body: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
/// 单个显示动画
FadeTransition(
opacity: animation,
child: const SizedBox(
width: 100,
height: 100,
child: FlutterLogo(),
),
),
/// 多个显示动画 配合使用
// FadeTransition( // 淡入淡出
// opacity: animation,
// child: RotationTransition( // 旋转
// turns: animation,
// child: ScaleTransition( // 更替
// scale: animation,
// child: const SizedBox(
// width: 100,
// height: 100,
// child: FlutterLogo(),
// ),
// ),
// ),
// ),
ElevatedButton(
onPressed: () {
if (controller.isCompleted) {
controller.reverse();
} else {
controller.forward();
}
// controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
// controller.reverse(); // 反向,当动画已经完成,进行还原动画
// controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
},
child: const Text('淡入淡出'),
)
],
)
],
),
),
);
}
}
使用方式四
AnimatedBuilder,这种方式感觉是 通过逻辑 动态选择 Widget,比如 flag ? widgetA : widgetB;
官方解释:
- AnimatedBuilder 知道如何渲染过渡效果
- 但 AnimatedBuilder 不会渲染 widget,也不会控制动画对象。
- 使用 AnimatedBuilder 描述一个动画是其他 widget 构建方法的一部分。
- 如果只是单纯需要用可重复使用的动画定义一个 widget,可参考文档:简单使用 AnimatedWidget。
import 'package:flutter/material.dart';
class TweenAnimation extends StatefulWidget {
const TweenAnimation({super.key});
@override
State<TweenAnimation> createState() => _TweenAnimationState();
}
/// 测试 AnimatedBuilder
class _TweenAnimationState extends State<TweenAnimation>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
animation = Tween<double>(begin: 50, end: 100).animate(controller)
..addStatusListener((status) {
debugPrint('status:$status'); // 监听动画执行状态
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'显式动画',
style: TextStyle(fontSize: 20),
)),
body: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GrowTransition(
animation: animation,
child: const FlutterLogo()),
ElevatedButton(
onPressed: () {
if (controller.isCompleted) {
controller.reverse();
} else {
controller.forward();
}
// controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
// controller.reverse(); // 反向,当动画已经完成,进行还原动画
// controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
},
child: const Text('缩放'),
)
],
)
],
),
),
);
}
}
class GrowTransition extends StatelessWidget {
final Widget child;
final Animation<double> animation;
const GrowTransition(
{required this.child, required this.animation, super.key});
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return SizedBox(
width: animation.value,
height: animation.value,
child: child,
);
},
child: child,
),
);
}
}
使用方式五
CurvedAnimation 曲线动画,一个Widget,同时使用多个动画;
import 'package:flutter/material.dart';
class TweenAnimation extends StatefulWidget {
const TweenAnimation({super.key});
@override
State<TweenAnimation> createState() => _TweenAnimationState();
}
/// 测试 动画同步使用
class _TweenAnimationState extends State<TweenAnimation>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
..addStatusListener((status) {
debugPrint('status:$status'); // 监听动画执行状态
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'显式动画',
style: TextStyle(fontSize: 20),
)),
body: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedLogoSync(animation: animation),
ElevatedButton(
onPressed: () {
if (controller.isCompleted) {
controller.reverse();
} else {
controller.forward();
}
// controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
// controller.reverse(); // 反向,当动画已经完成,进行还原动画
// controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
},
child: const Text('缩放 + 淡入淡出'),
)
],
)
],
),
),
);
}
}
/// 动画同步使用
class AnimatedLogoSync extends AnimatedWidget {
AnimatedLogoSync({super.key, required Animation<double> animation})
: super(listenable: animation);
final Tween<double> _opacityTween = Tween<double>(begin: 0.1, end: 1);
final Tween<double> _sizeTween = Tween<double>(begin: 50, end: 100);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Opacity(
opacity: _opacityTween.evaluate(animation),
child: SizedBox(
width: _sizeTween.evaluate(animation),
height: _sizeTween.evaluate(animation),
child: const FlutterLogo(),
),
),
);
}
}
隐式动画
- 根据属性值变化,为 UI 中的 widget 添加动作并创造视觉效果,有些库包含各种各样可以帮你管理动画的widget这些 widgets 被统称为 隐式动画 或 隐式动画 widget。
- 前缀是 Animated 的组件,几乎都是 隐式动画 widget;
import 'dart:math';
import 'package:flutter/material.dart';
class ImplicitAnimation extends StatefulWidget {
const ImplicitAnimation({super.key});
@override
State<ImplicitAnimation> createState() => _ImplicitAnimationState();
}
class _ImplicitAnimationState extends State<ImplicitAnimation> {
double opacity = 0;
late Color color;
late double borderRadius;
late double margin;
double randomBorderRadius() {
return Random().nextDouble() * 64;
}
double randomMargin() {
return Random().nextDouble() * 32;
}
Color randomColor() {
return Color(0xFFFFFFFF & Random().nextInt(0xFFFFFFFF));
}
@override
void initState() {
super.initState();
color = randomColor();
borderRadius = randomBorderRadius();
margin = randomMargin();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'隐式动画',
style: TextStyle(fontSize: 20),
)),
body: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedOpacity(
opacity: opacity,
curve: Curves.easeInOutBack,
duration: const Duration(milliseconds: 1000),
child: Container(
width: 50,
height: 50,
margin: const EdgeInsets.only(right: 12),
color: Colors.primaries[2],
),
),
ElevatedButton(
onPressed: () {
if(opacity == 0) {
opacity = 1;
} else {
opacity = 0;
}
setState(() {});
},
child: const Text('淡入或淡出'),
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
width: 50,
height: 50,
margin: EdgeInsets.all(margin),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(borderRadius)
),
curve: Curves.easeInBack,
duration: const Duration(milliseconds: 1000),
),
ElevatedButton(
onPressed: () {
color = randomColor();
borderRadius = randomBorderRadius();
margin = randomMargin();
setState(() {});
},
child: const Text('形状变化'),
)
],
)
],
),
),
);
}
}
显示和隐式的区别
看图,隐式动画 就是 显示动画 封装后的产物,是不是很蒙,这有什么意义?
应用场景不同:如果想 控制动画,使用 显示动画,controller.forward()、controller.reverse()、controller.reset(),反之只是在Widget属性值发生改变,进行UI过渡这种简单操作,使用 隐式动画;
误区
Flutter显式动画的关键对象 Tween,翻译过来 补间,联想到 Android原生的补间动画,就会有一个问题,Android原生的补间动画,只是视觉上的UI变化,对象属性并非真正改变,那么Flutter是否也是如此?
答案:非也,是真的改变了,和Android原生补间动画不同,看图:
以下偏移动画,在Flutter中的,点击偏移后的矩形位置,可以触发提示,反之Android原生不可以,只能在矩形原来的位置,才能触发;
Flutte 提示库 以及 封装相关 的代码
fluttertoast: ^8.2.4
toast_util.dart
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
class ToastUtil {
static FToast fToast = FToast();
static void init(BuildContext context) {
fToast.init(context);
}
static void showToast(String msg) {
Widget toast = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25.0),
color: Colors.greenAccent,
),
alignment: Alignment.center,
child: Text(msg),
)
],
);
fToast.showToast(
child: toast,
gravity: ToastGravity.BOTTOM,
toastDuration: const Duration(seconds: 2),
);
}
}
Flutter显示动画 代码
import 'package:flutter/material.dart';
import 'package:flutter_animation/util/toast_util.dart';
class TweenAnimation extends StatefulWidget {
const TweenAnimation({super.key});
@override
State<TweenAnimation> createState() => _TweenAnimationState();
}
/// 测试显式动画,属性是否真的改变了
class _TweenAnimationState extends State<TweenAnimation>
with SingleTickerProviderStateMixin {
late Animation<Offset> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
animation =
Tween<Offset>(begin: const Offset(0, 0), end: const Offset(1.5, 0))
.animate(controller)
..addStatusListener((status) {
debugPrint('status:$status'); // 监听动画执行状态
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'Flutter 显式动画',
style: TextStyle(fontSize: 20),
)),
body: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.primaries[5],
child: Stack(
children: [
Align(
alignment: Alignment.center,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 1.0),
),
),
),
Align(
alignment: Alignment.center,
child: SlideTransition(
position: animation,
child: InkWell(
onTap: () {
ToastUtil.showToast('点击了');
},
child: Container(
width: 80,
height: 80,
color: Colors.primaries[2],
),
),
),
),
Positioned(
left: (MediaQuery.of(context).size.width / 2) - 35,
top: 200,
child: ElevatedButton(
onPressed: () {
if (controller.isCompleted) {
controller.reverse();
} else {
controller.forward();
}
// controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
// controller.reverse(); // 反向,当动画已经完成,进行还原动画
// controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
},
child: const Text('偏移'),
),
)
],
),
),
);
}
}
Flutter隐式动画 代码
import 'package:flutter/material.dart';
import 'package:flutter_animation/util/toast_util.dart';
class ImplicitAnimation extends StatefulWidget {
const ImplicitAnimation({super.key});
@override
State<ImplicitAnimation> createState() => _ImplicitAnimationState();
}
/// 测试隐式动画,属性是否真的改变了
class _ImplicitAnimationState extends State<ImplicitAnimation> {
late double offsetX = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'Flutter 隐式动画',
style: TextStyle(fontSize: 20),
)),
body: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.primaries[5],
child: Stack(
children: [
Align(
alignment: Alignment.center,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 1.0),
),
),
),
Align(
alignment: Alignment.center,
child: AnimatedSlide(
offset: Offset(offsetX, 0),
duration: const Duration(milliseconds: 500),
child: InkWell(
onTap: () {
ToastUtil.showToast('点击了');
},
child: Container(
width: 80,
height: 80,
color: Colors.primaries[2],
),
),
),
),
Positioned(
left: (MediaQuery.of(context).size.width / 2) - 35,
top: 200,
child: ElevatedButton(
onPressed: () {
if (offsetX == 0) {
offsetX = 1.5;
} else {
offsetX = 0;
}
setState(() {});
},
child: const Text('偏移'),
),
)
],
),
),
);
}
}
Android原生补间动画 代码
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@android:color/holo_blue_light"
android:gravity="center|left"
android:text="Android原生 补间动画"
android:paddingStart="16dp"
android:textColor="@android:color/white"
android:textSize="20sp" />
<TextView
android:id="@+id/border"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:layout_marginBottom="50dp"
android:background="@drawable/border" />
<TextView
android:id="@+id/offset_box"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:layout_marginBottom="50dp"
android:background="@android:color/holo_orange_light" />
<Button
android:id="@+id/offset_x"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="12dp"
android:text="偏移" />
</FrameLayout>
import android.app.Activity
import android.os.Bundle
import android.view.View
import android.view.animation.TranslateAnimation
import android.widget.Toast
import com.example.flutter_animation.databinding.ActivityMainBinding
class MainActivity : Activity(), View.OnClickListener {
private lateinit var bind: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bind = ActivityMainBinding.inflate(layoutInflater)
setContentView(bind.root)
bind.offsetX.setOnClickListener(this)
bind.offsetBox.setOnClickListener(this)
}
private fun offsetAnimation() {
val translateAnimation = TranslateAnimation(0f, 200f, 0f, 0f)
translateAnimation.duration = 800
translateAnimation.fillAfter = true
bind.offsetBox.startAnimation(translateAnimation)
}
override fun onClick(v: View?) {
if (bind.offsetX == v) {
offsetAnimation()
} else if (bind.offsetBox == v) {
Toast.makeText(this,"点击了",Toast.LENGTH_SHORT).show()
}
}
}
Hero动画
应用于 元素共享 的动画。
下面这三个图片详情案例的使用方式,将 Widget 从 A页面 共享到 B页面 后,改变Widget大小,被称为 标准 hero 动画;
图片详情案例一:本地图片
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
class HeroAnimation extends StatefulWidget {
const HeroAnimation({super.key});
@override
State<HeroAnimation> createState() => _HeroAnimationState();
}
/// 将 Widget 从 A页面 共享到 B页面 后,改变Widget大小
class _HeroAnimationState extends State<HeroAnimation> {
/// 测试本地图片
final List<String> images = [
'assets/images/01.jpg',
'assets/images/02.jpg',
'assets/images/03.jpg',
'assets/images/04.jpg',
];
@override
Widget build(BuildContext context) {
// 减慢动画速度,可以通过此值帮助开发,
// 注意这个值是针对所有动画,所以路由动画也会受影响
// timeDilation = 10.0;
return Scaffold(
appBar: AppBar(
title: const Text('Photo List Page'),
),
body: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
alignment: Alignment.topLeft,
child: GridView.count(
padding: const EdgeInsets.all(10),
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
children: List.generate(
images.length,
(index) => PhotoHero(
photo: images[index],
size: 100,
onTap: () {
Navigator.of(context).push(CupertinoPageRoute<void>(
builder: (context) => PhotoDetail(
size: MediaQuery.of(context).size.width,
photo: images[index]),
));
},
)),
),
),
);
}
}
class PhotoHero extends StatelessWidget {
const PhotoHero({
super.key,
required this.photo,
this.onTap,
required this.size,
});
final String photo;
final VoidCallback? onTap;
final double size;
@override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: Hero(
tag: photo,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
/// 测试本地图片
child: Image.asset(
photo,
fit: BoxFit.cover,
),
),
),
),
);
}
}
class PhotoDetail extends StatelessWidget {
const PhotoDetail({
super.key,
required this.photo,
required this.size,
});
final String photo;
final double size;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Photo Detail Page'),
),
body: Column(
children: [
Container(
color: Colors.lightBlueAccent,
padding: const EdgeInsets.all(16),
alignment: Alignment.topCenter,
child: PhotoHero(
photo: photo,
size: size,
onTap: () {
Navigator.of(context).pop();
},
),
),
const Text(
'详情xxx',
style: TextStyle(fontSize: 20),
)
],
),
);
}
}
图片详情案例二:网络图片
可以看出,在有延迟的情况下,效果没有本地图片好;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
class HeroAnimation extends StatefulWidget {
const HeroAnimation({super.key});
@override
State<HeroAnimation> createState() => _HeroAnimationState();
}
/// 将 Widget 从 A页面 共享到 B页面 后,改变Widget大小
class _HeroAnimationState extends State<HeroAnimation> {
/// 测试网络图片
final List<String> images = [
'https://img1.baidu.com/it/u=1161835547,3275770506&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500',
'https://p9.toutiaoimg.com/origin/pgc-image/6d817289d3b44d53bb6e55aa81e41bd2?from=pc',
'https://img0.baidu.com/it/u=102503057,4196586556&fm=253&fmt=auto&app=138&f=BMP?w=500&h=724',
'https://lmg.jj20.com/up/allimg/1114/041421115008/210414115008-3-1200.jpg',
];
@override
Widget build(BuildContext context) {
// 减慢动画速度,可以通过此值帮助开发,
// 注意这个值是针对所有动画,所以路由动画也会受影响
// timeDilation = 10.0;
return Scaffold(
appBar: AppBar(
title: const Text('Photo List Page'),
),
body: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
alignment: Alignment.topLeft,
child: GridView.count(
padding: const EdgeInsets.all(10),
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
children: List.generate(
images.length,
(index) => PhotoHero(
photo: images[index],
size: 100,
onTap: () {
Navigator.of(context).push(CupertinoPageRoute<void>(
builder: (context) => PhotoDetail(
size: MediaQuery.of(context).size.width,
photo: images[index]),
));
},
)),
),
),
);
}
}
class PhotoHero extends StatelessWidget {
const PhotoHero({
super.key,
required this.photo,
this.onTap,
required this.size,
});
final String photo;
final VoidCallback? onTap;
final double size;
@override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: Hero(
tag: photo,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
/// 测试网络图片
child: Image.network(
photo,
fit: BoxFit.cover,
),
),
),
),
);
}
}
class PhotoDetail extends StatelessWidget {
const PhotoDetail({
super.key,
required this.photo,
required this.size,
});
final String photo;
final double size;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Photo Detail Page'),
),
body: Column(
children: [
Container(
color: Colors.lightBlueAccent,
padding: const EdgeInsets.all(16),
alignment: Alignment.topCenter,
child: PhotoHero(
photo: photo,
size: size,
onTap: () {
Navigator.of(context).pop();
},
),
),
const Text(
'详情xxx',
style: TextStyle(fontSize: 20),
)
],
),
);
}
}
图片详情案例三:背景透明
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
class HeroAnimation extends StatefulWidget {
const HeroAnimation({super.key});
@override
State<HeroAnimation> createState() => _HeroAnimationState();
}
/// 测试 新页面背景透明色 的图片详情
class _HeroAnimationState extends State<HeroAnimation> {
/// 测试本地图片
final List<String> images = [
'assets/images/01.jpg',
'assets/images/02.jpg',
'assets/images/03.jpg',
'assets/images/04.jpg',
];
@override
Widget build(BuildContext context) {
// 减慢动画速度,可以通过此值帮助开发,
// 注意这个值是针对所有动画,所以路由动画也会受影响
// timeDilation = 10.0;
return Scaffold(
appBar: AppBar(
title: const Text('Photo List Page'),
),
body: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
alignment: Alignment.topLeft,
child: GridView.count(
padding: const EdgeInsets.all(10),
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
children: List.generate(
images.length,
(index) => PhotoHero(
photo: images[index],
size: 100,
onTap: () {
Navigator.of(context).push(
PageRouteBuilder<void>(
opaque: false, // 新页面,背景色不透明度
pageBuilder: (context, animation, secondaryAnimation) {
return PhotoDetail(
size: MediaQuery.of(context).size.width,
photo: images[index]);
},
),
);
},
)),
),
),
);
}
}
class PhotoHero extends StatelessWidget {
const PhotoHero({
super.key,
required this.photo,
this.onTap,
required this.size,
});
final String photo;
final VoidCallback? onTap;
final double size;
@override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: Hero(
tag: photo,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
/// 测试本地图片
child: Image.asset(
photo,
fit: BoxFit.cover,
),
),
),
),
);
}
}
class PhotoDetail extends StatelessWidget {
const PhotoDetail({
super.key,
required this.photo,
required this.size,
});
final String photo;
final double size;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
// backgroundColor: const Color(0x66000000),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.center,
child: PhotoHero(
photo: photo,
size: size,
onTap: () {
Navigator.of(context).pop();
},
),
),
const Text(
'详情xxx',
style: TextStyle(fontSize: 20,color: Colors.white),
)
],
),
);
}
}
图片形状转换案例:圆形 转 矩形
这个案例的使用方式,被称为 径向hero动画;
- 径向hero动画的 径 是半径距离,圆形状 向 矩形状转换,矩形状的对角半径距离 = 圆形状半径距离 * 2;
- 这个是官方模版代码,我也没改什么;
- 官方代码地址:https://github.com/cfug/flutter.cn/blob/main/examples/_animation/radial_hero_animation/lib/main.dart
- 问题:这种官方代码是 初始化为 圆形 点击向 矩形改变的方式,我尝试反向操作:初始化为 矩形 点击向 圆形改变,但没有成功,如果有哪位同学找到实现方式,麻烦评论区留言;
我是这样修改的:
class RadialExpansion extends StatelessWidget {
... ...
@override
Widget build(BuildContext context) {
/// 原来的代码
// 控制形状变化的核心代码
// return ClipOval( // 圆形
// child: Center(
// child: SizedBox(
// width: clipRectSize,
// height: clipRectSize,
// child: ClipRect( // 矩形
// child: child,
// ),
// ),
// ),
// );
/// 尝试修改 形状顺序
return ClipRect( // 矩形
child: Center(
child: SizedBox(
width: clipRectSize,
height: clipRectSize,
child: ClipOval( // 圆形
child: child,
),
),
),
);
}
}
官方代码演示
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'package:flutter/scheduler.dart' show timeDilation;
class HeroAnimation extends StatefulWidget {
const HeroAnimation({super.key});
@override
State<HeroAnimation> createState() => _HeroAnimationState();
}
/// 将 Widget 从 A页面 共享到 B页面 后,改变Widget形状
class _HeroAnimationState extends State<HeroAnimation> {
static double kMinRadius = 32.0;
static double kMaxRadius = 128.0;
static Interval opacityCurve =
const Interval(0.0, 0.75, curve: Curves.fastOutSlowIn);
static RectTween _createRectTween(Rect? begin, Rect? end) {
return MaterialRectCenterArcTween(begin: begin, end: end);
}
static Widget _buildPage(
BuildContext context, String imageName, String description) {
return Container(
color: Theme.of(context).canvasColor,
child: Center(
child: Card(
elevation: 8,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: kMaxRadius * 2.0,
height: kMaxRadius * 2.0,
child: Hero(
createRectTween: _createRectTween,
tag: imageName,
child: RadialExpansion(
maxRadius: kMaxRadius,
child: Photo(
photo: imageName,
onTap: () {
Navigator.of(context).pop();
},
),
),
),
),
Text(
description,
style: const TextStyle(fontWeight: FontWeight.bold),
textScaleFactor: 3,
),
const SizedBox(height: 16),
],
),
),
),
);
}
Widget _buildHero(
BuildContext context,
String imageName,
String description,
) {
return SizedBox(
width: kMinRadius * 2.0,
height: kMinRadius * 2.0,
child: Hero(
createRectTween: _createRectTween,
tag: imageName,
child: RadialExpansion(
maxRadius: kMaxRadius,
child: Photo(
photo: imageName,
onTap: () {
Navigator.of(context).push(
PageRouteBuilder<void>(
pageBuilder: (context, animation, secondaryAnimation) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Opacity(
opacity: opacityCurve.transform(animation.value),
child: _buildPage(context, imageName, description),
);
},
);
},
),
);
},
),
),
),
);
}
@override
Widget build(BuildContext context) {
timeDilation = 5.0; // 1.0 is normal animation speed.
return Scaffold(
appBar: AppBar(
title: const Text('Radial Transition Demo'),
),
body: Container(
padding: const EdgeInsets.all(32),
alignment: FractionalOffset.bottomLeft,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildHero(context, 'assets/images/01.jpg', 'Chair'),
_buildHero(context, 'assets/images/02.jpg', 'Binoculars'),
_buildHero(context, 'assets/images/03.jpg', 'Beach ball'),
_buildHero(context, 'assets/images/04.jpg', 'Beach ball'),
],
),
),
);
}
}
class Photo extends StatelessWidget {
const Photo({super.key, required this.photo, this.onTap});
final String photo;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return Material(
// Slightly opaque color appears where the image has transparency.
color: Theme.of(context).primaryColor.withOpacity(0.25),
child: InkWell(
onTap: onTap,
child: LayoutBuilder(
builder: (context, size) {
return Image.asset(
photo,
fit: BoxFit.contain,
);
},
),
),
);
}
}
class RadialExpansion extends StatelessWidget {
const RadialExpansion({
super.key,
required this.maxRadius,
this.child,
}) : clipRectSize = 2.0 * (maxRadius / math.sqrt2);
final double maxRadius;
final double clipRectSize;
final Widget? child;
@override
Widget build(BuildContext context) {
// 控制形状变化的核心代码
return ClipOval( // 圆形
child: Center(
child: SizedBox(
width: clipRectSize,
height: clipRectSize,
child: ClipRect( // 矩形
child: child,
),
),
),
);
}
}
页面转场动画
在自定义路由时,添加动画,自定义路由需要用到PageRouteBuilder<T>;
import 'package:flutter/material.dart';
/// 为页面切换加入动画效果
class PageAnimation extends StatefulWidget {
const PageAnimation({super.key});
@override
State<PageAnimation> createState() => _PageAnimationState();
}
class _PageAnimationState extends State<PageAnimation> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'为页面切换加入动画效果',
style: TextStyle(fontSize: 20),
)),
body: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context).push(_createRouteX());
},
child: const Text(
'X轴偏移',
style: TextStyle(fontSize: 20),
)),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(_createRouteY());
},
child: const Text(
'Y轴偏移',
style: TextStyle(fontSize: 20),
)),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(_createRouteMix());
},
child: const Text(
'混合动画',
style: TextStyle(fontSize: 20),
)),
],
),
),
);
}
/// X轴 平移动画,切换页面
Route _createRouteX() {
return PageRouteBuilder(
// opaque: false, // 新页面,背景色不透明度
pageBuilder: (context, animation, secondaryAnimation) => const TestPage01(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(1.0, 0.0); // 将 dx 参数设为 1,这代表在水平方向左切换整个页面的宽度
const end = Offset.zero;
const curve = Curves.ease;
var tween =
Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
});
}
/// Y轴 平移动画,切换页面
Route _createRouteY() {
return PageRouteBuilder(
// opaque: false, // 新页面,背景色不透明度
pageBuilder: (context, animation, secondaryAnimation) => const TestPage01(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(0.0, 1.0); // 将 dy 参数设为 1,这代表在竖直方向上切换整个页面的高度
const end = Offset.zero;
const curve = Curves.ease;
var tween =
Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
});
}
/// 多个动画配合,切换页面
Route _createRouteMix() {
return PageRouteBuilder(
// opaque: false, // 新页面,背景色不透明度
pageBuilder: (context, animation, secondaryAnimation) => const TestPage01(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
var tween = Tween<double>(begin: 0.1, end: 1.0)
.chain(CurveTween(curve: Curves.ease));
return FadeTransition(
// 淡入淡出
opacity: animation.drive(tween),
child: RotationTransition(
// 旋转
turns: animation.drive(tween),
child: ScaleTransition(
// 更替
scale: animation.drive(tween),
child: child,
),
),
);
});
}
}
class TestPage01 extends StatelessWidget {
const TestPage01({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.lightBlue,
appBar: AppBar(
title: const Text('TestPage01'),
),
);
}
}
交错动画
多个动画配合使用
这个案例是官方的,原汁原味;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
class IntertwinedAnimation extends StatefulWidget {
const IntertwinedAnimation({super.key});
@override
State<IntertwinedAnimation> createState() => _IntertwinedAnimationState();
}
class _IntertwinedAnimationState extends State<IntertwinedAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _playAnimation() async {
try {
await _controller.forward().orCancel;
await _controller.reverse().orCancel;
} on TickerCanceled {}
}
@override
Widget build(BuildContext context) {
// timeDilation = 10.0;
return Scaffold(
appBar: AppBar(
title: const Text(
'交错动画',
style: TextStyle(fontSize: 20),
)),
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
_playAnimation();
},
child: Center(
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.1),
border: Border.all(
color: Colors.black.withOpacity(0.5),
)),
child: StaggerAnimation(controller: _controller),
),
),
),
);
}
}
class StaggerAnimation extends StatelessWidget {
final Animation<double> controller;
final Animation<double> opacity;
final Animation<double> width;
final Animation<double> height;
final Animation<EdgeInsets> padding;
final Animation<BorderRadius?> borderRadius;
final Animation<Color?> color;
StaggerAnimation({super.key, required this.controller})
: opacity = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: controller,
curve: const Interval(
0.0,
0.100,
curve: Curves.ease,
))),
width = Tween<double>(
begin: 50.0,
end: 150.0,
).animate(CurvedAnimation(
parent: controller,
curve: const Interval(
0.125,
0.250,
curve: Curves.ease,
))),
height = Tween<double>(begin: 50.0, end: 150.0).animate(CurvedAnimation(
parent: controller,
curve: const Interval(
0.250,
0.375,
curve: Curves.ease,
))),
padding = EdgeInsetsTween(
begin: const EdgeInsets.only(bottom: 16),
end: const EdgeInsets.only(bottom: 75),
).animate(CurvedAnimation(
parent: controller,
curve: const Interval(
0.250,
0.375,
curve: Curves.ease,
))),
borderRadius = BorderRadiusTween(
begin: BorderRadius.circular(4),
end: BorderRadius.circular(75),
).animate(CurvedAnimation(
parent: controller,
curve: const Interval(
0.375,
0.500,
curve: Curves.ease,
))),
color = ColorTween(begin: Colors.indigo[100], end: Colors.orange[400])
.animate(CurvedAnimation(
parent: controller,
curve: const Interval(
0.500,
0.750,
curve: Curves.ease,
)));
Widget _buildAnimation(BuildContext context, Widget? child) {
return Container(
padding: padding.value,
alignment: Alignment.bottomCenter,
child: Opacity(
opacity: opacity.value,
child: Container(
width: width.value,
height: height.value,
decoration: BoxDecoration(
color: color.value,
border: Border.all(
color: Colors.indigo[300]!,
width: 3,
),
borderRadius: borderRadius.value),
),
),
);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
builder: _buildAnimation,
animation: controller,
);
}
}
依次执行动画
这个案例是根据官方demo改的,它那个太复杂了,不利于新手阅读(个人觉得);
官方文档:创建一个交错效果的侧边栏菜单 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter
import 'package:flutter/material.dart';
class Intertwined02Animation extends StatefulWidget {
const Intertwined02Animation({super.key});
@override
State<Intertwined02Animation> createState() => _Intertwined02AnimationState();
}
class _Intertwined02AnimationState extends State<Intertwined02Animation> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: const TableList(),
// child: const Column(
// crossAxisAlignment: CrossAxisAlignment.center,
// children: [
// TableList()
// ],
// ),
),
);
}
}
class TableList extends StatefulWidget {
const TableList({super.key});
@override
State<TableList> createState() => _TableListState();
}
class _TableListState extends State<TableList> with SingleTickerProviderStateMixin {
/// 遍历循环写法
late AnimationController _controller;
final Duration _durationTime = const Duration(milliseconds: 3000);
@override
initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: _durationTime);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
/// 遍历Interval
List<Interval> _createInterval() {
List<Interval> intervals = [];
// Interval(0.0,0.5);
// Interval(0.5,0.75);
// Interval(0.75,1.0);
double begin = 0.0;
double end = 0.5;
for (int i = 0; i < 3; i++) {
if (i == 0) {
intervals.add(Interval(begin, end));
} else {
begin = end;
end = begin + 0.25;
intervals.add(Interval(begin, end));
}
// debugPrint('begin:$begin --- end:$end');
}
return intervals;
}
/// 遍历循环组件
List<Widget> _createWidget() {
var intervals = _createInterval();
List<Widget> listItems = [];
for (int i = 0; i < 3; i++) {
listItems.add(AnimatedBuilder(
animation: _controller,
builder: (context, child) {
var animationPercent = Curves.easeOut.transform(intervals[i].transform(_controller.value));
final opacity = animationPercent;
final slideDistance = (1.0 - animationPercent) * 150;
return Opacity(
opacity: i == 2 ? opacity : 1,
child: Transform.translate(
offset: Offset(slideDistance, 100 + (i * 50)),
child: child,
));
},
child: Container(
width: 100,
height: 50,
color: Colors.lightBlue,
),
));
}
return listItems;
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Column(
children: _createWidget(),
),
);
}
/// 非遍历循环写法
// late AnimationController _controller;
//
// final Interval _intervalA = const Interval(0.0, 0.5);
// final Interval _intervalB = const Interval(0.5, 0.8);
// final Interval _intervalC = const Interval(0.8, 1.0);
//
// final Duration _durationTime = const Duration(milliseconds: 3000);
//
// @override
// void initState() {
// super.initState();
// _controller = AnimationController(vsync: this, duration: _durationTime);
// _controller.forward();
// }
//
// @override
// void dispose() {
// _controller.dispose();
// super.dispose();
// }
//
// @override
// Widget build(BuildContext context) {
// return SizedBox(
// width: MediaQuery.of(context).size.width,
// height: MediaQuery.of(context).size.height,
// child: Column(
// children: [
// AnimatedBuilder(
// animation: _controller,
// builder: (context,child) {
// var animationPercent = Curves.easeOut.transform(_intervalA.transform(_controller.value));
// final slideDistance = (1.0 - animationPercent) * 150;
// return Transform.translate(
// offset: Offset(slideDistance,100),
// child: child
// );
// },
// child: Container(
// width: 100,
// height: 50,
// color: Colors.lightBlue,
// ),
// ),
// AnimatedBuilder(
// animation: _controller,
// builder: (context,child) {
// var animationPercent = Curves.easeOut.transform(_intervalB.transform(_controller.value));
// final slideDistance = (1.0 - animationPercent) * 150;
// return Transform.translate(
// offset: Offset(slideDistance,150),
// child: child
// );
// },
// child: Container(
// width: 100,
// height: 50,
// color: Colors.lightBlue,
// ),
// ),
// AnimatedBuilder(
// animation: _controller,
// builder: (context,child) {
// var animationPercent = Curves.easeOut.transform(_intervalC.transform(_controller.value));
// final opacity = animationPercent;
// final slideDistance = (1.0 - animationPercent) * 150;
// return Opacity(
// opacity: opacity,
// child: Transform.translate(
// offset: Offset(slideDistance,200),
// child: child
// ),
// );
// },
// child: Container(
// width: 100,
// height: 50,
// color: Colors.lightBlue,
// ),
// ),
// ],
// ),
// );
// }
/// 基础版本写法
// late AnimationController _controller;
// final Duration _durationTime = const Duration(milliseconds: 2000);
// // 0.0 - 1.0 / 0% - 100%
// final Interval _interval = const Interval(0.5, 1.0); // 延迟 50% 再开始 启动动画,执行到 100%
// // final Interval _interval = const Interval(0.5, 0.7); // 延迟 50% 再开始 启动动画,后期的执行速度,增加 30%
// // final Interval _interval = const Interval(0.0, 0.1); // 不延迟 动画执行速度,增加 90%
//
// @override
// void initState() {
// super.initState();
// _controller = AnimationController(vsync: this, duration: _durationTime);
// _controller.forward();
// }
//
// @override
// void dispose() {
// _controller.dispose();
// super.dispose();
// }
// @override
// Widget build(BuildContext context) {
// return AnimatedBuilder(
// animation: _controller,
// builder: (context,child) {
// // var animationPercent = Curves.easeOut.transform(_controller.value); // 加动画曲线
// // var animationPercent = _interval.transform(_controller.value); // 加动画间隔
// var animationPercent = Curves.easeOut.transform(_interval.transform(_controller.value)); // 动画曲线 + 动画间隔
//
// final slideDistance = (1.0 - animationPercent) * 150; // 就是对150 做递减
// // debugPrint('animationPercent:$animationPercent --- slideDistance:$slideDistance');
// debugPrint('slideDistance:$slideDistance');
//
// return Transform.translate(
// offset: Offset(0,slideDistance),
// child: child
// );
// },
// child: Container(
// width: 100,
// height: 50,
// color: Colors.lightBlue,
// ),
// );
// }
}
官方文档
动画效果介绍 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter