Files
Client/lib/widget/route_transition_gate.dart
T

83 lines
2.7 KiB
Dart

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<RouteTransitionGate> createState() => _RouteTransitionGateState();
}
class _RouteTransitionGateState extends State<RouteTransitionGate> {
Animation<double>? _animation;
Animation<double>? _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<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() {
_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);
}
}