From a23ce614467b4297f495b824f0958ff07c21ae92 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 18 Aug 2022 12:15:31 +0600 Subject: [PATCH] fix(performance): always running marquee text causes high GPU usage #175 and UserArtist overflow on smaller displays --- lib/components/Artist/ArtistCard.dart | 96 ++++---- lib/components/Library/UserArtists.dart | 4 +- lib/components/Lyrics/SyncedLyrics.dart | 17 +- lib/components/Player/PlayerView.dart | 26 +-- lib/components/Shared/HoverBuilder.dart | 25 +++ lib/components/Shared/PlaybuttonCard.dart | 206 +++++++++--------- lib/components/Shared/SpotubeMarqueeText.dart | 46 +++- 7 files changed, 232 insertions(+), 188 deletions(-) create mode 100644 lib/components/Shared/HoverBuilder.dart diff --git a/lib/components/Artist/ArtistCard.dart b/lib/components/Artist/ArtistCard.dart index 6168f6835..f25c91701 100644 --- a/lib/components/Artist/ArtistCard.dart +++ b/lib/components/Artist/ArtistCard.dart @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/components/Shared/HoverBuilder.dart'; import 'package:spotube/components/Shared/SpotubeMarqueeText.dart'; class ArtistCard extends StatelessWidget { @@ -15,52 +16,57 @@ class ArtistCard extends StatelessWidget { false) ? artist.images!.first.url! : "https://avatars.dicebear.com/api/open-peeps/${artist.id}.png?b=%231ed760&r=50&flip=1&translateX=3&translateY=-6"); - return InkWell( - onTap: () { - GoRouter.of(context).push("/artist/${artist.id}"); - }, - borderRadius: BorderRadius.circular(10), - child: Ink( - width: 200, - decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - blurRadius: 10, - offset: const Offset(0, 3), - spreadRadius: 5, - color: Theme.of(context).shadowColor) - ], - ), - child: Padding( - padding: const EdgeInsets.all(15), - child: Column( - children: [ - CircleAvatar( - maxRadius: 80, - minRadius: 20, - backgroundImage: backgroundImage, + return SizedBox( + height: 240, + width: 200, + child: InkWell( + onTap: () { + GoRouter.of(context).push("/artist/${artist.id}"); + }, + borderRadius: BorderRadius.circular(10), + child: HoverBuilder(builder: (context, isHovering) { + return Ink( + width: 200, + decoration: BoxDecoration( + color: Theme.of(context).backgroundColor, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + blurRadius: 10, + offset: const Offset(0, 3), + spreadRadius: 5, + color: Theme.of(context).shadowColor) + ], + ), + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + CircleAvatar( + maxRadius: 80, + minRadius: 20, + backgroundImage: backgroundImage, + ), + SizedBox( + height: 20, + child: SpotubeMarqueeText( + text: artist.name!, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.bold, + ), + minStartLength: 15, + isHovering: isHovering, + ), + ), + Text( + "Artist", + style: Theme.of(context).textTheme.subtitle1, + ) + ], ), - SizedBox( - height: 30, - child: artist.name!.length > 15 - ? SpotubeMarqueeText( - text: artist.name!, - style: Theme.of(context).textTheme.headline5!, - ) - : Text( - artist.name!, - style: Theme.of(context).textTheme.headline5, - ), - ), - Text( - "Artist", - style: Theme.of(context).textTheme.subtitle1, - ) - ], - ), - ), + ), + ); + }), ), ); } diff --git a/lib/components/Library/UserArtists.dart b/lib/components/Library/UserArtists.dart index ef2b0d7a2..4014ac1d8 100644 --- a/lib/components/Library/UserArtists.dart +++ b/lib/components/Library/UserArtists.dart @@ -31,8 +31,8 @@ class UserArtists extends HookConsumerWidget { return PagedGridView( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 250, - childAspectRatio: 9 / 11, + maxCrossAxisExtent: 200, + mainAxisExtent: 250, crossAxisSpacing: 20, mainAxisSpacing: 20, ), diff --git a/lib/components/Lyrics/SyncedLyrics.dart b/lib/components/Lyrics/SyncedLyrics.dart index edee3676b..b75f264e5 100644 --- a/lib/components/Lyrics/SyncedLyrics.dart +++ b/lib/components/Lyrics/SyncedLyrics.dart @@ -157,17 +157,12 @@ class SyncedLyrics extends HookConsumerWidget { child: Stack( children: [ Center( - child: playback.track?.name != null && - playback.track!.name!.length > 29 - ? SpotubeMarqueeText( - text: playback.track?.name ?? - "Not Playing", - style: headlineTextStyle, - ) - : Text( - playback.track?.name ?? "Not Playing", - style: headlineTextStyle, - ), + child: SpotubeMarqueeText( + text: playback.track?.name ?? "Not Playing", + style: headlineTextStyle, + minStartLength: 29, + isHovering: true, + ), ), Positioned.fill( child: Align( diff --git a/lib/components/Player/PlayerView.dart b/lib/components/Player/PlayerView.dart index c7b253c75..1e9df5c3d 100644 --- a/lib/components/Player/PlayerView.dart +++ b/lib/components/Player/PlayerView.dart @@ -82,22 +82,16 @@ class PlayerView extends HookConsumerWidget { children: [ SizedBox( height: 30, - child: currentTrack?.name != null && - currentTrack!.name!.length > 29 - ? SpotubeMarqueeText( - text: currentTrack.name ?? "Not playing", - style: Theme.of(context) - .textTheme - .headline5 - ?.copyWith( - fontWeight: FontWeight.bold, - color: paletteColor.titleTextColor, - ), - ) - : Text( - currentTrack?.name ?? "Not Playing", - style: Theme.of(context).textTheme.headline5, - ), + child: SpotubeMarqueeText( + text: currentTrack?.name ?? "Not playing", + style: + Theme.of(context).textTheme.headline5?.copyWith( + fontWeight: FontWeight.bold, + color: paletteColor.titleTextColor, + ), + isHovering: true, + minStartLength: 29, + ), ), TypeConversionUtils.artists_X_ClickableArtists( currentTrack?.artists ?? [], diff --git a/lib/components/Shared/HoverBuilder.dart b/lib/components/Shared/HoverBuilder.dart new file mode 100644 index 000000000..f336c0120 --- /dev/null +++ b/lib/components/Shared/HoverBuilder.dart @@ -0,0 +1,25 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +class HoverBuilder extends HookWidget { + final Widget Function(BuildContext context, bool isHovering) builder; + const HoverBuilder({ + required this.builder, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final hovering = useState(false); + + return MouseRegion( + onEnter: (_) { + if (!hovering.value) hovering.value = true; + }, + onExit: (_) { + if (hovering.value) hovering.value = false; + }, + child: builder(context, hovering.value), + ); + } +} diff --git a/lib/components/Shared/PlaybuttonCard.dart b/lib/components/Shared/PlaybuttonCard.dart index 739cfff60..06eb305fe 100644 --- a/lib/components/Shared/PlaybuttonCard.dart +++ b/lib/components/Shared/PlaybuttonCard.dart @@ -1,5 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:spotube/components/Shared/HoverBuilder.dart'; import 'package:spotube/components/Shared/SpotubeMarqueeText.dart'; class PlaybuttonCard extends StatelessWidget { @@ -32,120 +33,109 @@ class PlaybuttonCard extends StatelessWidget { borderRadius: BorderRadius.circular(8), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 200), - child: Ink( - decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - blurRadius: 10, - offset: const Offset(0, 3), - spreadRadius: 5, - color: Theme.of(context).shadowColor, - ) - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // thumbnail of the playlist - Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: CachedNetworkImage( - imageUrl: imageUrl, - placeholder: (context, url) => - Image.asset("assets/placeholder.png"), - ), - ), - Positioned.directional( - textDirection: TextDirection.ltr, - bottom: 10, - end: 5, - child: Builder(builder: (context) { - return ElevatedButton( - onPressed: onPlaybuttonPressed, - child: isLoading - ? const SizedBox( - height: 23, - width: 23, - child: CircularProgressIndicator(), - ) - : Icon( - isPlaying - ? Icons.pause_rounded - : Icons.play_arrow_rounded, - ), - style: ButtonStyle( - shape: MaterialStateProperty.all( - const CircleBorder(), - ), - padding: MaterialStateProperty.all( - const EdgeInsets.all(16), - ), - ), - ); - }), - ) - ], - ), - const SizedBox(height: 5), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - child: Column( + child: HoverBuilder(builder: (context, isHovering) { + return Ink( + decoration: BoxDecoration( + color: Theme.of(context).backgroundColor, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + blurRadius: 10, + offset: const Offset(0, 3), + spreadRadius: 5, + color: Theme.of(context).shadowColor, + ) + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // thumbnail of the playlist + Stack( children: [ - Tooltip( - message: title, - child: SizedBox( - height: 20, - child: title.length > 25 - ? SpotubeMarqueeText( - text: title, - style: const TextStyle( - fontWeight: FontWeight.bold), - ) - : Text( - title, - style: const TextStyle( - fontWeight: FontWeight.bold), - ), + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImage( + imageUrl: imageUrl, + placeholder: (context, url) => + Image.asset("assets/placeholder.png"), ), ), - if (description != null) ...[ - const SizedBox(height: 10), - SizedBox( - height: 30, - child: description!.length > 30 - ? SpotubeMarqueeText( - text: description!, - style: TextStyle( - fontSize: 13, - color: Theme.of(context) - .textTheme - .headline4 - ?.color, - ), - ) - : Text( - description!, - style: TextStyle( - fontSize: 13, - color: Theme.of(context) - .textTheme - .headline4 - ?.color, + Positioned.directional( + textDirection: TextDirection.ltr, + bottom: 10, + end: 5, + child: Builder(builder: (context) { + return ElevatedButton( + onPressed: onPlaybuttonPressed, + child: isLoading + ? const SizedBox( + height: 23, + width: 23, + child: CircularProgressIndicator(), + ) + : Icon( + isPlaying + ? Icons.pause_rounded + : Icons.play_arrow_rounded, ), - ), - ), - ] + style: ButtonStyle( + shape: MaterialStateProperty.all( + const CircleBorder(), + ), + padding: MaterialStateProperty.all( + const EdgeInsets.all(16), + ), + ), + ); + }), + ) ], ), - ), - ], - ), - ), + const SizedBox(height: 5), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 10), + child: Column( + children: [ + Tooltip( + message: title, + child: SizedBox( + height: 20, + child: SpotubeMarqueeText( + text: title, + style: + const TextStyle(fontWeight: FontWeight.bold), + minStartLength: 25, + isHovering: isHovering, + ), + ), + ), + if (description != null) ...[ + const SizedBox(height: 10), + SizedBox( + height: 30, + child: SpotubeMarqueeText( + text: description!, + style: TextStyle( + fontSize: 13, + color: Theme.of(context) + .textTheme + .headline4 + ?.color, + ), + isHovering: isHovering, + minStartLength: 30, + ), + ), + ] + ], + ), + ), + ], + ), + ); + }), ), ), ); diff --git a/lib/components/Shared/SpotubeMarqueeText.dart b/lib/components/Shared/SpotubeMarqueeText.dart index 9a7762cd9..8b2e82719 100644 --- a/lib/components/Shared/SpotubeMarqueeText.dart +++ b/lib/components/Shared/SpotubeMarqueeText.dart @@ -1,29 +1,63 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:marquee/marquee.dart'; +import 'package:spotube/utils/platform.dart'; -class SpotubeMarqueeText extends StatelessWidget { - const SpotubeMarqueeText({Key? key, required this.text, this.style}) - : super(key: key); +class SpotubeMarqueeText extends HookWidget { + final int? minStartLength; + final bool? isHovering; + const SpotubeMarqueeText({ + Key? key, + required this.text, + this.style, + this.minStartLength, + this.isHovering, + }) : super(key: key); final TextStyle? style; final String text; @override Widget build(BuildContext context) { + final hovering = useState(false); + final isInitial = useState(true); + + useEffect(() { + if (isHovering != null && isHovering != hovering.value) { + hovering.value = isHovering!; + } + return null; + }, [isHovering]); + + if ((!isInitial.value && !hovering.value && kIsDesktop) || + minStartLength != null && text.length <= minStartLength!) { + return Text( + text, + style: style, + overflow: TextOverflow.ellipsis, + ); + } + return Marquee( text: text, style: style, scrollAxis: Axis.horizontal, crossAxisAlignment: CrossAxisAlignment.start, - blankSpace: 60.0, + blankSpace: 40.0, velocity: 30.0, - startAfter: const Duration(seconds: 2), - pauseAfterRound: const Duration(seconds: 2), accelerationDuration: const Duration(seconds: 1), accelerationCurve: Curves.linear, decelerationDuration: const Duration(milliseconds: 500), decelerationCurve: Curves.easeOut, fadingEdgeStartFraction: 0.15, fadingEdgeEndFraction: 0.15, + showFadingOnlyWhenScrolling: true, + onDone: () { + if (isInitial.value) { + isInitial.value = false; + hovering.value = false; + } + }, + numberOfRounds: hovering.value ? null : 1, ); } }