Skip to content

Instantly share code, notes, and snippets.

@fredgrott
Created September 16, 2025 17:20
Show Gist options
  • Save fredgrott/2a11f0d68845fd4cce1ed94eab323c0d to your computer and use it in GitHub Desktop.
Save fredgrott/2a11f0d68845fd4cce1ed94eab323c0d to your computer and use it in GitHub Desktop.
animate icon widget until we get better variable font api for Material 3 Expressive, article at
// Copyright 2025 Fredrick Allan Grott. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// ignore_for_file: unnecessary_new, avoid_redundant_argument_values
import 'dart:math' as math;
import 'package:flutter/material.dart';
class AnimateIcons extends StatefulWidget {
const AnimateIcons({
/// The IconData that will be visible before animation Starts
required this.startIcon,
/// The IconData that will be visible after animation ends
required this.endIcon,
/// The callback on startIcon Press
/// It should return a bool
/// If true is returned it'll animate to the end icon
/// if false is returned it'll not animate to the end icons
required this.onStartIconPress,
/// The callback on endIcon Press
/// /// It should return a bool
/// If true is returned it'll animate to the end icon
/// if false is returned it'll not animate to the end icons
required this.onEndIconPress,
/// The size of the icon that are to be shown.
this.size,
/// AnimateIcons controller
required this.controller,
/// The color to be used for the [startIcon]
this.startIconColor,
// The color to be used for the [endIcon]
this.endIconColor,
/// The duration for which the animation runs
this.duration,
/// If the animation runs in the clockwise or anticlockwise direction
this.clockwise,
/// This is the tooltip that will be used for the [startIcon]
this.startTooltip,
/// This is the tooltip that will be used for the [endIcon]
this.endTooltip,
});
final IconData startIcon;
final IconData endIcon;
final bool Function() onStartIconPress;
final bool Function() onEndIconPress;
final Duration? duration;
final bool? clockwise;
final double? size;
final Color? startIconColor;
final Color? endIconColor;
final AnimateIconController controller;
final String? startTooltip;
final String? endTooltip;
@override
_AnimateIconsState createState() => _AnimateIconsState();
}
class _AnimateIconsState extends State<AnimateIcons> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
_controller = AnimationController(
vsync: this,
duration: widget.duration ?? const Duration(seconds: 1),
lowerBound: 0.0,
upperBound: 1.0,
);
_controller.addListener(() {
if (mounted) {
setState(() {});
}
});
initControllerFunctions();
super.initState();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void initControllerFunctions() {
widget.controller.animateToEnd = () {
if (mounted) {
_controller.forward();
return true;
} else {
return false;
}
};
widget.controller.animateToStart = () {
if (mounted) {
_controller.reverse();
return true;
} else {
return false;
}
};
widget.controller.isStart = () => _controller.value == 0.0;
widget.controller.isEnd = () => _controller.value == 1.0;
}
void _onStartIconPress() {
if (widget.onStartIconPress() && mounted) _controller.forward();
}
void _onEndIconPress() {
if (widget.onEndIconPress() && mounted) _controller.reverse();
}
@override
Widget build(BuildContext context) {
final double x = _controller.value;
final double y = 1.0 - _controller.value;
final double angleX = math.pi / 180 * (180 * x);
final double angleY = math.pi / 180 * (180 * y);
Widget first() {
final icon = Icon(widget.startIcon, size: widget.size);
return Transform.rotate(
angle: widget.clockwise ?? false ? angleX : -angleX,
child: Opacity(
opacity: y,
child: IconButton(
iconSize: widget.size ?? 24.0,
color: widget.startIconColor ?? Theme.of(context).primaryColor,
disabledColor: Colors.grey.shade500,
icon: widget.startTooltip == null ? icon : Tooltip(message: widget.startTooltip, child: icon),
onPressed: _onStartIconPress,
),
),
);
}
Widget second() {
final icon = Icon(widget.endIcon);
return Transform.rotate(
angle: widget.clockwise ?? false ? -angleY : angleY,
child: Opacity(
opacity: x,
child: IconButton(
iconSize: widget.size ?? 24.0,
color: widget.endIconColor ?? Theme.of(context).primaryColor,
disabledColor: Colors.grey.shade500,
icon: widget.endTooltip == null ? icon : Tooltip(message: widget.endTooltip, child: icon),
onPressed: _onEndIconPress,
),
),
);
}
return Stack(
alignment: Alignment.center,
children: [if (x == 1 && y == 0) second() else first(), if (x == 0 && y == 1) first() else second()],
);
}
}
class AnimateIconController {
late bool Function() animateToStart;
late bool Function() animateToEnd;
late bool Function() isStart;
late bool Function() isEnd;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment