Fix: RenderFlex-Overflow in NowPlayingScreen während der Öffnen-Geste
Bug (Code-Review, verifiziert): _Angaben (Titel/Künstler/Fortschritt/ Controls) hat eine intrinsische Mindesthöhe, die während der Öffnen-Geste (progress zwischen ~0.3 und ~1.0, siehe main.dart Positioned.fromRect mit Rect.lerp Mini-/Vollbild-Player) das noch kleinere, interpolierte Rechteck übersteigt — Flutter meldete dann "RenderFlex overflowed" bei praktisch jeder normalen Öffnen-Geste. Fix: der Inhalt-Slot (in beiden responsiven Zweigen — Row nebeneinander und Column untereinander) steckt jetzt in ClipRect(child: ConstrainedBox(maxHeight: raum.maxHeight - Lücke, child: OverflowBox(maxHeight: infinity, fit: deferToChild, child: _Angaben))). ConstrainedBox deckelt nur, was OverflowBox nach AUSSEN als eigene Größe meldet (verhindert, dass der umgebende Row/Column selbst überläuft); OverflowBox gibt _Angaben davon unabhängig weiterhin unbegrenzte Höhe (verhindert, dass _Angaben intern überläuft) — überschüssiger Inhalt wird vom ClipRect während der kurzen, ohnehin noch recht durchsichtigen Übergangsphase einfach nicht gemalt statt eine Overflow-Warnung auszulösen. Bei ausreichend Platz (Endzustand progress==1) ist raum.maxHeight groß genug, dass nichts geklemmt wird — die Optik bleibt dort unverändert (now_playing_heart_test.dart bestätigt das weiterhin). Test zuerst (RED: "RenderFlex overflowed by 13 pixels" reproduziert, mit NowPlayingScreen in einem echten, interpolierten Zwischenrechteck bei progress≈0.4 auf Basis der HomeShell-Rect-Formel): test/player/ now_playing_screen_overflow_test.dart. Volle Suite (626 Tests) grün. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SM6e73V9ZethPiqHSocxUA
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0fe624d1e8
commit
bc58438262
@@ -3,6 +3,7 @@ import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart' show OverflowBoxFit;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -61,7 +62,43 @@ class NowPlayingScreen extends StatelessWidget {
|
||||
return LayoutBuilder(
|
||||
builder: (context, raum) {
|
||||
final nebeneinander = raum.maxWidth > raum.maxHeight;
|
||||
final inhalt = _Angaben(item: item, handler: handler);
|
||||
// Während der Öffnen-Geste ist das interpolierte
|
||||
// Rechteck (main.dart, Rect.lerp Mini-/Vollbild-
|
||||
// Player) oft noch kleiner, als _Angaben an
|
||||
// Mindesthöhe braucht — ohne diese Kapselung meldet
|
||||
// Flutter dann "RenderFlex overflowed" (im Row-Zweig
|
||||
// unten bekäme _Angaben selbst eine zu knappe Höhe
|
||||
// zugeteilt, im Column-Zweig überschreitet Lücke +
|
||||
// _Angaben-Eigenhöhe zusammen die verfügbare Höhe).
|
||||
// ConstrainedBox AUSSEN um OverflowBox deckelt, was
|
||||
// OverflowBox nach außen als eigene Größe meldet,
|
||||
// hart auf das von raum bekannte Maß; OverflowBox
|
||||
// selbst gibt ihrem Kind (_Angaben) über
|
||||
// maxHeight: infinity davon UNABHÄNGIG weiterhin
|
||||
// unbegrenzte Höhe — _Angaben behält so seine
|
||||
// natürliche Größe, der überschüssige Teil wird
|
||||
// vom ClipRect einfach nicht gemalt statt eine
|
||||
// Overflow-Warnung auszulösen. Läge die Deckelung
|
||||
// stattdessen INNEN (zwischen OverflowBox und
|
||||
// _Angaben), bekäme _Angaben selbst zu wenig Höhe
|
||||
// und liefe intern über. Bei ausreichend Platz
|
||||
// (Endzustand progress==1) ist raum.maxHeight groß
|
||||
// genug, dass nichts geklemmt wird — die Optik
|
||||
// bleibt dort unverändert.
|
||||
final inhalt = ClipRect(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: (raum.maxHeight - MeloSpace.lg)
|
||||
.clamp(0.0, double.infinity),
|
||||
),
|
||||
child: OverflowBox(
|
||||
alignment: Alignment.center,
|
||||
maxHeight: double.infinity,
|
||||
fit: OverflowBoxFit.deferToChild,
|
||||
child: _Angaben(item: item, handler: handler),
|
||||
),
|
||||
),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: nebeneinander
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:drift/drift.dart' show driftRuntimeOptions;
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:melo/library/category_service.dart';
|
||||
import 'package:melo/library/database.dart';
|
||||
import 'package:melo/library/playlist_service.dart';
|
||||
import 'package:melo/player/audio_handler.dart';
|
||||
import 'package:melo/player/mini_player.dart';
|
||||
import 'package:melo/player/now_playing_screen.dart';
|
||||
import 'package:melo/player/player_expansion_controller.dart';
|
||||
import 'package:melo/settings/app_settings.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Ersetzt in [MeloAudioHandler.loadPlaylist] den echten just_audio-Zugriff —
|
||||
/// dieselbe Klasse wie in now_playing_heart_test.dart.
|
||||
class _FakeAudioHandler extends MeloAudioHandler {
|
||||
_FakeAudioHandler(MeloDb db) : super(db: db);
|
||||
|
||||
@override
|
||||
Future<int> loadPlaylist(List<MediaItem> items, {int startIndex = 0}) async {
|
||||
queue.add(items);
|
||||
if (items.isNotEmpty) mediaItem.add(items[startIndex]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
void stelleKanaeleRuhig() {
|
||||
final messenger =
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
|
||||
messenger.setMockMethodCallHandler(
|
||||
const MethodChannel('plugins.flutter.io/path_provider'),
|
||||
(call) async => '/tmp/melo-test',
|
||||
);
|
||||
messenger.setMockMethodCallHandler(
|
||||
const MethodChannel('plugins.it_nomads.com/flutter_secure_storage'),
|
||||
(call) async => null,
|
||||
);
|
||||
}
|
||||
|
||||
setUp(() => SharedPreferences.setMockInitialValues({}));
|
||||
|
||||
testWidgets(
|
||||
'NowPlayingScreen in einem während der Öffnen-Geste noch kleinen, '
|
||||
'interpolierten Rechteck (progress ~0.4) wirft keinen '
|
||||
'RenderFlex-Overflow', (tester) async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
stelleKanaeleRuhig();
|
||||
final db = MeloDb(NativeDatabase.memory());
|
||||
final kategorien = CategoryService(db);
|
||||
final playlists = PlaylistService(db);
|
||||
final einstellungen = AppSettings();
|
||||
await einstellungen.init();
|
||||
final handler = _FakeAudioHandler(db);
|
||||
addTearDown(kategorien.dispose);
|
||||
addTearDown(playlists.dispose);
|
||||
|
||||
// Ein echtes MediaItem, wie es beim Abspielen im mediaItem-Stream steht
|
||||
// — NowPlayingScreen zeigt sonst nur "Nichts in Wiedergabe" und hat dann
|
||||
// gar keinen Inhalt, der überlaufen könnte.
|
||||
handler.mediaItem.add(const MediaItem(
|
||||
id: 'song-1',
|
||||
title: 'Roses',
|
||||
artist: 'Neoni',
|
||||
));
|
||||
|
||||
// Dieselbe Rect-Interpolation wie HomeShell._miniRect/_vollbildRect bei
|
||||
// progress=0.4 auf der Standard-Testbildschirmgröße (800x600, siehe
|
||||
// flutter_test-Default) — main.dart positioniert NowPlayingScreen
|
||||
// während der Öffnen-Geste über genau so ein Positioned.fromRect mit
|
||||
// Rect.lerp zwischen Mini- und Vollbild-Rechteck. Bei p>0.3 blendet der
|
||||
// Inhalt laut Spec schon sichtbar ein (main.dart: Opacity((p-0.3)/0.7)),
|
||||
// das Rechteck ist zu dem Zeitpunkt aber noch weit von der
|
||||
// Vollbildgröße entfernt.
|
||||
const p = 0.4;
|
||||
const screenSize = Size(800, 600);
|
||||
const miniHoehe = MiniPlayer.hoehe;
|
||||
final unten = screenSize.height - miniHoehe - kBottomNavigationBarHeight;
|
||||
final miniRect = Rect.fromLTWH(0, unten, screenSize.width, miniHoehe);
|
||||
final vollbildRect =
|
||||
Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
|
||||
final rect = Rect.lerp(miniRect, vollbildRect, p)!;
|
||||
|
||||
final expansion = PlayerExpansionController(vsync: tester);
|
||||
addTearDown(expansion.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
Provider<MeloDb>.value(value: db),
|
||||
Provider<MeloAudioHandler>.value(value: handler),
|
||||
ChangeNotifierProvider<CategoryService>.value(value: kategorien),
|
||||
ChangeNotifierProvider<PlaylistService>.value(value: playlists),
|
||||
ChangeNotifierProvider<AppSettings>.value(value: einstellungen),
|
||||
ChangeNotifierProvider<PlayerExpansionController>.value(
|
||||
value: expansion),
|
||||
],
|
||||
child: MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fromRect(
|
||||
rect: rect,
|
||||
child: const NowPlayingScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
// Aufräum-Reihenfolge wie in den übrigen Tests dieser Datei-Familie:
|
||||
// erst den Baum abbauen, dann den Handler disposen (offener Positions-
|
||||
// Timer, siehe now_playing_heart_test.dart).
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
handler.dispose();
|
||||
await tester.pump(const Duration(milliseconds: 1));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user