import 'package:flutter/material.dart'; import 'app_progress_indicator.dart'; /// Builds [builder]'s subtree only while the enclosing route is at rest — i.e. /// neither its own enter/exit animation nor its secondary (route-pushed-on-top) /// transition is running. /// /// Some widgets (notably `SfPdfViewer`) call `localToGlobal` during layout and /// crash with `RenderBox was not laid out` when an ancestor page-transition /// `RenderTransform` is mid-first-layout. Those transforms are inserted freshly /// whenever a transition *starts* — not only on the initial push, but also on /// pop and when another page is pushed on top. The gate therefore swaps the /// subtree for [placeholder] for the duration of any transition; the status /// listener fires before that frame's layout, so the fragile subtree is gone /// before the new transform lays out. class RouteTransitionGate extends StatefulWidget { const RouteTransitionGate({super.key, required this.builder, this.placeholder}); final WidgetBuilder builder; /// Shown while the route is transitioning. Defaults to a centered large /// progress indicator. final Widget? placeholder; @override State createState() => _RouteTransitionGateState(); } class _RouteTransitionGateState extends State { Animation? _animation; Animation? _secondaryAnimation; @override void didChangeDependencies() { super.didChangeDependencies(); final route = ModalRoute.of(context); _swapListener(route?.animation, _animation, (a) => _animation = a); _swapListener( route?.secondaryAnimation, _secondaryAnimation, (a) => _secondaryAnimation = a, ); } void _swapListener( Animation? next, Animation? current, void Function(Animation?) assign, ) { if (identical(next, current)) return; current?.removeStatusListener(_onAnimationStatus); assign(next?..addStatusListener(_onAnimationStatus)); } @override void dispose() { _animation?.removeStatusListener(_onAnimationStatus); _secondaryAnimation?.removeStatusListener(_onAnimationStatus); super.dispose(); } bool get _transitioning => _isAnimating(_animation?.status) || _isAnimating(_secondaryAnimation?.status); static bool _isAnimating(AnimationStatus? status) => status == AnimationStatus.forward || status == AnimationStatus.reverse; void _onAnimationStatus(AnimationStatus status) { if (mounted) setState(() {}); } @override Widget build(BuildContext context) { if (_transitioning) { return widget.placeholder ?? const Center(child: AppProgressIndicator.large()); } return widget.builder(context); } }