perf(player): suspend covered pages and streamline transitions

This commit is contained in:
zarzet committed 2026-09-30 23:40:04 +07:00
1 parent 1e246138c8
commit b11e925ee4
4 files changed
+138 -36

No files matched your search

+36 -31
View File
@@ -62,6 +62,16 @@ class _MainShellState extends ConsumerState<MainShell>
final GlobalKey _pageViewKey = GlobalKey();
late final PageController _pageController;
late final AnimationController _tabJumpTransitionController;
// Transitions update their layers directly; no shell subtree rebuilds per
// animation frame.
late final CurvedAnimation _tabJumpOpacity = CurvedAnimation(
parent: _tabJumpTransitionController,
curve: Curves.easeOutCubic,
);
late final Animation<double> _tabJumpScale = Tween<double>(
begin: 0.985,
end: 1,
).animate(_tabJumpOpacity);
bool _hasCheckedUpdate = false;
bool _hasCheckedAppAnnouncement = false;
bool _initialSafRepairComplete = false;
@@ -534,6 +544,7 @@ class _MainShellState extends ConsumerState<MainShell>
ShellNavigationService.unregisterTabSelectionHandler(this);
_shareSubscription?.cancel();
_pageController.dispose();
_tabJumpOpacity.dispose();
_tabJumpTransitionController.dispose();
_mornyeChrome.dispose();
super.dispose();
@@ -946,41 +957,35 @@ class _MainShellState extends ConsumerState<MainShell>
final pageView = KeyedSubtree(
key: _pageViewKey,
child: AnimatedBuilder(
animation: _tabJumpTransitionController,
child: PageView.builder(
controller: _pageController,
itemCount: tabs.length,
onPageChanged: _onPageChanged,
physics: const NeverScrollableScrollPhysics(),
// TickerMode mutes animations and lets visibility-aware widgets
// (e.g. MotionHeaderBanner) pause when their tab is hidden —
// kept-alive pages otherwise keep running offscreen.
itemBuilder: (context, index) => _KeepAliveTabPage(
key: ValueKey('page-$index'),
child: TickerMode(
enabled: index == _currentIndex,
child: NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (canMinimizeChrome && index == _currentIndex) {
return _mornyeChrome.handleScroll(notification);
}
return false;
},
child: tabs[index],
child: FadeTransition(
opacity: _tabJumpOpacity,
child: ScaleTransition(
scale: _tabJumpScale,
child: PageView.builder(
controller: _pageController,
itemCount: tabs.length,
onPageChanged: _onPageChanged,
physics: const NeverScrollableScrollPhysics(),
// TickerMode mutes animations and lets visibility-aware widgets
// (e.g. MotionHeaderBanner) pause when their tab is hidden —
// kept-alive pages otherwise keep running offscreen.
itemBuilder: (context, index) => _KeepAliveTabPage(
key: ValueKey('page-$index'),
child: TickerMode(
enabled: index == _currentIndex,
child: NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (canMinimizeChrome && index == _currentIndex) {
return _mornyeChrome.handleScroll(notification);
}
return false;
},
child: tabs[index],
),
),
),
),
),
builder: (context, child) {
final t = Curves.easeOutCubic.transform(
_tabJumpTransitionController.value,
);
return Opacity(
opacity: t,
child: Transform.scale(scale: 0.985 + (0.015 * t), child: child),
);
},
),
);
+27 -4
View File
@@ -101,10 +101,30 @@ class NowPlayingRoute extends PageRoute<void> {
bool _interactiveTransition = false;
int _dragGeneration = 0;
// Keep the previous page painted where a drag exposes it. The player itself
// fills the screen, including the status bar and bottom safe area.
// The player paints an opaque surface over the whole screen, including the
// status bar and bottom safe area. Once settled, the framework therefore
// keeps the pages below offstage: their glass, marquees and tickers stop
// compositing. Every drag or dismissal moves the animation off `completed`,
// which makes this route translucent again before the page is exposed.
@override
bool get opaque => false;
bool get opaque => true;
/// Overlay size when the pages below were last laid out, recorded while this
/// route hides them. A rotation while hidden leaves their geometry stale.
Size? _coveredOverlaySize;
Size? get _overlaySize {
final box = navigator?.overlay?.context.findRenderObject();
return box is RenderBox && box.hasSize ? box.size : null;
}
@override
void install() {
super.install();
animation!.addStatusListener((status) {
_coveredOverlaySize = status.isCompleted ? _overlaySize : null;
});
}
@override
Color? get barrierColor => null;
@@ -175,7 +195,10 @@ class NowPlayingRoute extends PageRoute<void> {
context.isMornye &&
!MediaQuery.disableAnimationsOf(context) &&
(controller?.value ?? 0) > 0) {
final target = miniPlayerGeometry?.call();
final coveredSize = _coveredOverlaySize;
final target = coveredSize == null || coveredSize == _overlaySize
? miniPlayerGeometry?.call()
: null;
if (target != null && !target.surface.isEmpty) {
final size = MediaQuery.sizeOf(context);
_dismissStartValue = controller!.value;
+2 -1
View File
@@ -155,7 +155,8 @@ class _ShimmerLoadingState extends State<ShimmerLoading>
child: child,
);
},
child: widget.child,
// The mask moves every frame; the skeleton beneath it does not.
child: RepaintBoundary(child: widget.child),
);
}
}