Skip to content

Commit b0e8ccc

Browse files
committed
feat: animation graph API
1 parent 8354af9 commit b0e8ccc

File tree

4 files changed

+1010
-6
lines changed

4 files changed

+1010
-6
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import 'package:fleet/fleet.dart';
2+
import 'package:flutter/material.dart' hide Action;
3+
4+
import 'app.dart';
5+
6+
final _scale = AnimatedValue.double$(defaultValue: 1, name: 'scale');
7+
final _rotation = AnimatedValue.double$(name: 'rotation');
8+
final _opacity = AnimatedValue.double$(defaultValue: 1, name: 'opacity');
9+
final _color = AnimatedValue.color(defaultValue: Colors.pink, name: 'color');
10+
final _offset = AnimatedValue.offset(name: 'offset');
11+
12+
AnimationNode _buildAnimation() {
13+
return Sequence([
14+
// This node resets all animated values of the running animation to their
15+
// default values.
16+
// This is necessary, in case the animation has already been run, because
17+
// the animated values are not reset automatically.
18+
// Unless `AnimatedValue.to(from: ...)` is specified, the animation of
19+
// that value starts from the value that was last set, either by an
20+
// animation, explicitly, or by resetting to the default value.
21+
// This concept generally simplifies creating animations.
22+
Reset(),
23+
Group([
24+
_scale.to(2, 300.ms, curve: Curves.ease),
25+
_rotation.to(.25, 300.ms, curve: Curves.ease),
26+
_opacity.to(1, from: 0, 200.ms),
27+
]),
28+
Pause(500.ms),
29+
Group([
30+
_color.to(Colors.teal, 500.ms),
31+
_scale.to(1, 500.ms, curve: Curves.ease),
32+
_offset.to(
33+
const Offset(300, 0),
34+
500.ms,
35+
curve: Curves.ease,
36+
delay: 200.ms,
37+
),
38+
_opacity.to(0, 1.s, delay: 300.ms),
39+
]),
40+
// ignore: avoid_print
41+
Action(() => print('Animation completed')),
42+
])
43+
// The speed of all nodes in the animation graph can be adjusted by
44+
// this single call. This is useful for debugging purposes.
45+
.speed(1);
46+
}
47+
48+
void main() {
49+
runApp(const ExampleApp(page: Page()));
50+
}
51+
52+
class Page extends StatefulWidget {
53+
const Page({super.key});
54+
55+
@override
56+
State<Page> createState() => _PageState();
57+
}
58+
59+
class _PageState extends State<Page>
60+
with TickerProviderStateMixin, AnimationGraphMixin {
61+
void _animate() {
62+
// Cancel all running animations before starting a new one, incase the
63+
// previous animation has not completed. If two animations are run in
64+
// parallel that affect the same value, the result can be unpredictable.
65+
// If one animation starts a new value animation for the same animated value
66+
// while the previous animation is still running, the previous animation
67+
// is stopped and the new animation starts from the current value.
68+
cancelAllAnimations();
69+
animate(_buildAnimation());
70+
}
71+
72+
@override
73+
Widget build(BuildContext context) {
74+
// By using an AnimationGraphScope, child widgets can access the
75+
// AnimationGraphController instance and it does not need to be passed
76+
// down the widget tree.
77+
return AnimationGraphScope(
78+
controller: animationGraphController,
79+
child: Scaffold(
80+
body: SizedBox.expand(
81+
child: Stack(
82+
alignment: Alignment.center,
83+
children: [
84+
TranslateTransition(
85+
offset: _offset.of(context),
86+
child: RotationTransition(
87+
turns: _rotation.of(context),
88+
child: ScaleTransition(
89+
scale: _scale.of(context),
90+
child: FadeTransition(
91+
opacity: _opacity.of(context),
92+
child: _ColoredSquare(color: _color),
93+
),
94+
),
95+
),
96+
),
97+
ElevatedButton(
98+
onPressed: _animate,
99+
child: const Text('Animate'),
100+
),
101+
],
102+
),
103+
),
104+
),
105+
);
106+
}
107+
}
108+
109+
// This widget is reusable since it is decoupled from the concrete
110+
// AnimationGraphController and AnimationKey used in the AnimationGraph.
111+
class _ColoredSquare extends StatelessWidget {
112+
const _ColoredSquare({required this.color});
113+
114+
final AnimatedValue<Color> color;
115+
116+
@override
117+
Widget build(BuildContext context) {
118+
return SizedBox.square(
119+
dimension: 200,
120+
child: ValueListenableBuilder(
121+
valueListenable: color.of(context),
122+
builder: (context, color, _) {
123+
return ColoredBox(color: color);
124+
},
125+
),
126+
);
127+
}
128+
}
129+
130+
class TranslateTransition extends AnimatedWidget {
131+
const TranslateTransition({
132+
super.key,
133+
required Animation<Offset> offset,
134+
required this.child,
135+
}) : super(listenable: offset);
136+
137+
Animation<Offset> get offset => listenable as Animation<Offset>;
138+
139+
final Widget? child;
140+
141+
@override
142+
Widget build(BuildContext context) {
143+
return Transform.translate(
144+
offset: offset.value,
145+
child: child,
146+
);
147+
}
148+
}

packages/fleet/lib/fleet.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@ export 'src/animation/animate.dart'
99
export 'src/animation/animation.dart'
1010
show AnimationFromCurveExtension, AnimationSpec;
1111
export 'src/animation/duration.dart' show DurationFromIntExtension;
12+
export 'src/animation/graph.dart'
13+
show
14+
Action,
15+
AnimatedValue,
16+
AnimationGraphController,
17+
AnimationGraphMixin,
18+
AnimationGraphScope,
19+
AnimationNode,
20+
AnimationNodeExtension,
21+
GraphAnimation,
22+
Group,
23+
Pause,
24+
Reset,
25+
Sequence,
26+
ValueAnimation;
1227
export 'src/animation/parameter.dart'
1328
show
1429
AnimatableAlignmentGeometry,

packages/fleet/lib/src/animation/animation.dart

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,14 @@ abstract class AnimationImpl<T> with Diagnosticable {
429429

430430
var elapsedForRepeat = elapsedForAllRepeats - _lastRepeatEnd;
431431

432+
var isDone = false;
433+
434+
void onFinishRepeat() {
435+
if (!_spec._repeatForever && ++_repeat >= _spec._repeatCount) {
436+
isDone = true;
437+
}
438+
}
439+
432440
if (_forward) {
433441
final endDelta = isAtEnd(elapsedForRepeat);
434442
if (endDelta != null) {
@@ -441,7 +449,7 @@ abstract class AnimationImpl<T> with Diagnosticable {
441449
_forward = false;
442450
}
443451

444-
_onFinishRepeat();
452+
onFinishRepeat();
445453
}
446454
} else {
447455
elapsedForRepeat = _lastRepeatDuration - elapsedForRepeat;
@@ -452,11 +460,11 @@ abstract class AnimationImpl<T> with Diagnosticable {
452460

453461
_forward = true;
454462

455-
_onFinishRepeat();
463+
onFinishRepeat();
456464
}
457465
}
458466

459-
if (_isStopped) {
467+
if (isDone) {
460468
// On the last tick we need to be exactly at the end of the animation.
461469
// If the animation is repeated and reversing it is possible that the
462470
// last tick is at the beginning of the animation.
@@ -466,10 +474,8 @@ abstract class AnimationImpl<T> with Diagnosticable {
466474
}
467475

468476
onChange?.call();
469-
}
470477

471-
void _onFinishRepeat() {
472-
if (!_spec._repeatForever && ++_repeat >= _spec._repeatCount) {
478+
if (isDone) {
473479
stop();
474480
}
475481
}

0 commit comments

Comments
 (0)