mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 04:42:02 +02:00
feat(album): zoom artwork when pulling down the Mornye header
This commit is contained in:
@@ -201,10 +201,10 @@ class AlbumDetailHeader extends StatelessWidget {
|
||||
alignment: Alignment.topCenter,
|
||||
child: SizedBox.square(
|
||||
dimension: artworkSize,
|
||||
child: artwork,
|
||||
child: _AlbumStretchArtwork(child: artwork),
|
||||
),
|
||||
)
|
||||
: artwork,
|
||||
: _AlbumStretchArtwork(child: artwork),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
@@ -458,6 +458,44 @@ class AlbumDetailHeader extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _AlbumStretchArtwork extends StatelessWidget {
|
||||
const _AlbumStretchArtwork({required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (MediaQuery.disableAnimationsOf(context)) return child;
|
||||
final position = Scrollable.of(context).position;
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) => AnimatedBuilder(
|
||||
animation: position,
|
||||
child: RepaintBoundary(child: child),
|
||||
builder: (context, artwork) {
|
||||
final overscroll = position.hasContentDimensions
|
||||
? (position.minScrollExtent - position.pixels).clamp(
|
||||
0.0,
|
||||
double.infinity,
|
||||
)
|
||||
: 0.0;
|
||||
final height = constraints.maxHeight;
|
||||
final scale = height > 0 ? 1 + overscroll / height : 1.0;
|
||||
// The viewport moves the details down during a pull. Keep the image
|
||||
// at the top and enlarge it without changing the scrollable layout.
|
||||
return Transform.translate(
|
||||
offset: Offset(0, -overscroll),
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
alignment: Alignment.topCenter,
|
||||
child: artwork,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps the pinned toolbar above the artwork without shifting the artwork or
|
||||
/// metadata down by the toolbar's safe-area height.
|
||||
class _AlbumOverlayToolbar extends SingleChildRenderObjectWidget {
|
||||
|
||||
@@ -355,6 +355,106 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
for (final squareArtwork in [true, false]) {
|
||||
for (final reduceMotion in [false, true]) {
|
||||
testWidgets(
|
||||
'album pull preserves controls (square: $squareArtwork, reduced motion: $reduceMotion)',
|
||||
(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);
|
||||
final controller = ScrollController();
|
||||
addTearDown(controller.dispose);
|
||||
const artworkKey = ValueKey('stretch-album-artwork');
|
||||
var plays = 0;
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
child: MaterialApp(
|
||||
theme: MornyeTheme.build(Brightness.dark),
|
||||
scrollBehavior: const MaterialScrollBehavior().copyWith(
|
||||
physics: const BouncingScrollPhysics(
|
||||
parent: AlwaysScrollableScrollPhysics(),
|
||||
),
|
||||
),
|
||||
builder: (context, child) => MediaQuery(
|
||||
data: MediaQuery.of(
|
||||
context,
|
||||
).copyWith(disableAnimations: reduceMotion),
|
||||
child: child!,
|
||||
),
|
||||
home: CollectionScaffold(
|
||||
scrollController: controller,
|
||||
isSelectionMode: false,
|
||||
onExitSelectionMode: () {},
|
||||
bottomInset: 0,
|
||||
appBar: AlbumDetailHeader(
|
||||
immersive: true,
|
||||
squareArtwork: squareArtwork,
|
||||
title: 'Album',
|
||||
expandedHeight: 400,
|
||||
showTitleInAppBar: false,
|
||||
background: const ColoredBox(
|
||||
key: artworkKey,
|
||||
color: Colors.orange,
|
||||
),
|
||||
actions: AlbumPlayActions(
|
||||
playLabel: 'Play',
|
||||
shuffleTooltip: 'Shuffle',
|
||||
onPlay: () => plays++,
|
||||
onShuffle: () {},
|
||||
),
|
||||
),
|
||||
slivers: const [
|
||||
SliverToBoxAdapter(child: Text('Track list')),
|
||||
SliverToBoxAdapter(child: SizedBox(height: 2000)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final artwork = find.byKey(artworkKey);
|
||||
final play = find.text('Play');
|
||||
final tracks = find.text('Track list');
|
||||
final originalArt = tester.getRect(artwork);
|
||||
final originalPlay = tester.getRect(play);
|
||||
final originalTracks = tester.getRect(tracks);
|
||||
final originalBack = tester.getRect(find.byTooltip('Back'));
|
||||
final element = tester.element(artwork);
|
||||
final gesture = await tester.startGesture(const Offset(195, 250));
|
||||
await gesture.moveBy(const Offset(0, 150));
|
||||
await tester.pump();
|
||||
final stretchedArt = tester.getRect(artwork);
|
||||
expect(controller.offset, lessThan(0));
|
||||
if (reduceMotion) {
|
||||
expect(stretchedArt.size, originalArt.size);
|
||||
} else {
|
||||
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(play), originalPlay.size);
|
||||
expect(tester.getRect(play).top, greaterThan(originalPlay.top));
|
||||
expect(tester.getRect(tracks).top, greaterThan(originalTracks.top));
|
||||
expect(tester.getRect(find.byTooltip('Back')), originalBack);
|
||||
expect(play.hitTestable(), findsOneWidget);
|
||||
expect(tester.element(artwork), same(element));
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getRect(artwork), originalArt);
|
||||
expect(tester.getRect(play), originalPlay);
|
||||
await tester.tap(play);
|
||||
expect(plays, 1);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (final brightness in Brightness.values) {
|
||||
for (final scale in [1.0, 2.0]) {
|
||||
testWidgets(
|
||||
|
||||
Reference in New Issue
Block a user