demo 地址: https://github.com/iotjin/jh_flutter_demo
代码不定时更新,请前往github查看最新代码
APP 主页面的tabbar点击时有些有一些tabbar图标缩放或跳动动画,还有文字点击时有抖动效果
本文通过flutter 实现一些缩放或位移动画。封装成几个自定义组件。
这几个自定义的动画组件可以实现子组件通过属性或者方法调用动画
动画效果:抖动,左右晃动,先放大再缩小等,也支持自定义
类似效果的三方库:
animate_do: ^3.0.2
文章:
flutter动画简介
动画是指将一系列静态图像连续播放,形成一种视觉上的动态效果。在Flutter中,动画是通过对属性进行插值计算,逐步改变属性值,从而实现动态效果的。
在Flutter中,动画是通过一系列的动画对象和控制器来实现的。动画对象通常是一个值的插值器,可以将一个值从一个范围映射到另一个范围。Flutter中提供了多种类型的动画对象,如Tween、Curve等。
动画控制器则用于控制动画的状态和进度。控制器可以启动、停止、反转动画,并且可以监听动画的状态变化和进度变化。Flutter中提供了AnimationController类来实现动画控制器。
在Flutter中,动画可以分为两种类型:显式动画和隐式动画。
显式动画:这种动画是由开发人员自己定义的,通常使用Flutter提供的AnimationController
动画类来实现。开发人员可以通过设置控制器的属性来控制动画的开始、结束、暂停和恢复等操作。例如,Animation、Tween和Curve等类可以用于创建各种类型的动画效果。
隐式动画:这种动画是由框架自动处理的,通过AnimatedWidget类来实现的,它会自动根据动画的变化来更新UI界面。例如,当应用中的Widget发生变化时,Flutter会自动创建一个动画来平滑地过渡到新的Widget状态。
- Flutter动画框架是基于
Animation
类的。Animation
类是一个抽象类,它定义了动画的基本属性和方法。- Flutter提供了许多
Animation
的子类,包括Tween
、Curve
、Interval
和AnimationController
等。Tween
类用于定义动画的起始值和结束值,Curve
类用于定义动画的加速和减速曲线,Interval
类用于定义动画的时间间隔,AnimationController
类用于定义动画的控制器。
- AnimationController:动画控制器,用于控制动画的开始、结束、暂停、恢复等操作。可以设置动画的持续时间、速度曲线等属性。
- Tween:插值器,用于计算属性值的变化。可以设置属性的起始值和结束值,以及插值的类型(如线性插值、曲线插值等)。它可以是一个数字、一个颜色、一个矩形或任何其他类型的值。
- Animation:动画对象,用于保存属性值的变化。表示动画的开始和结束值。它可以是一个数字、一个颜色、一个矩形或任何其他类型的值。可以通过addListener()方法监听动画的变化,并在回调函数中更新UI。
- Listener:监听动画的状态变化,例如动画开始、结束、停止等。
- AnimatedBuilder:动画构建器,用于构建动画组件。可以将Animation对象传递给子组件,使子组件随着动画变化而变化。
- Curve:表示动画的时间曲线,用于控制动画的变化速度。可以设置曲线类型(如匀速、加速、减速、先加速后减速等)。
- Hero动画:用于在路由跳转时实现两个页面中同一元素的平滑过渡效果。可以将两个页面中的同一元素包裹在Hero组件中,并设置唯一的tag值。
- AnimatedSwitcher:用于在多个组件之间实现平滑的切换效果。可以将多个组件包裹在AnimatedSwitcher组件中,并在组件切换时设置不同的key值。
Flutter的 AnimationStatus
枚举类型定义了动画的四种状态(begin: 0.0, end: 1.0时):
- dismissed:动画已经停止,并且值已经回到初始状态。即动画的值为0.0。
- forward:动画正在正向播放。即动画的值从0.0逐渐增加到1.0。(正向运行)
- reverse:动画正在反向播放。即动画的值从1.0逐渐减少到0.0。(反向运行)
- completed:动画已经停止,并且值已经达到最终状态。即动画的值为1.0。
动画的控制方法:
- forward:正向执行动画。
- reverse:反向执行动画。
- repeat:反复执行动画。
- reset:重置动画。
Flutter中常见的 Curve 类型有以下几种:
一个Curve 类型动画的网站
https://cubic-bezier.com/
Curves.linear:线性动画曲线,即匀速运动,适用于需要匀速移动的场景。
Curves.ease:默认的缓动动画曲线,适用于大部分场景。
Curves.easeIn:快速进入动画曲线,适用于需要快速进入的场景,如按钮点击等。
Curves.easeOut:快速退出动画曲线,适用于需要快速退出的场景,如弹窗关闭等。
Curves.easeInOut:快速进入并快速退出动画曲线,适用于需要快速进入并快速退出的场景,如页面切换等。
Curves.fastLinearToSlowEaseIn:快速匀速运动然后慢速进入动画曲线,适用于需要快速匀速运动然后慢速进入的场景,如列表滚动等。
Curves.bounceIn:弹性进入动画曲线,适用于需要弹性进入的场景,如弹窗出现等。
Curves.bounceOut:弹性退出动画曲线,适用于需要弹性退出的场景,如弹窗关闭等。
Curves.elasticIn:弹性进入动画曲线,适用于需要弹性进入的场景,如下拉刷新等。
Curves.elasticOut:弹性退出动画曲线,适用于需要弹性退出的场景,如列表滑动到底部等。
不同的 Curve 类型适用于不同的场景,根据具体的需求来选择合适的 Curve 类型可以让动画效果更加自然和流畅。
例如,如果需要实现一个快速进入的按钮点击效果,可以选择 Curves.easeIn;
如果需要实现一个缓慢的列表滚动效果,可以选择 Curves.fastLinearToSlowEaseIn;
如果需要实现一个弹性出现的弹窗效果,可以选择 Curves.bounceIn 等。
Flutter提供了多种实现动画的方式,包括一些隐式动画(如Opacity、AnimatedContainer、AnimatedPositioned),Tween动画、Curve动画、组合动画等。
可以使用 Flutter 的隐式动画来实现平滑的过渡效果。以下是一些常见的隐式动画示例:
Opacity(
opacity: _visible ? 1.0 : 0.0,
duration: Duration(milliseconds: 500),
child: Container(
// your widget
),
)
AnimatedContainer(
duration: Duration(milliseconds: 500),
curve: Curves.easeInOut,
width: _expanded ? 200.0 : 100.0,
height: _expanded ? 200.0 : 100.0,
child: Container(
// your widget
),
)
AnimatedContainer(
duration: Duration(milliseconds: 500),
curve: Curves.easeInOut,
transform: Matrix4.rotationZ(_expanded ? pi / 4 : 0),
child: Container(
// your widget
),
)
AnimatedPositioned(
duration: Duration(milliseconds: 500),
curve: Curves.easeInOut,
left: _expanded ? 100.0 : 0.0,
top: _expanded ? 100.0 : 0.0,
child: Container(
// your widget
),
)
AnimatedContainer(
duration: Duration(milliseconds: 500),
curve: Curves.easeInOut,
color: _expanded ? Colors.red : Colors.blue,
child: Container(
// your widget
),
)
Tween动画是Flutter中最基本的动画类型之一。它用于在两个值之间进行插值,从而创建一个平滑的过渡效果。例如,我们可以使用Tween动画在两个颜色之间创建一个渐变效果。以下示例代码演示了如何使用Tween动画创建一个颜色渐变效果:
class ColorTweenAnimation extends StatefulWidget {
_ColorTweenAnimationState createState() => _ColorTweenAnimationState();
}
class _ColorTweenAnimationState extends State<ColorTweenAnimation>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<Color> _animation;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true);
_animation = ColorTween(
begin: Colors.red,
end: Colors.blue,
).animate(_controller);
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Container(
color: _animation.value,
height: 200,
width: 200,
);
}
}
Curve动画用于控制动画的时间曲线。Flutter提供了许多预定义的Curve曲线,例如线性曲线、加速曲线、减速曲线等。以下示例代码演示了如何使用Curve动画创建一个弹跳效果:
class BounceAnimation extends StatefulWidget {
_BounceAnimationState createState() => _BounceAnimationState();
}
class _BounceAnimationState extends State<BounceAnimation>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _animation;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..repeat(reverse: true);
_animation = Tween<double>(
begin: 0,
end: 100,
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.bounceOut,
),
);
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Container(
alignment: Alignment.bottomCenter,
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
height: _animation.value,
width: 50,
color: Colors.red,
);
},
),
);
}
}
Hero动画是Flutter中非常流行的一种动画类型。它用于在不同屏幕之间平滑地过渡共享元素。例如,我们可以在两个不同的屏幕之间创建一个Hero动画,使得共享元素平滑地过渡到新的屏幕状态。以下示例代码演示了如何使用Hero动画创建一个共享元素过渡效果:
class HeroAnimation extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => HeroDetailScreen()),
);
},
child: Hero(
tag: 'image',
child: Image.network(
'https://picsum.photos/250?image=9',
height: 300,
width: double.infinity,
fit: BoxFit.cover,
),
),
),
);
}
}
class HeroDetailScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Hero(
tag: 'image',
child: Image.network(
'https://picsum.photos/250?image=9',
height: double.infinity,
width: double.infinity,
fit: BoxFit.cover,
),
),
),
);
}
}
组合动画可以将多个动画组合在一起,使动画效果更加丰富。
AnimationController _controller;
Animation<double> _animation1;
Animation<double> _animation2;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
_animation1 = Tween<double>(
begin: 0,
end: 1,
).animate(_controller);
_animation2 = Tween<double>(
begin: 0,
end: 1,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(0.5, 1),
));
}
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget child) {
return Transform.scale(
scale: _animation1.value + _animation2.value,
child: child,
);
},
child: Container(
width: 200,
height: 200,
color: Colors.red,
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller.forward();
},
child: Icon(Icons.play_arrow),
),
);
}
/// jh_pulse_animation_view.dart
///
/// Created by iotjin on 2023/03/25.
/// description: 跳动动画,先放大再缩小再还原
import 'package:flutter/material.dart';
class JhPulseAnimationView extends StatefulWidget {
const JhPulseAnimationView({
Key? key,
required this.child,
this.duration = const Duration(milliseconds: 300),
this.begin = 1.1,
this.end = 0.9,
this.isAnimating = false,
this.onCompleted,
}) : super(key: key);
final Widget child; // 点击child自身触发动画(方式一)
final Duration duration;
final double begin;
final double end;
final bool isAnimating; // 为true触发动画(方式二)
final Function? onCompleted;
State<JhPulseAnimationView> createState() => _JhPulseAnimationViewState();
}
class _JhPulseAnimationViewState extends State<JhPulseAnimationView> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
void initState() {
super.initState();
_init();
}
void dispose() {
_animationController.dispose();
super.dispose();
}
_init() {
_animationController = AnimationController(vsync: this, duration: widget.duration);
_animation = TweenSequence<double>([
TweenSequenceItem(tween: Tween(begin: 1, end: widget.begin), weight: 1),
TweenSequenceItem(tween: Tween(begin: widget.begin, end: widget.end), weight: 1),
TweenSequenceItem<double>(tween: Tween(begin: widget.end, end: 1), weight: 1),
]).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeIn));
_startAnimation();
}
_startAnimation([isClick = false]) {
if (widget.isAnimating || isClick) {
_animationController.forward().then((value) {
_animationController.reset();
widget.onCompleted?.call();
});
}
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _startAnimation(true),
child: AnimatedBuilder(
animation: _animationController,
builder: (BuildContext context, Widget? child) {
return Transform.scale(
scale: _animation.value,
child: widget.child,
);
},
),
);
}
void didUpdateWidget(covariant JhPulseAnimationView oldWidget) {
// TODO: implement didUpdateWidget
super.didUpdateWidget(oldWidget);
if (widget.isAnimating != oldWidget.isAnimating) {
_startAnimation();
}
}
}
/// jh_scale_animation_view.dart
///
/// Created by iotjin on 2023/03/25.
/// description: 缩放动画,默认放大再还原
import 'package:flutter/material.dart';
class JhScaleAnimationView extends StatefulWidget {
const JhScaleAnimationView({
Key? key,
required this.child,
this.duration = const Duration(milliseconds: 100),
this.begin = 1.0,
this.end = 1.1,
this.isAnimating = false,
this.onCompleted,
}) : super(key: key);
final Widget child; // 点击child自身触发动画(方式一)
final Duration duration;
final double begin;
final double end;
final bool isAnimating; // 为true触发动画(方式二)
final Function? onCompleted;
State<JhScaleAnimationView> createState() => _JhScaleAnimationViewState();
}
class _JhScaleAnimationViewState extends State<JhScaleAnimationView> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
void initState() {
super.initState();
_init();
}
void dispose() {
_animationController.dispose();
super.dispose();
}
_init() {
_animationController = AnimationController(vsync: this, duration: widget.duration);
_animation = Tween<double>(begin: widget.begin, end: widget.end).animate(_animationController)
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_animationController.reverse();
} else if (status == AnimationStatus.dismissed) {
widget.onCompleted?.call();
}
});
_startAnimation();
}
_startAnimation([isClick = false]) {
if (widget.isAnimating || isClick) {
_animationController.forward();
}
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _startAnimation(true),
child: Transform.scale(
scale: _animation.value,
child: widget.child,
),
);
}
void didUpdateWidget(covariant JhScaleAnimationView oldWidget) {
// TODO: implement didUpdateWidget
super.didUpdateWidget(oldWidget);
if (widget.isAnimating != oldWidget.isAnimating) {
_startAnimation();
}
}
}
/// jh_scale_animation_view2.dart
///
/// Created by iotjin on 2023/03/25.
/// description: 缩放动画,默认缩小再还原
import 'package:flutter/material.dart';
class JhScaleAnimationView2 extends StatefulWidget {
const JhScaleAnimationView2({
Key? key,
required this.child,
this.duration = const Duration(milliseconds: 100),
this.begin = 1.0,
this.end = 0.9,
this.isAnimating = false,
this.onCompleted,
}) : super(key: key);
final Widget child; // 点击child自身触发动画(方式一)
final Duration duration;
final double begin;
final double end;
final bool isAnimating; // 为true触发动画(方式二)
final Function? onCompleted;
State<JhScaleAnimationView2> createState() => _JhScaleAnimationView2State();
}
class _JhScaleAnimationView2State extends State<JhScaleAnimationView2> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
void initState() {
super.initState();
_init();
}
void dispose() {
_animationController.dispose();
super.dispose();
}
_init() {
_animationController = AnimationController(vsync: this, duration: widget.duration);
_animation = Tween<double>(begin: widget.begin, end: widget.end).animate(_animationController);
_startAnimation();
}
_startAnimation([isClick = false]) {
if (widget.isAnimating || isClick) {
_animationController.forward().then((value) {
_animationController.reverse().then((value) {
widget.onCompleted?.call();
});
});
}
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _startAnimation(true),
child: ScaleTransition(
scale: _animation,
child: widget.child,
),
);
}
void didUpdateWidget(covariant JhScaleAnimationView2 oldWidget) {
// TODO: implement didUpdateWidget
super.didUpdateWidget(oldWidget);
if (widget.isAnimating != oldWidget.isAnimating) {
_startAnimation();
}
}
}
/// jh_shake_animation_view.dart
///
/// Created by iotjin on 2023/03/25.
/// description: 抖动(位移)动画,支持上下/左右抖动,默认左右抖动
import 'package:flutter/material.dart';
enum ShakeDirection {
horizontal,
vertical,
}
class JhShakeAnimationView extends StatefulWidget {
const JhShakeAnimationView({
Key? key,
required this.child,
this.duration = const Duration(milliseconds: 100),
this.direction = ShakeDirection.horizontal,
this.begin = -5,
this.end = 5,
this.isAnimating = false,
this.onCompleted,
}) : super(key: key);
final Widget child; // 点击child自身触发动画(方式一)
final Duration duration;
final ShakeDirection direction;
final double begin;
final double end;
final bool isAnimating; // 为true触发动画(方式二)
final Function? onCompleted;
State<JhShakeAnimationView> createState() => _JhShakeAnimationViewState();
}
class _JhShakeAnimationViewState extends State<JhShakeAnimationView> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
void initState() {
super.initState();
_init();
}
void dispose() {
_animationController.dispose();
super.dispose();
}
_init() {
_animationController = AnimationController(vsync: this, duration: widget.duration);
_animation = TweenSequence<double>([
TweenSequenceItem(tween: Tween(begin: 0, end: widget.begin), weight: 1),
TweenSequenceItem(tween: Tween(begin: widget.begin, end: widget.end), weight: 1),
TweenSequenceItem(tween: Tween(begin: widget.end, end: 0), weight: 1),
]).animate(_animationController);
_startAnimation();
}
_startAnimation([isClick = false]) {
if (widget.isAnimating || isClick) {
_animationController.forward().then((value) {
_animationController.reset();
widget.onCompleted?.call();
});
}
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _startAnimation(true),
child: AnimatedBuilder(
animation: _animationController,
builder: (BuildContext context, Widget? child) {
var x = widget.direction == ShakeDirection.horizontal ? _animation.value : 0.0;
var y = widget.direction == ShakeDirection.vertical ? _animation.value : 0.0;
return Transform.translate(
offset: Offset(x, y),
child: widget.child,
);
},
),
);
}
void didUpdateWidget(covariant JhShakeAnimationView oldWidget) {
// TODO: implement didUpdateWidget
super.didUpdateWidget(oldWidget);
if (widget.isAnimating != oldWidget.isAnimating) {
_startAnimation();
}
}
}