[mob][photos] Move SeekBar and PlayPauseButton widgets to separate files
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:native_video_player/native_video_player.dart";
|
||||
|
||||
class PlayPauseButton extends StatefulWidget {
|
||||
final NativeVideoPlayerController? controller;
|
||||
const PlayPauseButton(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
State<PlayPauseButton> createState() => _PlayPauseButtonState();
|
||||
}
|
||||
|
||||
class _PlayPauseButtonState extends State<PlayPauseButton> {
|
||||
bool _isPlaying = true;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if (_playbackStatus == PlaybackStatus.playing) {
|
||||
widget.controller?.pause();
|
||||
setState(() {
|
||||
_isPlaying = false;
|
||||
});
|
||||
} else {
|
||||
widget.controller?.play();
|
||||
setState(() {
|
||||
_isPlaying = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 54,
|
||||
height: 54,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(scale: animation, child: child);
|
||||
},
|
||||
switchInCurve: Curves.easeInOutQuart,
|
||||
switchOutCurve: Curves.easeInOutQuart,
|
||||
child: _isPlaying
|
||||
? const Icon(
|
||||
Icons.pause,
|
||||
size: 32,
|
||||
key: ValueKey("pause"),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.play_arrow,
|
||||
size: 36,
|
||||
key: ValueKey("play"),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PlaybackStatus? get _playbackStatus =>
|
||||
widget.controller?.playbackInfo?.status;
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:native_video_player/native_video_player.dart";
|
||||
|
||||
class SeekBar extends StatefulWidget {
|
||||
final NativeVideoPlayerController controller;
|
||||
final int? duration;
|
||||
const SeekBar(this.controller, this.duration, {super.key});
|
||||
|
||||
@override
|
||||
State<SeekBar> createState() => _SeekBarState();
|
||||
}
|
||||
|
||||
class _SeekBarState extends State<SeekBar> with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _animationController;
|
||||
double _prevPositionFraction = 0.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
value: 0,
|
||||
);
|
||||
|
||||
widget.controller.onPlaybackStatusChanged.addListener(
|
||||
_onPlaybackStatusChanged,
|
||||
);
|
||||
widget.controller.onPlaybackPositionChanged.addListener(
|
||||
_onPlaybackPositionChanged,
|
||||
);
|
||||
|
||||
_startMovingSeekbar();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
widget.controller.onPlaybackStatusChanged.removeListener(
|
||||
_onPlaybackStatusChanged,
|
||||
);
|
||||
widget.controller.onPlaybackPositionChanged.removeListener(
|
||||
_onPlaybackPositionChanged,
|
||||
);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (_, __) {
|
||||
return SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: 2.0,
|
||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8.0),
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14.0),
|
||||
activeTrackColor: Colors.red,
|
||||
inactiveTrackColor: Colors.grey,
|
||||
thumbColor: Colors.red,
|
||||
overlayColor: Colors.red.withOpacity(0.4),
|
||||
),
|
||||
child: Slider(
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
value: _animationController.value,
|
||||
onChanged: (value) {},
|
||||
divisions: 4500,
|
||||
onChangeEnd: (value) {
|
||||
widget.controller.seekTo((value * widget.duration!).round());
|
||||
_animationController.animateTo(
|
||||
value,
|
||||
duration: const Duration(microseconds: 0),
|
||||
);
|
||||
},
|
||||
allowedInteraction: SliderInteraction.tapAndSlide,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startMovingSeekbar() {
|
||||
//Video starts playing after a slight delay. This delay is to ensure that
|
||||
//the seek bar animation starts after the video starts playing.
|
||||
Future.delayed(const Duration(milliseconds: 700), () {
|
||||
if (widget.duration != null) {
|
||||
unawaited(
|
||||
_animationController.animateTo(
|
||||
(1 / widget.duration!),
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
unawaited(
|
||||
_animationController.animateTo(
|
||||
0,
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onPlaybackStatusChanged() {
|
||||
if (widget.controller.playbackInfo?.status == PlaybackStatus.paused) {
|
||||
_animationController.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void _onPlaybackPositionChanged() async {
|
||||
if (widget.controller.playbackInfo?.status == PlaybackStatus.paused) {
|
||||
return;
|
||||
}
|
||||
final target = widget.controller.playbackInfo?.positionFraction ?? 0;
|
||||
|
||||
//To immediately set the position to 0 when the ends when playing in loop
|
||||
if (_prevPositionFraction == 1.0 && target == 0.0) {
|
||||
unawaited(
|
||||
_animationController.animateTo(0, duration: const Duration(seconds: 0)),
|
||||
);
|
||||
}
|
||||
|
||||
//There is a slight delay (around 350 ms) for the event being listened to
|
||||
//by this listener on the next target (target that comes after 0). Adding
|
||||
//this buffer to keep the seek bar animation smooth.
|
||||
if (target == 0) {
|
||||
await Future.delayed(const Duration(milliseconds: 450));
|
||||
}
|
||||
|
||||
if (widget.duration != null) {
|
||||
unawaited(
|
||||
_animationController.animateTo(
|
||||
target + (1 / widget.duration!),
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
unawaited(
|
||||
_animationController.animateTo(
|
||||
target,
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_prevPositionFraction = target;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import "package:photos/models/file/extensions/file_props.dart";
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/services/files_service.dart";
|
||||
import "package:photos/ui/actions/file/file_actions.dart";
|
||||
import "package:photos/ui/viewer/file/native_video_player_controls/play_pause_button.dart";
|
||||
import "package:photos/ui/viewer/file/native_video_player_controls/seek_bar.dart";
|
||||
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import "package:photos/utils/exif_util.dart";
|
||||
@@ -177,7 +179,7 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
child: ValueListenableBuilder(
|
||||
builder: (BuildContext context, bool value, _) {
|
||||
return value
|
||||
? _SeekBar(_controller!, duration)
|
||||
? SeekBar(_controller!, duration)
|
||||
: const SizedBox();
|
||||
},
|
||||
valueListenable: _isControllerInitialized,
|
||||
@@ -346,212 +348,3 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PlayPauseButton extends StatefulWidget {
|
||||
final NativeVideoPlayerController? controller;
|
||||
const PlayPauseButton(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
State<PlayPauseButton> createState() => _PlayPauseButtonState();
|
||||
}
|
||||
|
||||
class _PlayPauseButtonState extends State<PlayPauseButton> {
|
||||
bool _isPlaying = true;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if (_playbackStatus == PlaybackStatus.playing) {
|
||||
widget.controller?.pause();
|
||||
setState(() {
|
||||
_isPlaying = false;
|
||||
});
|
||||
} else {
|
||||
widget.controller?.play();
|
||||
setState(() {
|
||||
_isPlaying = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 54,
|
||||
height: 54,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(scale: animation, child: child);
|
||||
},
|
||||
switchInCurve: Curves.easeInOutQuart,
|
||||
switchOutCurve: Curves.easeInOutQuart,
|
||||
child: _isPlaying
|
||||
? const Icon(
|
||||
Icons.pause,
|
||||
size: 32,
|
||||
key: ValueKey("pause"),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.play_arrow,
|
||||
size: 36,
|
||||
key: ValueKey("play"),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PlaybackStatus? get _playbackStatus =>
|
||||
widget.controller?.playbackInfo?.status;
|
||||
}
|
||||
|
||||
class _SeekBar extends StatefulWidget {
|
||||
final NativeVideoPlayerController controller;
|
||||
final int? duration;
|
||||
const _SeekBar(this.controller, this.duration);
|
||||
|
||||
@override
|
||||
State<_SeekBar> createState() => _SeekBarState();
|
||||
}
|
||||
|
||||
class _SeekBarState extends State<_SeekBar>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _animationController;
|
||||
double _prevPositionFraction = 0.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
value: 0,
|
||||
);
|
||||
|
||||
widget.controller.onPlaybackStatusChanged.addListener(
|
||||
_onPlaybackStatusChanged,
|
||||
);
|
||||
widget.controller.onPlaybackPositionChanged.addListener(
|
||||
_onPlaybackPositionChanged,
|
||||
);
|
||||
|
||||
_startMovingSeekbar();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
widget.controller.onPlaybackStatusChanged.removeListener(
|
||||
_onPlaybackStatusChanged,
|
||||
);
|
||||
widget.controller.onPlaybackPositionChanged.removeListener(
|
||||
_onPlaybackPositionChanged,
|
||||
);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (_, __) {
|
||||
return SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
trackHeight: 2.0,
|
||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8.0),
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14.0),
|
||||
activeTrackColor: Colors.red,
|
||||
inactiveTrackColor: Colors.grey,
|
||||
thumbColor: Colors.red,
|
||||
overlayColor: Colors.red.withOpacity(0.4),
|
||||
),
|
||||
child: Slider(
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
value: _animationController.value,
|
||||
onChanged: (value) {},
|
||||
divisions: 4500,
|
||||
onChangeEnd: (value) {
|
||||
widget.controller.seekTo((value * widget.duration!).round());
|
||||
_animationController.animateTo(
|
||||
value,
|
||||
duration: const Duration(microseconds: 0),
|
||||
);
|
||||
},
|
||||
allowedInteraction: SliderInteraction.tapAndSlide,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startMovingSeekbar() {
|
||||
//Video starts playing after a slight delay. This delay is to ensure that
|
||||
//the seek bar animation starts after the video starts playing.
|
||||
Future.delayed(const Duration(milliseconds: 700), () {
|
||||
if (widget.duration != null) {
|
||||
unawaited(
|
||||
_animationController.animateTo(
|
||||
(1 / widget.duration!),
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
unawaited(
|
||||
_animationController.animateTo(
|
||||
0,
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onPlaybackStatusChanged() {
|
||||
if (widget.controller.playbackInfo?.status == PlaybackStatus.paused) {
|
||||
_animationController.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void _onPlaybackPositionChanged() async {
|
||||
if (widget.controller.playbackInfo?.status == PlaybackStatus.paused) {
|
||||
return;
|
||||
}
|
||||
final target = widget.controller.playbackInfo?.positionFraction ?? 0;
|
||||
|
||||
//To immediately set the position to 0 when the ends when playing in loop
|
||||
if (_prevPositionFraction == 1.0 && target == 0.0) {
|
||||
unawaited(
|
||||
_animationController.animateTo(0, duration: const Duration(seconds: 0)),
|
||||
);
|
||||
}
|
||||
|
||||
//There is a slight delay (around 350 ms) for the event being listened to
|
||||
//by this listener on the next target (target that comes after 0). Adding
|
||||
//this buffer to keep the seek bar animation smooth.
|
||||
if (target == 0) {
|
||||
await Future.delayed(const Duration(milliseconds: 450));
|
||||
}
|
||||
|
||||
if (widget.duration != null) {
|
||||
unawaited(
|
||||
_animationController.animateTo(
|
||||
target + (1 / widget.duration!),
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
unawaited(
|
||||
_animationController.animateTo(
|
||||
target,
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_prevPositionFraction = target;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user