前语
当时事例 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;
- 这个是官方模版代码,我也没改什么;
- 官方代码地址:github.com/cfug/flutte…
- 问题:这种官方代码是 初始化为 圆形 点击向 矩形改动的办法,我测验反向操作:初始化为 矩形 点击向 圆形改动,但没有成功,假如有哪位同学找到实现办法,麻烦评论区留言;
我是这样修改的:
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改的,它那个太复杂了,不利于新手阅览(个人觉得);
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,
// ),
// );
// }
}