diff --git a/lib/screens/now_playing_screen.dart b/lib/screens/now_playing_screen.dart index ba27c86e..2cc2bb4b 100644 --- a/lib/screens/now_playing_screen.dart +++ b/lib/screens/now_playing_screen.dart @@ -1240,11 +1240,20 @@ class _NowPlayingScreenState extends ConsumerState { Stack( fit: StackFit.expand, children: [ - for (final child in previousChildren) + // Preserve each panel's parent and key when + // it becomes outgoing, including lyric scroll. + for (final child in [ + ...previousChildren, + ?currentChild, + ]) IgnorePointer( - child: ExcludeSemantics(child: child), + key: child.key, + ignoring: child != currentChild, + child: ExcludeSemantics( + excluding: child != currentChild, + child: child, + ), ), - ?currentChild, ], ), // Keep the outgoing panel until it fades out; diff --git a/lib/widgets/mornye_landscape_player.dart b/lib/widgets/mornye_landscape_player.dart index fb6f5991..ee31d562 100644 --- a/lib/widgets/mornye_landscape_player.dart +++ b/lib/widgets/mornye_landscape_player.dart @@ -134,11 +134,15 @@ class _MornyeLandscapePlayerState extends State { layoutBuilder: (current, previous) => Stack( fit: StackFit.expand, children: [ - for (final child in previous) + for (final child in [...previous, ?current]) IgnorePointer( - child: ExcludeSemantics(child: child), + key: child.key, + ignoring: child != current, + child: ExcludeSemantics( + excluding: child != current, + child: child, + ), ), - ?current, ], ), child: widget.page == 0 diff --git a/test/now_playing_lyrics_transition_test.dart b/test/now_playing_lyrics_transition_test.dart index ce8bffa0..511af6b5 100644 --- a/test/now_playing_lyrics_transition_test.dart +++ b/test/now_playing_lyrics_transition_test.dart @@ -2142,6 +2142,52 @@ void main() { ); } + for (final landscape in [false, true]) { + testWidgets( + 'outgoing lyrics keep their scroll state while queue fades in (landscape: $landscape)', + (tester) async { + metadataOverrides['lyrics'] = List.generate( + 16, + (index) => + '[00:${(index * 2).toString().padLeft(2, '0')}.00]Line $index', + ).join('\n'); + await pumpNowPlaying( + tester, + theme: MornyeTheme.build(Brightness.dark), + size: landscape ? const Size(852, 393) : const Size(393, 852), + playback: PlaybackState( + processingState: AudioProcessingState.ready, + updatePosition: const Duration(seconds: 20), + ), + ); + mediaItems.add(item('first')); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(CupertinoIcons.quote_bubble)); + await tester.pumpAndSettle(); + final scrollable = find + .ancestor( + of: find.text('Line 10'), + matching: find.byType(Scrollable), + ) + .first; + final state = tester.state(scrollable); + final offset = state.position.pixels; + expect(offset, greaterThan(0)); + await tester.tap(find.byIcon(CupertinoIcons.list_bullet)); + await tester.pump(); + for (var frame = 0; frame < 3; frame++) { + expect(state.mounted, isTrue); + expect(tester.state(scrollable), same(state)); + expect(state.position.pixels, offset); + await tester.pump(const Duration(milliseconds: 50)); + } + await tester.pumpAndSettle(); + expect(state.mounted, isFalse); + expect(tester.takeException(), isNull); + }, + ); + } + testWidgets('header reverses in place and stays beside cover across panels', ( tester, ) async {