mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-31 16:27:24 +02:00
fix(ui): hero continuity for motion (Apple Music) headers
Albums/artists with a motion banner have no square cover, so the Hero had no destination: the open flight ended nowhere and there was no return flight. The Hero now rides the full-bleed banner, flying the still cover image as the shuttle so the video doesn't restart mid-air.
This commit is contained in:
@@ -379,7 +379,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen>
|
|||||||
final cacheWidth = coverCacheWidthForViewport(context);
|
final cacheWidth = coverCacheWidthForViewport(context);
|
||||||
final headerBgUrl =
|
final headerBgUrl =
|
||||||
_headerImageUrl ?? widget.headerImageUrl ?? widget.coverUrl;
|
_headerImageUrl ?? widget.headerImageUrl ?? widget.coverUrl;
|
||||||
final Widget headerBgImage = headerBgUrl != null
|
Widget headerBgImage() => headerBgUrl != null
|
||||||
? CachedNetworkImage(
|
? CachedNetworkImage(
|
||||||
imageUrl: highResCoverUrl(headerBgUrl) ?? headerBgUrl,
|
imageUrl: highResCoverUrl(headerBgUrl) ?? headerBgUrl,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
@@ -397,15 +397,27 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Widget background = hasMotion
|
||||||
|
? MotionHeaderBanner(videoUrl: motionUrl, fallback: headerBgImage())
|
||||||
|
: headerBgImage();
|
||||||
|
// With a motion banner there is no square cover, so the Hero rides the
|
||||||
|
// full-bleed background instead; the shuttle flies the still image (a
|
||||||
|
// shuttle-built banner would restart the video mid-flight).
|
||||||
|
if (hasMotion && widget.heroTag != null) {
|
||||||
|
background = Hero(
|
||||||
|
tag: widget.heroTag!,
|
||||||
|
flightShuttleBuilder: (_, _, _, _, _) => headerBgImage(),
|
||||||
|
child: background,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return AlbumDetailHeader(
|
return AlbumDetailHeader(
|
||||||
title: widget.albumName,
|
title: widget.albumName,
|
||||||
expandedHeight: expandedHeight,
|
expandedHeight: expandedHeight,
|
||||||
showTitleInAppBar: showTitleInAppBar,
|
showTitleInAppBar: showTitleInAppBar,
|
||||||
backgroundColor: pageBackgroundColor,
|
backgroundColor: pageBackgroundColor,
|
||||||
heroTag: widget.heroTag,
|
heroTag: widget.heroTag,
|
||||||
background: hasMotion
|
background: background,
|
||||||
? MotionHeaderBanner(videoUrl: motionUrl, fallback: headerBgImage)
|
|
||||||
: headerBgImage,
|
|
||||||
blurAndScrimBackground: showSquareCover,
|
blurAndScrimBackground: showSquareCover,
|
||||||
coverBuilder: showSquareCover
|
coverBuilder: showSquareCover
|
||||||
? (context, coverSize) => coverThumbUrl != null
|
? (context, coverSize) => coverThumbUrl != null
|
||||||
|
|||||||
@@ -121,8 +121,18 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen>
|
|||||||
int? _monthlyListeners;
|
int? _monthlyListeners;
|
||||||
String? _error;
|
String? _error;
|
||||||
|
|
||||||
Widget _heroWrap(Widget child) =>
|
/// [shuttle], when given, is flown instead of re-instantiating [child]
|
||||||
widget.heroTag != null ? Hero(tag: widget.heroTag!, child: child) : child;
|
/// (needed for motion banners, which would restart their video mid-flight).
|
||||||
|
Widget _heroWrap(Widget child, {Widget Function()? shuttle}) =>
|
||||||
|
widget.heroTag != null
|
||||||
|
? Hero(
|
||||||
|
tag: widget.heroTag!,
|
||||||
|
flightShuttleBuilder: shuttle == null
|
||||||
|
? null
|
||||||
|
: (_, _, _, _, _) => shuttle(),
|
||||||
|
child: child,
|
||||||
|
)
|
||||||
|
: child;
|
||||||
|
|
||||||
bool _showTitleInAppBar = false;
|
bool _showTitleInAppBar = false;
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
@@ -1170,34 +1180,42 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen>
|
|||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
if (hasMotionBanner)
|
if (hasMotionBanner)
|
||||||
MotionHeaderBanner(
|
Builder(
|
||||||
videoUrl: headerVideoUrl,
|
builder: (context) {
|
||||||
fallback: hasValidImage
|
Widget fallbackImage() => hasValidImage
|
||||||
? CachedCoverImage(
|
? CachedCoverImage(
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl!,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
memCacheWidth: 800,
|
memCacheWidth: 800,
|
||||||
placeholder: (context, url) => Container(
|
placeholder: (context, url) => Container(
|
||||||
color: colorScheme.surfaceContainerHighest,
|
color: colorScheme.surfaceContainerHighest,
|
||||||
),
|
),
|
||||||
errorWidget: (context, url, error) => Container(
|
errorWidget: (context, url, error) => Container(
|
||||||
|
color: colorScheme.surfaceContainerHighest,
|
||||||
|
child: Icon(
|
||||||
|
Icons.person,
|
||||||
|
size: 80,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
color: colorScheme.surfaceContainerHighest,
|
color: colorScheme.surfaceContainerHighest,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.person,
|
Icons.person,
|
||||||
size: 80,
|
size: 80,
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
)
|
return _heroWrap(
|
||||||
: Container(
|
MotionHeaderBanner(
|
||||||
color: colorScheme.surfaceContainerHighest,
|
videoUrl: headerVideoUrl!,
|
||||||
child: Icon(
|
fallback: fallbackImage(),
|
||||||
Icons.person,
|
),
|
||||||
size: 80,
|
shuttle: fallbackImage,
|
||||||
color: colorScheme.onSurfaceVariant,
|
);
|
||||||
),
|
},
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else if (hasValidImage)
|
else if (hasValidImage)
|
||||||
_heroWrap(
|
_heroWrap(
|
||||||
|
|||||||
Reference in New Issue
Block a user