Skip to content

Instantly share code, notes, and snippets.

@HansMuller
Last active September 27, 2023 16:55
Show Gist options
  • Save HansMuller/e79d5d26428f27a726d49ddc0e09bc00 to your computer and use it in GitHub Desktop.
Save HansMuller/e79d5d26428f27a726d49ddc0e09bc00 to your computer and use it in GitHub Desktop.
import 'package:flutter/material.dart';
/// Animated text widget that will animate changes in [temperature] to match
/// the effect on the new thermostats.
class AnimatedTemperatureText extends StatefulWidget {
final int temperature;
final Color textColor;
const AnimatedTemperatureText({
super.key,
required this.temperature,
required this.textColor,
});
@override
State<AnimatedTemperatureText> createState() =>
_AnimatedTemperatureTextState();
}
enum _SlideAnimationDirection {
up,
down,
}
class _AnimatedTemperatureTextState extends State<AnimatedTemperatureText> {
/// Which direction the digits should animate, set in [didUpdateWidget].
_SlideAnimationDirection slideAnimationDirection =
_SlideAnimationDirection.up;
@override
void didUpdateWidget(AnimatedTemperatureText oldWidget) {
// Determine if we increased or decreased in temperature, and if so,
// animate.
if (widget.temperature != oldWidget.temperature) {
slideAnimationDirection = widget.temperature > oldWidget.temperature
? _SlideAnimationDirection.up
: _SlideAnimationDirection.down;
}
super.didUpdateWidget(oldWidget);
}
/// Returns the number in the first temperature digit, from 0-9.
///
/// Example: Temperature is 53, this would return 5.
int firstTemperatureDigit(AnimatedTemperatureText widget) {
// The first digit is in the 10s place.
return widget.temperature ~/ 10;
}
/// Returns the number in the second temperature digit, from 0-9.
///
/// Example: Temperature is 53, this would return 3.
int secondTemperatureDigit(AnimatedTemperatureText widget) {
// The second digit is in the 1s place.
return widget.temperature % 10;
}
@override
Widget build(BuildContext context) {
return ClipRect(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedDigit(
value: firstTemperatureDigit(widget),
textColor: widget.textColor,
animationDirection: slideAnimationDirection,
textAlign: TextAlign.start,
),
AnimatedDigit(
value: secondTemperatureDigit(widget),
textColor: widget.textColor,
animationDirection: slideAnimationDirection,
textAlign: TextAlign.end,
),
],
),
);
}
}
// Occupies the same width as the widest single digit used by AnimatedDigit.
class _PlaceholderDigit extends StatelessWidget {
const _PlaceholderDigit();
@override
Widget build(BuildContext context) {
final TextStyle textStyle = Theme.of(context).textTheme.displayLarge!.copyWith(
fontWeight: FontWeight.w500,
);
final Iterable<Widget> placeholderDigits = <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map<Widget>(
(int n) {
return Opacity(
opacity: 0,
child: Text('$n', style: textStyle),
);
},
);
return Stack(children: placeholderDigits.toList());
}
}
class AnimatedDigit extends StatelessWidget {
/// Animation that goes from -1 to 0 on the y-axis, which looks like an
/// upward motion.
final Tween<Offset> upwardSlide = Tween<Offset>(
begin: const Offset(0, -1.0),
end: Offset.zero,
);
/// Animation that goes from 1 to 0 on the y-axis, which looks like a
/// downward motion.
final Tween<Offset> downwardSlide = Tween<Offset>(
begin: const Offset(0, 1.0),
end: Offset.zero,
);
final int value;
final Color textColor;
final _SlideAnimationDirection animationDirection;
final TextAlign textAlign;
AnimatedDigit({
super.key,
required this.value,
required this.textColor,
required this.animationDirection,
required this.textAlign,
});
@override
Widget build(BuildContext context) {
final TextStyle textStyle = Theme.of(context).textTheme.displayLarge!.copyWith(
color: textColor,
fontWeight: FontWeight.w500,
);
return AnimatedSwitcher(
duration: const Duration(milliseconds: 1000),
switchInCurve: const Interval(0, 1, curve: Curves.easeInOut),
switchOutCurve: const Interval(0, 1, curve: Curves.easeInOut),
transitionBuilder: (child, animation) {
late Tween<Offset> outAnimation;
late Tween<Offset> inAnimation;
switch (animationDirection) {
case _SlideAnimationDirection.up:
outAnimation = upwardSlide;
inAnimation = downwardSlide;
break;
case _SlideAnimationDirection.down:
outAnimation = downwardSlide;
inAnimation = upwardSlide;
break;
}
if (child.key == ValueKey(value)) {
return SlideTransition(
position: inAnimation.animate(animation),
child: child,
);
} else {
return SlideTransition(
position: outAnimation.animate(animation),
child: child,
);
}
},
child: Stack(
key: ValueKey(value),
children: <Widget>[
const _PlaceholderDigit(),
Text(value.toString(), style: textStyle),
],
),
);
}
}
class AnimatedTemperatureHome extends StatefulWidget {
const AnimatedTemperatureHome({ super.key });
@override
State<AnimatedTemperatureHome> createState() => _AnimatedTemperatureHomeState();
}
class _AnimatedTemperatureHomeState extends State<AnimatedTemperatureHome> {
int temperature = 31;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: AnimatedTemperatureText(
temperature: temperature,
textColor: Colors.black,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() { temperature += 1; });
},
tooltip: 'Increment Digit',
child: const Icon(Icons.add),
),
);
}
}
class AnimatedTemperatureApp extends StatelessWidget {
const AnimatedTemperatureApp({ super.key });
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'AnimatedTemperature',
theme: ThemeData(useMaterial3: true),
home: const Scaffold(
body: Center(
child: AnimatedTemperatureHome(),
),
),
);
}
}
void main() {
runApp(const AnimatedTemperatureApp());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment