fix: excessive repaints caused by Player progress bar

This commit is contained in:
Kingkor Roy Tirtho 2023-06-10 14:22:19 +06:00
parent 4ec04240a5
commit 09b24cf1fd
2 changed files with 287 additions and 276 deletions

View File

@ -89,208 +89,215 @@ class PlayerControls extends HookConsumerWidget {
iconSize: compact ? 18 : 24, iconSize: compact ? 18 : 24,
); );
return GestureDetector( return RepaintBoundary(
behavior: HitTestBehavior.translucent, child: GestureDetector(
onTap: () { behavior: HitTestBehavior.translucent,
if (focusNode.canRequestFocus) { onTap: () {
focusNode.requestFocus(); if (focusNode.canRequestFocus) {
} focusNode.requestFocus();
}, }
child: FocusableActionDetector( },
focusNode: focusNode, child: FocusableActionDetector(
shortcuts: shortcuts, focusNode: focusNode,
actions: actions, shortcuts: shortcuts,
child: Container( actions: actions,
constraints: const BoxConstraints(maxWidth: 600), child: Container(
child: Column( constraints: const BoxConstraints(maxWidth: 600),
children: [ child: Column(
if (!compact) children: [
HookBuilder( if (!compact)
builder: (context) { HookBuilder(
final ( builder: (context) {
:bufferProgress, final (
:duration, :bufferProgress,
:position, :duration,
:progressStatic :position,
) = useProgress(ref); :progressStatic
) = useProgress(ref);
final totalMinutes = PrimitiveUtils.zeroPadNumStr( final totalMinutes = PrimitiveUtils.zeroPadNumStr(
duration.inMinutes.remainder(60), duration.inMinutes.remainder(60),
); );
final totalSeconds = PrimitiveUtils.zeroPadNumStr( final totalSeconds = PrimitiveUtils.zeroPadNumStr(
duration.inSeconds.remainder(60), duration.inSeconds.remainder(60),
); );
final currentMinutes = PrimitiveUtils.zeroPadNumStr( final currentMinutes = PrimitiveUtils.zeroPadNumStr(
position.inMinutes.remainder(60), position.inMinutes.remainder(60),
); );
final currentSeconds = PrimitiveUtils.zeroPadNumStr( final currentSeconds = PrimitiveUtils.zeroPadNumStr(
position.inSeconds.remainder(60), position.inSeconds.remainder(60),
); );
final progress = useState<num>( final progress = useState<num>(
useMemoized(() => progressStatic, []), useMemoized(() => progressStatic, []),
); );
useEffect(() { useEffect(() {
progress.value = progressStatic; progress.value = progressStatic;
return null; return null;
}, [progressStatic]); }, [progressStatic]);
return Column( return Column(
children: [ children: [
Tooltip( Tooltip(
message: context.l10n.slide_to_seek, message: context.l10n.slide_to_seek,
child: Slider( child: Slider(
// cannot divide by zero // cannot divide by zero
// there's an edge case for value being bigger // there's an edge case for value being bigger
// than total duration. Keeping it resolved // than total duration. Keeping it resolved
value: progress.value.toDouble(), value: progress.value.toDouble(),
secondaryTrackValue: bufferProgress, secondaryTrackValue: bufferProgress,
onChanged: playlist.isFetching == true || buffering onChanged:
playlist.isFetching == true || buffering
? null
: (v) {
progress.value = v;
},
onChangeEnd: (value) async {
await audioPlayer.seek(
Duration(
seconds:
(value * duration.inSeconds).toInt(),
),
);
},
activeColor: sliderColor,
secondaryActiveColor:
sliderColor.withOpacity(0.2),
inactiveColor: sliderColor.withOpacity(0.15),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
),
child: DefaultTextStyle(
style: theme.textTheme.bodySmall!.copyWith(
color: palette?.dominantColor?.bodyTextColor,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text("$currentMinutes:$currentSeconds"),
Text("$totalMinutes:$totalSeconds"),
],
),
),
),
],
);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
StreamBuilder<bool>(
stream: audioPlayer.shuffledStream,
builder: (context, snapshot) {
final shuffled = snapshot.data ?? false;
return IconButton(
tooltip: shuffled
? context.l10n.unshuffle_playlist
: context.l10n.shuffle_playlist,
icon: const Icon(SpotubeIcons.shuffle),
style: shuffled ? activeButtonStyle : buttonStyle,
onPressed: playlist.isFetching == true || buffering
? null ? null
: (v) { : () {
progress.value = v; if (shuffled) {
audioPlayer.setShuffle(false);
} else {
audioPlayer.setShuffle(true);
}
}, },
onChangeEnd: (value) async { );
await audioPlayer.seek( }),
Duration( IconButton(
seconds: (value * duration.inSeconds).toInt(), tooltip: context.l10n.previous_track,
), icon: const Icon(SpotubeIcons.skipBack),
); style: buttonStyle,
}, onPressed: playlist.isFetching == true || buffering
activeColor: sliderColor, ? null
secondaryActiveColor: sliderColor.withOpacity(0.2), : playlistNotifier.previous,
inactiveColor: sliderColor.withOpacity(0.15), ),
), IconButton(
), tooltip: playing
Padding( ? context.l10n.pause_playback
padding: const EdgeInsets.symmetric( : context.l10n.resume_playback,
horizontal: 8.0, icon: playlist.isFetching == true
), ? SizedBox(
child: DefaultTextStyle( height: 20,
style: theme.textTheme.bodySmall!.copyWith( width: 20,
color: palette?.dominantColor?.bodyTextColor, child: CircularProgressIndicator(
color: accentColor?.titleTextColor ??
theme.colorScheme.onPrimary,
),
)
: Icon(
playing ? SpotubeIcons.pause : SpotubeIcons.play,
), ),
child: Row( style: resumePauseStyle,
mainAxisAlignment: MainAxisAlignment.spaceBetween, onPressed: playlist.isFetching == true
children: [ ? null
Text("$currentMinutes:$currentSeconds"), : Actions.handler<PlayPauseIntent>(
Text("$totalMinutes:$totalSeconds"), context,
], PlayPauseIntent(ref),
), ),
), ),
), IconButton(
], tooltip: context.l10n.next_track,
); icon: const Icon(SpotubeIcons.skipForward),
}, style: buttonStyle,
onPressed: playlist.isFetching == true || buffering
? null
: playlistNotifier.next,
),
StreamBuilder<PlaybackLoopMode>(
stream: audioPlayer.loopModeStream,
builder: (context, snapshot) {
final loopMode =
snapshot.data ?? PlaybackLoopMode.none;
return IconButton(
tooltip: loopMode == PlaybackLoopMode.one
? context.l10n.loop_track
: loopMode == PlaybackLoopMode.all
? context.l10n.repeat_playlist
: null,
icon: Icon(
loopMode == PlaybackLoopMode.one
? SpotubeIcons.repeatOne
: SpotubeIcons.repeat,
),
style: loopMode == PlaybackLoopMode.one ||
loopMode == PlaybackLoopMode.all
? activeButtonStyle
: buttonStyle,
onPressed: playlist.isFetching == true || buffering
? null
: () async {
switch (await audioPlayer.loopMode) {
case PlaybackLoopMode.all:
audioPlayer
.setLoopMode(PlaybackLoopMode.one);
break;
case PlaybackLoopMode.one:
audioPlayer
.setLoopMode(PlaybackLoopMode.none);
break;
case PlaybackLoopMode.none:
audioPlayer
.setLoopMode(PlaybackLoopMode.all);
break;
}
},
);
}),
],
), ),
Row( const SizedBox(height: 5)
mainAxisAlignment: MainAxisAlignment.spaceEvenly, ],
children: [ ),
StreamBuilder<bool>(
stream: audioPlayer.shuffledStream,
builder: (context, snapshot) {
final shuffled = snapshot.data ?? false;
return IconButton(
tooltip: shuffled
? context.l10n.unshuffle_playlist
: context.l10n.shuffle_playlist,
icon: const Icon(SpotubeIcons.shuffle),
style: shuffled ? activeButtonStyle : buttonStyle,
onPressed: playlist.isFetching == true || buffering
? null
: () {
if (shuffled) {
audioPlayer.setShuffle(false);
} else {
audioPlayer.setShuffle(true);
}
},
);
}),
IconButton(
tooltip: context.l10n.previous_track,
icon: const Icon(SpotubeIcons.skipBack),
style: buttonStyle,
onPressed: playlist.isFetching == true || buffering
? null
: playlistNotifier.previous,
),
IconButton(
tooltip: playing
? context.l10n.pause_playback
: context.l10n.resume_playback,
icon: playlist.isFetching == true
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: accentColor?.titleTextColor ??
theme.colorScheme.onPrimary,
),
)
: Icon(
playing ? SpotubeIcons.pause : SpotubeIcons.play,
),
style: resumePauseStyle,
onPressed: playlist.isFetching == true
? null
: Actions.handler<PlayPauseIntent>(
context,
PlayPauseIntent(ref),
),
),
IconButton(
tooltip: context.l10n.next_track,
icon: const Icon(SpotubeIcons.skipForward),
style: buttonStyle,
onPressed: playlist.isFetching == true || buffering
? null
: playlistNotifier.next,
),
StreamBuilder<PlaybackLoopMode>(
stream: audioPlayer.loopModeStream,
builder: (context, snapshot) {
final loopMode = snapshot.data ?? PlaybackLoopMode.none;
return IconButton(
tooltip: loopMode == PlaybackLoopMode.one
? context.l10n.loop_track
: loopMode == PlaybackLoopMode.all
? context.l10n.repeat_playlist
: null,
icon: Icon(
loopMode == PlaybackLoopMode.one
? SpotubeIcons.repeatOne
: SpotubeIcons.repeat,
),
style: loopMode == PlaybackLoopMode.one ||
loopMode == PlaybackLoopMode.all
? activeButtonStyle
: buttonStyle,
onPressed: playlist.isFetching == true || buffering
? null
: () async {
switch (await audioPlayer.loopMode) {
case PlaybackLoopMode.all:
audioPlayer
.setLoopMode(PlaybackLoopMode.one);
break;
case PlaybackLoopMode.one:
audioPlayer
.setLoopMode(PlaybackLoopMode.none);
break;
case PlaybackLoopMode.none:
audioPlayer
.setLoopMode(PlaybackLoopMode.all);
break;
}
},
);
}),
],
),
const SizedBox(height: 5)
],
), ),
), ),
), ),

View File

@ -62,95 +62,99 @@ class PlayerOverlay extends HookConsumerWidget {
child: AnimatedOpacity( child: AnimatedOpacity(
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
opacity: canShow ? 1 : 0, opacity: canShow ? 1 : 0,
child: Material( child: RepaintBoundary(
type: MaterialType.transparency, child: Material(
child: Column( type: MaterialType.transparency,
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
HookBuilder( children: [
builder: (context) { HookBuilder(
final progress = useProgress(ref); builder: (context) {
// animated final progress = useProgress(ref);
return TweenAnimationBuilder<double>( // animated
duration: const Duration(milliseconds: 250), return TweenAnimationBuilder<double>(
tween: Tween<double>( duration: const Duration(milliseconds: 250),
begin: 0, tween: Tween<double>(
end: progress.progressStatic, begin: 0,
), end: progress.progressStatic,
builder: (context, value, child) { ),
return LinearProgressIndicator( builder: (context, value, child) {
value: value, return LinearProgressIndicator(
minHeight: 2, value: value,
backgroundColor: Colors.transparent, minHeight: 2,
valueColor: AlwaysStoppedAnimation( backgroundColor: Colors.transparent,
theme.colorScheme.primary, valueColor: AlwaysStoppedAnimation(
), theme.colorScheme.primary,
); ),
}, );
); },
}, );
), },
Expanded( ),
child: Row( Expanded(
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
Expanded( children: [
child: MouseRegion( Expanded(
cursor: SystemMouseCursors.click, child: MouseRegion(
child: GestureDetector( cursor: SystemMouseCursors.click,
onTap: () => child: GestureDetector(
GoRouter.of(context).push("/player"), onTap: () =>
child: PlayerTrackDetails( GoRouter.of(context).push("/player"),
albumArt: albumArt, child: PlayerTrackDetails(
color: textColor, albumArt: albumArt,
color: textColor,
),
), ),
), ),
), ),
), Row(
Row( children: [
children: [ IconButton(
IconButton( icon: Icon(
icon: Icon( SpotubeIcons.skipBack,
SpotubeIcons.skipBack, color: textColor,
color: textColor, ),
onPressed: playlistNotifier.previous,
), ),
onPressed: playlistNotifier.previous, Consumer(
), builder: (context, ref, _) {
Consumer( return IconButton(
builder: (context, ref, _) { icon: playlist.isFetching
return IconButton( ? const SizedBox(
icon: playlist.isFetching height: 20,
? const SizedBox( width: 20,
height: 20, child:
width: 20, CircularProgressIndicator(),
child: CircularProgressIndicator(), )
) : Icon(
: Icon( playing
playing ? SpotubeIcons.pause
? SpotubeIcons.pause : SpotubeIcons.play,
: SpotubeIcons.play, color: textColor,
color: textColor, ),
), onPressed:
onPressed: Actions.handler<PlayPauseIntent>( Actions.handler<PlayPauseIntent>(
context, context,
PlayPauseIntent(ref), PlayPauseIntent(ref),
), ),
); );
}, },
),
IconButton(
icon: Icon(
SpotubeIcons.skipForward,
color: textColor,
), ),
onPressed: playlistNotifier.next, IconButton(
), icon: Icon(
], SpotubeIcons.skipForward,
), color: textColor,
], ),
onPressed: playlistNotifier.next,
),
],
),
],
),
), ),
), ],
], ),
), ),
), ),
), ),