1
I'm trying to implement the OpenContainer transition for go_router in Flutter. It seems like no matter what I do, the end result is choppy. :/
I'm using the native Transition widgets as well as the relatively new SnapshotWidget, but to no avail. Should I maybe use a custom painter? Would that improve performance?
Please take a look at the code below and let me know if you see any ways that I could improve performance.
Here's my attempt at an OpenContainer transition for go_router:
import 'package:flutter/material.dart';
import 'package:your_project_name_xxx/ui_models/container_transition_extra.dart';
class ContainerTransition extends StatefulWidget {
final ContainerTransitionExtra extra;
final Animation<double> animation;
final Widget? sourceWidget;
final Widget targetWidget;
const ContainerTransition({
super.key,
required this.extra,
required this.animation,
required this.sourceWidget,
required this.targetWidget,
});
@override
State<ContainerTransition> createState() => _ContainerTransitionState();
}
class _ContainerTransitionState extends State<ContainerTransition> {
static final _toTween = Tween<double>(begin: 0, end: 1);
static final _fromTween = Tween<double>(begin: 1, end: 0);
late SnapshotController _snapshotController;
late CurvedAnimation _curvedAnimation;
late CurvedAnimation _sourceAnimation;
late CurvedAnimation _targetAnimation;
late RelativeRect? _sourcePosition;
late Animation<double> _scrimOpacityAnimation;
late Animation<double> _sourceOpacityAnimation;
late Animation<double> _targetOpacityAnimation;
late Animation<BorderRadius?> _containerRadiusAnimation;
late Animation<RelativeRect> _containerPositionAnimation;
@override
void initState() {
super.initState();
_sourcePosition = widget.extra.tween.begin;
_snapshotController = SnapshotController(allowSnapshotting: true);
_curvedAnimation = CurvedAnimation(
parent: widget.animation,
curve: Curves.easeInOut,
);
_curvedAnimation.addStatusListener((status) {
if (status.isAnimating) {
_snapshotController.allowSnapshotting = true;
} else if (status.isCompleted || status.isDismissed) {
_snapshotController.allowSnapshotting = false;
}
});
_sourceAnimation = CurvedAnimation(
parent: _curvedAnimation,
curve: Interval(0, 1 / 3),
);
_targetAnimation = CurvedAnimation(
parent: _curvedAnimation,
curve: Interval(1 / 3, 1),
);
_scrimOpacityAnimation = _toTween.animate(_sourceAnimation);
_sourceOpacityAnimation = _fromTween.animate(_sourceAnimation);
_targetOpacityAnimation = _toTween.animate(_targetAnimation);
_containerRadiusAnimation = BorderRadiusTween(
begin: BorderRadius.circular(widget.extra.containerRadius),
end: BorderRadius.zero)
.animate(_curvedAnimation);
_containerPositionAnimation = _curvedAnimation.drive(widget.extra.tween);
}
@override
void dispose() {
_snapshotController.dispose();
_sourceAnimation.dispose();
_targetAnimation.dispose();
_curvedAnimation.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(
child: FadeTransition(
opacity: _scrimOpacityAnimation,
child: ColoredBox(color: widget.extra.scrimColor),
),
),
PositionedTransition(
rect: _containerPositionAnimation,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: _containerRadiusAnimation.value,
color: widget.extra.containerColor,
),
child: FadeTransition(
opacity: _targetOpacityAnimation,
child: widget.targetWidget,
),
),
),
if (_sourcePosition != null)
Positioned.fromRelativeRect(
rect: _sourcePosition!,
child: FadeTransition(
opacity: _sourceOpacityAnimation,
child: widget.sourceWidget,
),
),
],
);
}
}
Here's how I'm building the route:
Page<T> buildContainerRoute<T>(
BuildContext context,
GoRouterState state,
Ref ref,
Widget child,
) {
final containerExtra = state.extra;
if (containerExtra is ContainerTransitionExtra) {
final registryTag = containerExtra.sourceBuilderTag;
final registryBuilder =
registryTag != null ? SourceBuilderRegistry().get(registryTag) : null;
if (registryBuilder != null) {
if (registryBuilder.wasUsedAlready) {
SourceBuilderRegistry().unregister(registryTag!);
} else {
SourceBuilderRegistry().markAsUsed(registryTag!);
}
}
return CustomTransitionPage<T>(
key: state.pageKey,
child: child,
transitionDuration: Durations.medium2,
reverseTransitionDuration: Durations.medium1,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return ContainerTransition(
animation: animation,
extra: containerExtra,
sourceWidget:
Center(child: registryBuilder?.registryItem.call(context)),
targetWidget: ClipRect(
child: OverflowBox(
alignment: Alignment.topCenter,
maxWidth: containerExtra.cachedMaxWidth,
maxHeight: containerExtra.cachedMaxHeight,
child: child,
),
),
);
},
);
} else {
if (!kIsWeb && Platform.isAndroid) {
return MaterialPage(key: state.pageKey, child: child);
} else {
return CupertinoPage(key: state.pageKey, child: child);
}
}
}