fix(artist): zoom artwork during pull-down stretching

This commit is contained in:
zarzet
2026-09-26 23:24:28 +07:00
parent 39f7f27e35
commit 2cfd2e8a48
2 changed files with 90 additions and 3 deletions
+29 -3
View File
@@ -157,9 +157,10 @@ class MornyeArtistHeader extends StatelessWidget {
final String? logoUrl;
final Widget? badge;
// A SliverMainAxisGroup clamps negative overlap, preventing this header from
// receiving the overscroll used for its stretch animation.
@override
Widget build(BuildContext context) =>
SliverMainAxisGroup(slivers: buildSlivers(context));
Widget build(BuildContext context) => buildSlivers(context).single;
List<Widget> buildSlivers(BuildContext context) {
final surface = Theme.of(context).colorScheme.surface;
@@ -206,6 +207,7 @@ class MornyeArtistHeader extends StatelessWidget {
32;
return SliverAppBar(
pinned: true,
stretch: !MediaQuery.disableAnimationsOf(context),
expandedHeight: expandedHeight - MediaQuery.paddingOf(context).top,
backgroundColor: surface,
surfaceTintColor: Colors.transparent,
@@ -233,6 +235,7 @@ class MornyeArtistHeader extends StatelessWidget {
),
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.pin,
stretchModes: const [StretchMode.zoomBackground],
background: Stack(
fit: StackFit.expand,
children: [
@@ -366,7 +369,30 @@ class _ArtistCollapsingArtwork extends ConsumerWidget {
sigmaY: blur,
tileMode: TileMode.clamp,
),
child: RepaintBoundary(child: child),
child: LayoutBuilder(
builder: (context, constraints) {
final height = settings?.maxExtent ?? constraints.maxHeight;
final zoom = height > 0
? (constraints.maxHeight / height).clamp(
1.0,
double.infinity,
)
: 1.0;
// Stretch the photograph uniformly around its top edge.
// Identity and actions remain outside this transform, moving
// down with the sliver without growing along with the image.
return OverflowBox(
alignment: Alignment.topCenter,
minHeight: height,
maxHeight: height,
child: Transform.scale(
scale: zoom,
alignment: Alignment.topCenter,
child: RepaintBoundary(child: child),
),
);
},
),
),
),
if (blurEnabled && fade < 1)
+61
View File
@@ -222,6 +222,67 @@ void main() {
expect(tester.takeException(), isNull);
});
testWidgets('pulling the artist header zooms only the artwork', (
tester,
) async {
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
tester.view.padding = FakeViewPadding(top: 59, bottom: 34);
addTearDown(tester.view.reset);
const artworkKey = ValueKey('stretch-artwork');
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
theme: MornyeTheme.build(Brightness.dark),
home: Scaffold(
body: CustomScrollView(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
slivers: const [
MornyeArtistHeader(
name: 'Artist',
artwork: ColoredBox(key: artworkKey, color: Colors.orange),
actions: [Text('Artist action')],
showTitle: false,
),
SliverToBoxAdapter(child: Text('Album list')),
SliverToBoxAdapter(child: SizedBox(height: 2000)),
],
),
),
),
),
);
await tester.pumpAndSettle();
final artwork = find.byKey(artworkKey);
final action = find.text('Artist action');
final albums = find.text('Album list');
final originalArt = tester.getRect(artwork);
final originalAction = tester.getRect(action);
final originalAlbums = tester.getRect(albums);
final element = tester.element(artwork);
final gesture = await tester.startGesture(const Offset(195, 350));
await gesture.moveBy(const Offset(0, 150));
await tester.pump();
final stretchedArt = tester.getRect(artwork);
expect(stretchedArt.width, greaterThan(originalArt.width));
expect(
stretchedArt.width / originalArt.width,
closeTo(stretchedArt.height / originalArt.height, 0.001),
);
expect(stretchedArt.top, closeTo(originalArt.top, 0.01));
expect(tester.getSize(action), originalAction.size);
expect(tester.getRect(action).top, greaterThan(originalAction.top));
expect(tester.getRect(albums).top, greaterThan(originalAlbums.top));
expect(tester.element(artwork), same(element));
await gesture.up();
await tester.pumpAndSettle();
expect(tester.getRect(artwork), originalArt);
expect(tester.getRect(action), originalAction);
expect(tester.takeException(), isNull);
});
for (final brightness in Brightness.values) {
for (final scale in [1.0, 2.0]) {
testWidgets(