Анимации — важная часть пользовательского опыта. В этом руководстве разберём все типы анимаций во Flutter.
Виды анимаций
Flutter поддерживает два основных типа:
- Implicit animations — простые анимации с минимальным кодом
- Explicit animations — сложные анимации с полным контролем
Implicit анимации
AnimatedContainer
class AnimatedContainerWidget extends StatefulWidget {
@override
State<AnimatedContainerWidget> createState() => _AnimatedContainerWidgetState();
}
class _AnimatedContainerWidgetState extends State<AnimatedContainerWidget> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _expanded ? 200 : 100,
height: _expanded ? 200 : 100,
color: _expanded ? Colors.blue : Colors.red,
child: Center(child: Text('Tap me')),
),
);
}
}
AnimatedOpacity
class FadeWidget extends StatefulWidget {
@override
State<FadeWidget> createState() => _FadeWidgetState();
}
class _FadeWidgetState extends State<FadeWidget> {
double _opacity = 1.0;
@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedOpacity(
opacity: _opacity,
duration: Duration(milliseconds: 500),
child: FlutterLogo(size: 100),
),
Slider(
value: _opacity,
onChanged: (value) => setState(() => _opacity = value),
),
],
);
}
}
AnimatedPositioned
class PositionedWidget extends StatefulWidget {
@override
State<PositionedWidget> createState() => _PositionedWidgetState();
}
class _PositionedWidgetState extends State<PositionedWidget> {
bool _selected = false;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
AnimatedPositioned(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
top: _selected ? 100 : 50,
left: _selected ? 100 : 50,
child: GestureDetector(
onTap: () => setState(() => _selected = !_selected),
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
),
],
),
);
}
}
AnimatedCrossFade
class CrossFadeWidget extends StatefulWidget {
@override
State<CrossFadeWidget> createState() => _CrossFadeWidgetState();
}
class _CrossFadeWidgetState extends State<CrossFadeWidget> {
bool _showFirst = true;
@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedCrossFade(
firstChild: FlutterLogo(size: 100),
secondChild: Icon(Icons.star, size: 100),
crossFadeState: _showFirst
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: Duration(milliseconds: 500),
),
ElevatedButton(
onPressed: () => setState(() => _showFirst = !_showFirst),
child: Text('Toggle'),
),
],
);
}
}
Explicit анимации
AnimationController
class ExplicitAnimationWidget extends StatefulWidget {
@override
State<ExplicitAnimationWidget> createState() => _ExplicitAnimationWidgetState();
}
class _ExplicitAnimationWidgetState extends State<ExplicitAnimationWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_animation = Tween<double>(begin: 0, end: 1).animate(_controller);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Opacity(
opacity: _animation.value,
child: FlutterLogo(size: 100),
);
},
);
}
}
Tween анимация
class TweenAnimationWidget extends StatefulWidget {
@override
State<TweenAnimationWidget> createState() => _TweenAnimationWidgetState();
}
class _TweenAnimationWidgetState extends State<TweenAnimationWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Color?> _colorAnimation;
late Animation<double> _sizeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true);
_colorAnimation = ColorTween(
begin: Colors.blue,
end: Colors.red,
).animate(_controller);
_sizeAnimation = Tween<double>(begin: 50, end: 150).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
decoration: BoxDecoration(
color: _colorAnimation.value,
shape: BoxShape.circle,
),
);
},
);
}
}
СтackedTween
class StackedAnimationWidget extends StatefulWidget {
@override
State<StackedAnimationWidget> createState() => _StackedAnimationWidgetState();
}
class _StackedAnimationWidgetState extends State<StackedAnimationWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _opacity;
late Animation<double> _size;
late Animation<double> _rotation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true);
_opacity = Tween<double>(begin: 0.5, end: 1).animate(_controller);
_size = Tween<double>(begin: 50, end: 150).animate(_controller);
_rotation = Tween<double>(begin: 0, end: 2 * pi).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _rotation.value,
child: Opacity(
opacity: _opacity.value,
child: Container(
width: _size.value,
height: _size.value,
color: Colors.blue,
),
),
);
},
);
}
}
Hero анимации
Базовая Hero
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('First')),
body: GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => SecondPage()),
),
child: Hero(
tag: 'image-hero',
child: Image.network('https://example.com/image.jpg'),
),
),
);
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second')),
body: Center(
child: Hero(
tag: 'image-hero',
child: Image.network('https://example.com/image.jpg'),
),
),
);
}
}
Hero с placeholder
Hero(
tag: 'avatar',
placeholderBuilder: (context, heroSize, child) {
return Container(
width: heroSize.width,
height: heroSize.height,
child: CircularProgressIndicator(),
);
},
child: CircleAvatar(
backgroundImage: NetworkImage(url),
),
)
CustomPainter анимации
Анимированный painter
class AnimatedCirclePainter extends CustomPainter {
final double progress;
AnimatedCirclePainter(this.progress);
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 * 0.8;
final paint = Paint()
..color = Colors.blue
..strokeWidth = 10
..style = PaintingStyle.stroke;
const startAngle = -pi / 2;
final sweepAngle = 2 * pi * progress;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
paint,
);
}
@override
bool shouldRepaint(AnimatedCirclePainter oldDelegate) {
return oldDelegate.progress != progress;
}
}
// Использование
class AnimatedCircleWidget extends StatefulWidget {
@override
State<AnimatedCircleWidget> createState() => _AnimatedCircleWidgetState();
}
class _AnimatedCircleWidgetState extends State<AnimatedCircleWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
painter: AnimatedCirclePainter(_controller.value),
size: Size(200, 200),
);
},
);
}
}
Lottie анимации
Установка
dependencies:
lottie: ^3.0.0
Использование
class LottieWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Lottie.asset('assets/animation.json');
}
}
// С контроллером
class ControlledLottieWidget extends StatefulWidget {
@override
State<ControlledLottieWidget> createState() => _ControlledLottieWidgetState();
}
class _ControlledLottieWidgetState extends State<ControlledLottieWidget> {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
// Загрузка анимации
_loadAnimation();
}
void _loadAnimation() {
final composition = Lottie.asset('assets/animation.json');
composition.then((value) {
_controller.duration = value.duration;
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Lottie.asset(
'assets/animation.json',
controller: _controller,
onLoaded: (composition) {
_controller.duration = composition.duration;
},
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.play_arrow),
onPressed: () => _controller.forward(),
),
IconButton(
icon: Icon(Icons.pause),
onPressed: () => _controller.stop(),
),
],
),
],
);
}
}
Rive анимации
Установка
dependencies:
flutter_rive: ^0.13.0
Использование
class RiveWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RiveAnimation.asset('assets/animation.riv');
}
}
// С контроллером
class ControlledRiveWidget extends StatefulWidget {
@override
State<ControlledRiveWidget> createState() => _ControlledRiveWidgetState();
}
class _ControlledRiveWidgetState extends State<ControlledRiveWidget> {
late SMITrigger _bump;
void _onRiveInit(Artboard artboard) {
final controller = StateMachineController.fromArtboard(artboard, 'bumpy');
artboard.addController(controller!);
_bump = controller.findInput<bool>('bump') as SMITrigger;
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _bump.fire(),
child: RiveAnimation.asset(
'assets/animation.riv',
onInit: _onRiveInit,
),
);
}
}
Curves
Популярные кривые
// Линейная
Curves.linear
// Ускорение
Curves.easeIn
Curves.easeInCubic
Curves.easeInQuart
// Замедление
Curves.easeOut
Curves.easeOutCubic
// Ускорение и замедление
Curves.easeInOut
Curves.easeInOutCubic
// Bounce эффект
Curves.bounceIn
Curves.bounceOut
Curves.elasticOut
Заключение
Анимации во Flutter — это мощный инструмент для создания потрясающего пользовательского интерфейса. Используйте implicit анимации для простых случаев и explicit для сложных сценариев.