HomeShell: Blur-Layer und Mini-Player-Fade an progress gekoppelt
This commit is contained in:
+59
-3
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -255,15 +256,69 @@ class _HomeShellState extends State<HomeShell>
|
|||||||
child: child!,
|
child: child!,
|
||||||
),
|
),
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: Column(
|
extendBody: true, // NowPlayingScreen (Task 5) braucht die volle Höhe bis unter die BottomNavigationBar
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _expansion,
|
||||||
|
builder: (context, child) => IgnorePointer(
|
||||||
|
ignoring: _expansion.progress > 0,
|
||||||
|
child: ExcludeSemantics(
|
||||||
|
excluding: _expansion.progress > 0,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: IndexedStack(index: _index, children: tabs)),
|
Expanded(child: IndexedStack(index: _index, children: tabs)),
|
||||||
const MiniPlayer(),
|
const SizedBox(height: MiniPlayer.hoehe), // Platz für den Overlay-Mini-Player
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _expansion,
|
||||||
|
builder: (context, child) {
|
||||||
|
final p = _expansion.progress;
|
||||||
|
if (p == 0) return const SizedBox.shrink();
|
||||||
|
return Positioned.fill(
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: p * 20.0, sigmaY: p * 20.0),
|
||||||
|
child: Container(color: Colors.transparent),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _expansion,
|
||||||
|
builder: (context, child) => Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: (1 - _expansion.progress).clamp(0.0, 1.0),
|
||||||
|
child: IgnorePointer(
|
||||||
|
ignoring: _expansion.progress > 0,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const MiniPlayer(),
|
||||||
|
),
|
||||||
|
// NowPlayingScreen-Overlay-Inhalt folgt in Task 5 hier.
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// Haarlinie darüber: ohne sie geht die Leiste auf schwarzem Grund
|
// Haarlinie darüber: ohne sie geht die Leiste auf schwarzem Grund
|
||||||
// optisch im Inhalt auf und wirkt nicht wie ein Hauptmenü.
|
// optisch im Inhalt auf und wirkt nicht wie ein Hauptmenü.
|
||||||
bottomNavigationBar: DecoratedBox(
|
bottomNavigationBar: AnimatedBuilder(
|
||||||
|
animation: _expansion,
|
||||||
|
builder: (context, child) => Opacity(
|
||||||
|
opacity: (1 - _expansion.progress).clamp(0.0, 1.0),
|
||||||
|
child: IgnorePointer(
|
||||||
|
ignoring: _expansion.progress > 0,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: DecoratedBox(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
border: Border(top: BorderSide(color: MeloTheme.border)),
|
border: Border(top: BorderSide(color: MeloTheme.border)),
|
||||||
),
|
),
|
||||||
@@ -291,6 +346,7 @@ class _HomeShellState extends State<HomeShell>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -9,7 +11,8 @@ import 'package:melo/player/player_expansion_controller.dart';
|
|||||||
// sobald Task 4/5 die Stack-Struktur eingeführt haben; dieser erste Test
|
// sobald Task 4/5 die Stack-Struktur eingeführt haben; dieser erste Test
|
||||||
// sichert nur die PopScope-Erweiterung ab, isoliert an einem Test-Double.
|
// sichert nur die PopScope-Erweiterung ab, isoliert an einem Test-Double.
|
||||||
class _TestShell extends StatefulWidget {
|
class _TestShell extends StatefulWidget {
|
||||||
const _TestShell();
|
const _TestShell({this.tabContent = const SizedBox.shrink()});
|
||||||
|
final Widget tabContent;
|
||||||
@override
|
@override
|
||||||
State<_TestShell> createState() => _TestShellState();
|
State<_TestShell> createState() => _TestShellState();
|
||||||
}
|
}
|
||||||
@@ -46,7 +49,38 @@ class _TestShellState extends State<_TestShell> with TickerProviderStateMixin {
|
|||||||
},
|
},
|
||||||
child: child!,
|
child: child!,
|
||||||
),
|
),
|
||||||
child: const Scaffold(body: SizedBox()),
|
// Derselbe Stack-Aufbau wie HomeShell (Task 4): Blur-Layer und
|
||||||
|
// Tab-Blocker bekommen jeweils ihren eigenen AnimatedBuilder, NICHT
|
||||||
|
// nur den äußeren aus dem PopScope-Consumer oben (Reaktivitäts-Regel
|
||||||
|
// aus dem Plan) — sonst würde dieser Test den genau davor gefundenen
|
||||||
|
// Bug (Review-Panel) nicht abdecken.
|
||||||
|
child: Scaffold(
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) => IgnorePointer(
|
||||||
|
ignoring: _controller.progress > 0,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
child: widget.tabContent,
|
||||||
|
),
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
final p = _controller.progress;
|
||||||
|
if (p == 0) return const SizedBox.shrink();
|
||||||
|
return Positioned.fill(
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: p * 20.0, sigmaY: p * 20.0),
|
||||||
|
child: Container(color: Colors.transparent),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -70,4 +104,42 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(controller.progress, 0.0);
|
expect(controller.progress, 0.0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Blur-Layer erscheint erst, wenn progress > 0', (tester) async {
|
||||||
|
await tester.pumpWidget(const MaterialApp(home: _TestShell()));
|
||||||
|
expect(find.byType(BackdropFilter), findsNothing);
|
||||||
|
|
||||||
|
final controller =
|
||||||
|
tester.state<_TestShellState>(find.byType(_TestShell))._controller;
|
||||||
|
controller.open(tester.element(find.byType(_TestShell)));
|
||||||
|
// Ein zusätzlicher, leerer pump() startet den Ticker — sein erster Tick
|
||||||
|
// liefert laut Flutter-Konvention elapsed=0 (Baseline), erst der
|
||||||
|
// folgende pump(duration) rückt die Animation tatsächlich vorwärts.
|
||||||
|
// Ohne diesen Zwischenschritt bliebe progress nach nur einem
|
||||||
|
// pump(duration) fälschlich bei 0.0 (per Debug-Ausgabe verifiziert).
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 50)); // mitten in der Animation
|
||||||
|
expect(find.byType(BackdropFilter), findsOneWidget);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Tab-Inhalt ist bei offenem Player nicht mehr antippbar',
|
||||||
|
(tester) async {
|
||||||
|
var getappt = false;
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: _TestShell(
|
||||||
|
tabContent: GestureDetector(
|
||||||
|
onTap: () => getappt = true,
|
||||||
|
child: const SizedBox(width: 200, height: 200),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
final controller =
|
||||||
|
tester.state<_TestShellState>(find.byType(_TestShell))._controller;
|
||||||
|
controller.open(tester.element(find.byType(_TestShell)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byType(GestureDetector), warnIfMissed: false);
|
||||||
|
expect(getappt, isFalse);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user