fix(performance): always running marquee text causes high GPU usage #175 and UserArtist overflow on smaller displays

This commit is contained in:
Kingkor Roy Tirtho 2022-08-18 12:15:31 +06:00
parent e48b67cd47
commit a23ce61446
7 changed files with 232 additions and 188 deletions

View File

@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/HoverBuilder.dart';
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart'; import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
class ArtistCard extends StatelessWidget { class ArtistCard extends StatelessWidget {
@ -15,52 +16,57 @@ class ArtistCard extends StatelessWidget {
false) false)
? artist.images!.first.url! ? artist.images!.first.url!
: "https://avatars.dicebear.com/api/open-peeps/${artist.id}.png?b=%231ed760&r=50&flip=1&translateX=3&translateY=-6"); : "https://avatars.dicebear.com/api/open-peeps/${artist.id}.png?b=%231ed760&r=50&flip=1&translateX=3&translateY=-6");
return InkWell( return SizedBox(
onTap: () { height: 240,
GoRouter.of(context).push("/artist/${artist.id}"); width: 200,
}, child: InkWell(
borderRadius: BorderRadius.circular(10), onTap: () {
child: Ink( GoRouter.of(context).push("/artist/${artist.id}");
width: 200, },
decoration: BoxDecoration( borderRadius: BorderRadius.circular(10),
color: Theme.of(context).backgroundColor, child: HoverBuilder(builder: (context, isHovering) {
borderRadius: BorderRadius.circular(8), return Ink(
boxShadow: [ width: 200,
BoxShadow( decoration: BoxDecoration(
blurRadius: 10, color: Theme.of(context).backgroundColor,
offset: const Offset(0, 3), borderRadius: BorderRadius.circular(8),
spreadRadius: 5, boxShadow: [
color: Theme.of(context).shadowColor) BoxShadow(
], blurRadius: 10,
), offset: const Offset(0, 3),
child: Padding( spreadRadius: 5,
padding: const EdgeInsets.all(15), color: Theme.of(context).shadowColor)
child: Column( ],
children: [ ),
CircleAvatar( child: Padding(
maxRadius: 80, padding: const EdgeInsets.all(15),
minRadius: 20, child: Column(
backgroundImage: backgroundImage, 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,
)
],
),
),
), ),
); );
} }

View File

@ -31,8 +31,8 @@ class UserArtists extends HookConsumerWidget {
return PagedGridView( return PagedGridView(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 250, maxCrossAxisExtent: 200,
childAspectRatio: 9 / 11, mainAxisExtent: 250,
crossAxisSpacing: 20, crossAxisSpacing: 20,
mainAxisSpacing: 20, mainAxisSpacing: 20,
), ),

View File

@ -157,17 +157,12 @@ class SyncedLyrics extends HookConsumerWidget {
child: Stack( child: Stack(
children: [ children: [
Center( Center(
child: playback.track?.name != null && child: SpotubeMarqueeText(
playback.track!.name!.length > 29 text: playback.track?.name ?? "Not Playing",
? SpotubeMarqueeText( style: headlineTextStyle,
text: playback.track?.name ?? minStartLength: 29,
"Not Playing", isHovering: true,
style: headlineTextStyle, ),
)
: Text(
playback.track?.name ?? "Not Playing",
style: headlineTextStyle,
),
), ),
Positioned.fill( Positioned.fill(
child: Align( child: Align(

View File

@ -82,22 +82,16 @@ class PlayerView extends HookConsumerWidget {
children: [ children: [
SizedBox( SizedBox(
height: 30, height: 30,
child: currentTrack?.name != null && child: SpotubeMarqueeText(
currentTrack!.name!.length > 29 text: currentTrack?.name ?? "Not playing",
? SpotubeMarqueeText( style:
text: currentTrack.name ?? "Not playing", Theme.of(context).textTheme.headline5?.copyWith(
style: Theme.of(context) fontWeight: FontWeight.bold,
.textTheme color: paletteColor.titleTextColor,
.headline5 ),
?.copyWith( isHovering: true,
fontWeight: FontWeight.bold, minStartLength: 29,
color: paletteColor.titleTextColor, ),
),
)
: Text(
currentTrack?.name ?? "Not Playing",
style: Theme.of(context).textTheme.headline5,
),
), ),
TypeConversionUtils.artists_X_ClickableArtists( TypeConversionUtils.artists_X_ClickableArtists(
currentTrack?.artists ?? [], currentTrack?.artists ?? [],

View File

@ -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),
);
}
}

View File

@ -1,5 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/components/Shared/HoverBuilder.dart';
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart'; import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
class PlaybuttonCard extends StatelessWidget { class PlaybuttonCard extends StatelessWidget {
@ -32,120 +33,109 @@ class PlaybuttonCard extends StatelessWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200), constraints: const BoxConstraints(maxWidth: 200),
child: Ink( child: HoverBuilder(builder: (context, isHovering) {
decoration: BoxDecoration( return Ink(
color: Theme.of(context).backgroundColor, decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), color: Theme.of(context).backgroundColor,
boxShadow: [ borderRadius: BorderRadius.circular(8),
BoxShadow( boxShadow: [
blurRadius: 10, BoxShadow(
offset: const Offset(0, 3), blurRadius: 10,
spreadRadius: 5, offset: const Offset(0, 3),
color: Theme.of(context).shadowColor, spreadRadius: 5,
) color: Theme.of(context).shadowColor,
], )
), ],
child: Column( ),
crossAxisAlignment: CrossAxisAlignment.center, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.center,
// thumbnail of the playlist children: [
Stack( // thumbnail of the playlist
children: [ Stack(
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(
children: [ children: [
Tooltip( ClipRRect(
message: title, borderRadius: BorderRadius.circular(8),
child: SizedBox( child: CachedNetworkImage(
height: 20, imageUrl: imageUrl,
child: title.length > 25 placeholder: (context, url) =>
? SpotubeMarqueeText( Image.asset("assets/placeholder.png"),
text: title,
style: const TextStyle(
fontWeight: FontWeight.bold),
)
: Text(
title,
style: const TextStyle(
fontWeight: FontWeight.bold),
),
), ),
), ),
if (description != null) ...[ Positioned.directional(
const SizedBox(height: 10), textDirection: TextDirection.ltr,
SizedBox( bottom: 10,
height: 30, end: 5,
child: description!.length > 30 child: Builder(builder: (context) {
? SpotubeMarqueeText( return ElevatedButton(
text: description!, onPressed: onPlaybuttonPressed,
style: TextStyle( child: isLoading
fontSize: 13, ? const SizedBox(
color: Theme.of(context) height: 23,
.textTheme width: 23,
.headline4 child: CircularProgressIndicator(),
?.color, )
: Icon(
isPlaying
? Icons.pause_rounded
: Icons.play_arrow_rounded,
), ),
) style: ButtonStyle(
: Text( shape: MaterialStateProperty.all(
description!, const CircleBorder(),
style: TextStyle( ),
fontSize: 13, padding: MaterialStateProperty.all(
color: Theme.of(context) const EdgeInsets.all(16),
.textTheme ),
.headline4 ),
?.color, );
), }),
), )
),
]
], ],
), ),
), 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,
),
),
]
],
),
),
],
),
);
}),
), ),
), ),
); );

View File

@ -1,29 +1,63 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:marquee/marquee.dart'; import 'package:marquee/marquee.dart';
import 'package:spotube/utils/platform.dart';
class SpotubeMarqueeText extends StatelessWidget { class SpotubeMarqueeText extends HookWidget {
const SpotubeMarqueeText({Key? key, required this.text, this.style}) final int? minStartLength;
: super(key: key); final bool? isHovering;
const SpotubeMarqueeText({
Key? key,
required this.text,
this.style,
this.minStartLength,
this.isHovering,
}) : super(key: key);
final TextStyle? style; final TextStyle? style;
final String text; final String text;
@override @override
Widget build(BuildContext context) { 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( return Marquee(
text: text, text: text,
style: style, style: style,
scrollAxis: Axis.horizontal, scrollAxis: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
blankSpace: 60.0, blankSpace: 40.0,
velocity: 30.0, velocity: 30.0,
startAfter: const Duration(seconds: 2),
pauseAfterRound: const Duration(seconds: 2),
accelerationDuration: const Duration(seconds: 1), accelerationDuration: const Duration(seconds: 1),
accelerationCurve: Curves.linear, accelerationCurve: Curves.linear,
decelerationDuration: const Duration(milliseconds: 500), decelerationDuration: const Duration(milliseconds: 500),
decelerationCurve: Curves.easeOut, decelerationCurve: Curves.easeOut,
fadingEdgeStartFraction: 0.15, fadingEdgeStartFraction: 0.15,
fadingEdgeEndFraction: 0.15, fadingEdgeEndFraction: 0.15,
showFadingOnlyWhenScrolling: true,
onDone: () {
if (isInitial.value) {
isInitial.value = false;
hovering.value = false;
}
},
numberOfRounds: hovering.value ? null : 1,
); );
} }
} }