From 937680eb0ee2db0aab9b7f9b2945dadbe956a8c4 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 26 Jul 2026 23:28:19 +0300 Subject: [PATCH] perf(ui): isolate SwitchingBody/CrossFade layers with RepaintBoundary Wrap keep-alive morph children in RepaintBoundary and TickerMode so inactive panes do not drive child tickers or share paint layers. Closes #358 Co-authored-by: Cursor --- lib/core/motion/querya_cross_fade_stack.dart | 5 ++- lib/core/motion/querya_switching_body.dart | 7 +++- .../motion/querya_switching_body_test.dart | 35 +++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/lib/core/motion/querya_cross_fade_stack.dart b/lib/core/motion/querya_cross_fade_stack.dart index e3e01724..4527cf85 100644 --- a/lib/core/motion/querya_cross_fade_stack.dart +++ b/lib/core/motion/querya_cross_fade_stack.dart @@ -35,7 +35,10 @@ class QueryaCrossFadeStack extends StatelessWidget { opacity: index == i ? 1 : 0, duration: duration, curve: curve, - child: children[i], + child: TickerMode( + enabled: index == i, + child: RepaintBoundary(child: children[i]), + ), ), ), ), diff --git a/lib/core/motion/querya_switching_body.dart b/lib/core/motion/querya_switching_body.dart index 025f1490..77a54f91 100644 --- a/lib/core/motion/querya_switching_body.dart +++ b/lib/core/motion/querya_switching_body.dart @@ -74,11 +74,16 @@ class _SwitchingLayer extends StatelessWidget { @override Widget build(BuildContext context) { final curve = active ? inCurve : outCurve; + // Isolate paint; pause child tickers when inactive (opacity anim still runs). + final content = TickerMode( + enabled: active, + child: RepaintBoundary(child: child), + ); Widget layer = AnimatedOpacity( opacity: active ? 1 : 0, duration: duration, curve: curve, - child: child, + child: content, ); if (slide != Offset.zero) { diff --git a/test/core/motion/querya_switching_body_test.dart b/test/core/motion/querya_switching_body_test.dart index 07e19353..ecab84c6 100644 --- a/test/core/motion/querya_switching_body_test.dart +++ b/test/core/motion/querya_switching_body_test.dart @@ -251,6 +251,41 @@ void main() { ); }); + testWidgets('inactive layer disables TickerMode and uses RepaintBoundary', + (tester) async { + await tester.pumpWidget( + wrap( + const QueryaSwitchingBody( + index: 0, + children: [ + Text('active'), + Text('inactive'), + ], + ), + ), + ); + + final tickers = tester + .widgetList( + find.descendant( + of: find.byType(QueryaSwitchingBody), + matching: find.byType(TickerMode), + ), + ) + .toList(); + expect(tickers.length, 2); + expect(tickers.first.enabled, isTrue); + expect(tickers.last.enabled, isFalse); + + expect( + find.descendant( + of: find.byType(QueryaSwitchingBody), + matching: find.byType(RepaintBoundary), + ), + findsNWidgets(2), + ); + }); + testWidgets('ExcludeSemantics excludes inactive child', (tester) async { await tester.pumpWidget( wrap(