enhanced route transition gate because of a syncfusion viewer bug

This commit is contained in:
2026-08-08 22:13:19 +02:00
parent b9cb1df473
commit 39c16bd4ea
3 changed files with 134 additions and 24 deletions
+45 -22
View File
@@ -2,18 +2,25 @@ import 'package:flutter/material.dart';
import 'app_progress_indicator.dart';
/// Delays building [builder] until the enclosing route's enter animation has
/// finished. Some widgets (notably `SfPdfViewer`) call `localToGlobal` during
/// their first layout and assert with `RenderBox was not laid out` when an
/// ancestor page-transition `RenderTransform` still has no size mid-push.
/// Gating the mount behind the settled animation avoids that race.
/// 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 still animating in. Defaults to a centered
/// large progress indicator.
/// Shown while the route is transitioning. Defaults to a centered large
/// progress indicator.
final Widget? placeholder;
@override
@@ -21,36 +28,52 @@ class RouteTransitionGate extends StatefulWidget {
}
class _RouteTransitionGateState extends State<RouteTransitionGate> {
bool _ready = false;
Animation<double>? _routeAnimation;
Animation<double>? _animation;
Animation<double>? _secondaryAnimation;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_ready || _routeAnimation != null) return;
final animation = ModalRoute.of(context)?.animation;
if (animation == null || animation.isCompleted) {
_ready = true;
return;
}
_routeAnimation = animation..addStatusListener(_onAnimationStatus);
final route = ModalRoute.of(context);
_swapListener(route?.animation, _animation, (a) => _animation = a);
_swapListener(
route?.secondaryAnimation,
_secondaryAnimation,
(a) => _secondaryAnimation = a,
);
}
void _onAnimationStatus(AnimationStatus status) {
if (status == AnimationStatus.completed && mounted) {
setState(() => _ready = true);
}
void _swapListener(
Animation<double>? next,
Animation<double>? current,
void Function(Animation<double>?) assign,
) {
if (identical(next, current)) return;
current?.removeStatusListener(_onAnimationStatus);
assign(next?..addStatusListener(_onAnimationStatus));
}
@override
void dispose() {
_routeAnimation?.removeStatusListener(_onAnimationStatus);
_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 (!_ready) {
if (_transitioning) {
return widget.placeholder ??
const Center(child: AppProgressIndicator.large());
}