Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support always animating state changes with Animated #40

Merged
merged 1 commit into from
May 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/fleet/example/README.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading