Skip to content

Commit

Permalink
feat: support always animating state changes with Animated (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
blaugold committed May 20, 2023
1 parent 46324ba commit 4d2533e
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 8 deletions.
3 changes: 3 additions & 0 deletions packages/fleet/example/README.md
@@ -1,6 +1,7 @@
This Flutter project contains a number of small self contained demos to
demonstrate the Fleet framework:

- [hike_graph.dart] shows how to animate a graph of data points.
- [interactive_box_fleet.dart] and [interactive_box_flutter.dart] implement the
same interactive animation, one using the Fleet framework and the other using
the standard components from the Flutter framework.
Expand All @@ -10,6 +11,8 @@ demonstrate the Fleet framework:
with `AnimatingStateMixin.setStateAsync`.
- [stack.dart] shows how to stagger animations through `AnimatedSpec.delay`.

[hike_graph.dart]:
https://github.com/blaugold/fleet/blob/main/packages/fleet/example/lib/hike_graph.dart
[interactive_box_fleet.dart]:
https://github.com/blaugold/fleet/blob/main/packages/fleet/example/lib/interactive_box_fleet.dart
[interactive_box_flutter.dart]:
Expand Down
236 changes: 236 additions & 0 deletions packages/fleet/example/lib/hike_graph.dart
@@ -0,0 +1,236 @@
import 'package:fleet/fleet.dart';
import 'package:flutter/material.dart';

void main() {
runApp(const App());
}

class App extends StatelessWidget {
const App({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Page(),
);
}
}

class Page extends StatefulWidget {
const Page({super.key});

@override
State<Page> createState() => _PageState();
}

class _PageState extends State<Page> {
var _observation = Observation.heartRate;

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SegmentedButton<Observation>(
segments: const [
ButtonSegment(
value: Observation.heartRate,
label: Text('Heart Rate'),
),
ButtonSegment(
value: Observation.pace,
label: Text('Pace'),
),
],
selected: {_observation},
onSelectionChanged: (selection) {
setState(() {
_observation = selection.single;
});
},
),
const SizedBox(height: 10),
SizedBox(
width: 400,
height: 200,
child: HikeGraph(
hike: hike,
observation: _observation,
),
)
],
),
),
);
}
}

AnimationSpec ripple(int index) {
return Curves.bounceOut.animation().delay(Duration(milliseconds: 30 * index));
}

extension on Widget {
Widget animation(AnimationSpec animation) {
return Animated(
animation: animation,
child: this,
);
}
}

class HikeGraph extends StatelessWidget {
const HikeGraph({
super.key,
required this.hike,
required this.observation,
});

final Hike hike;
final Observation observation;

Color get color {
switch (observation) {
case Observation.heartRate:
return Colors.red;
case Observation.pace:
return Colors.blue;
}
}

@override
Widget build(BuildContext context) {
final data = hike.observations[observation]!;
final overallRange = data.overallRange;

return LayoutBuilder(
builder: (context, constraints) {
final spacing = constraints.maxWidth / 120;
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
for (var i = 0; i < data.length; i++) ...[
GraphCapsule(
color: color,
height: constraints.maxHeight,
range: data[i],
overallRange: overallRange,
).animation(ripple(i)),
if (i < data.length - 1) SizedBox(width: spacing)
],
],
);
},
);
}
}

class GraphCapsule extends StatelessWidget {
const GraphCapsule({
super.key,
required this.color,
required this.height,
required this.range,
required this.overallRange,
});

final Color color;
final double height;
final Range range;
final Range overallRange;

@override
Widget build(BuildContext context) {
final relativeRange = range.relativeTo(overallRange);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AContainer(
height: height * relativeRange.magnitude,
width: 24,
decoration: ShapeDecoration(
shape: const StadiumBorder(),
color: color,
),
),
ASizedBox(
height: height * relativeRange.min,
)
],
);
}
}

class Range {
Range({required this.min, required this.max});

final double min;
final double max;

double get magnitude => max - min;

Range combine(Range other) {
return Range(
min: min < other.min ? min : other.min,
max: max > other.max ? max : other.max,
);
}

Range relativeTo(Range other) {
return Range(
min: (min - other.min) / other.magnitude,
max: (max - other.min) / other.magnitude,
);
}
}

extension on Iterable<Range> {
Range get overallRange =>
reduce((overallRange, range) => overallRange.combine(range));
}

enum Observation {
heartRate,
pace,
}

class Hike {
Hike({required this.observations});

final Map<Observation, List<Range>> observations;
}

final hike = Hike(
observations: {
Observation.heartRate: [
Range(min: 10, max: 20),
Range(min: 15, max: 25),
Range(min: 30, max: 40),
Range(min: 35, max: 50),
Range(min: 40, max: 50),
Range(min: 50, max: 60),
Range(min: 40, max: 70),
Range(min: 20, max: 50),
Range(min: 25, max: 45),
Range(min: 30, max: 40),
Range(min: 25, max: 35),
Range(min: 30, max: 45),
],
Observation.pace: [
Range(min: 30, max: 45),
Range(min: 25, max: 35),
Range(min: 30, max: 40),
Range(min: 25, max: 45),
Range(min: 40, max: 55),
Range(min: 35, max: 65),
Range(min: 50, max: 60),
Range(min: 40, max: 50),
Range(min: 35, max: 50),
Range(min: 30, max: 40),
Range(min: 15, max: 25),
Range(min: 10, max: 20),
]
},
);
6 changes: 3 additions & 3 deletions packages/fleet/example/macos/Runner.xcodeproj/project.pbxproj
Expand Up @@ -345,7 +345,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.13;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
Expand Down Expand Up @@ -424,7 +424,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.13;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
Expand Down Expand Up @@ -471,7 +471,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.13;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
Expand Down
4 changes: 2 additions & 2 deletions packages/fleet/lib/src/animatable_flutter_widgets.dart
Expand Up @@ -514,8 +514,8 @@ class ASizedBox extends StatefulWidget {
/// Creates an animatable version of [SizedBox].
const ASizedBox({
super.key,
required this.height,
required this.width,
this.height,
this.width,
this.child,
});

Expand Down
12 changes: 9 additions & 3 deletions packages/fleet/lib/src/animate.dart
Expand Up @@ -160,7 +160,8 @@ mixin AnimatingStateMixin<T extends StatefulWidget> on State<T> {
/// {@endtemplate}
///
/// State changes are only animated if they happen at the same time that [value]
/// changes.
/// changes. If no [value] or the [alwaysAnimateValue] is provided, the
/// [animation] is applied to all state changes.
///
/// [Animated] can be nested, with the closest enclosing [Animated] widget
/// taking precedence.
Expand Down Expand Up @@ -210,10 +211,14 @@ class Animated extends StatefulWidget {
const Animated({
super.key,
this.animation = const AnimationSpec(),
required this.value,
this.value = alwaysAnimateValue,
required this.child,
});

/// A [value] that can be provided to animate all state changes in
/// descendants.
static const alwaysAnimateValue = Object();

/// The [AnimationSpec] to use for animating state changes in descendants.
final AnimationSpec animation;

Expand All @@ -236,7 +241,8 @@ class _AnimatedState extends State<Animated> {
@override
void didUpdateWidget(covariant Animated oldWidget) {
super.didUpdateWidget(oldWidget);
_animationIsActive = widget.value != oldWidget.value;
_animationIsActive = identical(widget.value, Animated.alwaysAnimateValue) ||
widget.value != oldWidget.value;
}

@override
Expand Down

0 comments on commit 4d2533e

Please sign in to comment.