| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733 |
- import 'dart:async';
- import 'package:chewie/src/chewie_player.dart';
- import 'package:chewie/src/chewie_progress_colors.dart';
- import 'package:chewie/src/helpers/utils.dart';
- import 'package:chewie/src/material/color_compat_extensions.dart';
- import 'package:chewie/src/material/material_progress_bar.dart';
- import 'package:chewie/src/material/widgets/options_dialog.dart';
- import 'package:chewie/src/material/widgets/playback_speed_dialog.dart';
- import 'package:chewie/src/models/option_item.dart';
- import 'package:chewie/src/models/subtitle_model.dart';
- import 'package:chewie/src/notifiers/index.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter_screenutil/flutter_screenutil.dart';
- import 'package:provider/provider.dart';
- import 'package:video_player/video_player.dart';
- import '../../resource/assets.gen.dart';
- /// 自定义播放器UI
- class CustomMaterialControls extends StatefulWidget {
- final bool showPlayButton;
- const CustomMaterialControls({super.key, this.showPlayButton = true});
- @override
- State<StatefulWidget> createState() {
- return _CustomMaterialControlsState();
- }
- }
- class _CustomMaterialControlsState extends State<CustomMaterialControls>
- with SingleTickerProviderStateMixin {
- late PlayerNotifier notifier;
- late VideoPlayerValue _latestValue;
- double? _latestVolume;
- Timer? _hideTimer;
- Timer? _initTimer;
- late var _subtitlesPosition = Duration.zero;
- bool _subtitleOn = false;
- Timer? _showAfterExpandCollapseTimer;
- bool _dragging = false;
- bool _displayTapped = false;
- Timer? _bufferingDisplayTimer;
- bool _displayBufferingIndicator = false;
- final barHeight = 48.0 * 1.5;
- final marginSize = 5.0;
- late VideoPlayerController controller;
- ChewieController? _chewieController;
- // We know that _chewieController is set in didChangeDependencies
- ChewieController get chewieController => _chewieController!;
- @override
- void initState() {
- super.initState();
- notifier = Provider.of<PlayerNotifier>(context, listen: false);
- }
- @override
- Widget build(BuildContext context) {
- if (_latestValue.hasError) {
- return chewieController.errorBuilder?.call(
- context,
- chewieController.videoPlayerController.value.errorDescription!,
- ) ??
- const Center(child: Icon(Icons.error, color: Colors.white, size: 42));
- }
- return MouseRegion(
- onHover: (_) {
- _cancelAndRestartTimer();
- },
- child: GestureDetector(
- onTap: () => _cancelAndRestartTimer(),
- child: AbsorbPointer(
- absorbing: notifier.hideStuff,
- child: Stack(
- children: [
- if (_displayBufferingIndicator)
- _chewieController?.bufferingBuilder?.call(context) ??
- const Center(child: CircularProgressIndicator())
- else
- _buildHitArea(),
- _buildActionBar(),
- Column(
- mainAxisAlignment: MainAxisAlignment.end,
- children: <Widget>[
- if (_subtitleOn)
- Transform.translate(
- offset: Offset(
- 0.0,
- notifier.hideStuff ? barHeight * 0.8 : 0.0,
- ),
- child: _buildSubtitles(
- context,
- chewieController.subtitle!,
- ),
- ),
- _buildBottomBar(context),
- ],
- ),
- ],
- ),
- ),
- ),
- );
- }
- @override
- void dispose() {
- _dispose();
- super.dispose();
- }
- void _dispose() {
- controller.removeListener(_updateState);
- _hideTimer?.cancel();
- _initTimer?.cancel();
- _showAfterExpandCollapseTimer?.cancel();
- }
- @override
- void didChangeDependencies() {
- final oldController = _chewieController;
- _chewieController = ChewieController.of(context);
- controller = chewieController.videoPlayerController;
- if (oldController != chewieController) {
- _dispose();
- _initialize();
- }
- super.didChangeDependencies();
- }
- Widget _buildActionBar() {
- return Positioned(
- top: 0,
- right: 0,
- child: SafeArea(
- child: AnimatedOpacity(
- opacity: notifier.hideStuff ? 0.0 : 1.0,
- duration: const Duration(milliseconds: 250),
- child: Row(
- children: [
- _buildSubtitleToggle(),
- if (chewieController.showOptions) _buildOptionsButton(),
- ],
- ),
- ),
- ),
- );
- }
- List<OptionItem> _buildOptions(BuildContext context) {
- final options = <OptionItem>[
- OptionItem(
- onTap: (context) async {
- Navigator.pop(context);
- _onSpeedButtonTap();
- },
- iconData: Icons.speed,
- title:
- chewieController.optionsTranslation?.playbackSpeedButtonText ??
- 'Playback speed',
- ),
- ];
- if (chewieController.additionalOptions != null &&
- chewieController.additionalOptions!(context).isNotEmpty) {
- options.addAll(chewieController.additionalOptions!(context));
- }
- return options;
- }
- Widget _buildOptionsButton() {
- return AnimatedOpacity(
- opacity: notifier.hideStuff ? 0.0 : 1.0,
- duration: const Duration(milliseconds: 250),
- child: IconButton(
- onPressed: () async {
- _hideTimer?.cancel();
- if (chewieController.optionsBuilder != null) {
- await chewieController.optionsBuilder!(
- context,
- _buildOptions(context),
- );
- } else {
- await showModalBottomSheet<OptionItem>(
- context: context,
- isScrollControlled: true,
- useRootNavigator: chewieController.useRootNavigator,
- builder:
- (context) => OptionsDialog(
- options: _buildOptions(context),
- cancelButtonText:
- chewieController.optionsTranslation?.cancelButtonText,
- ),
- );
- }
- if (_latestValue.isPlaying) {
- _startHideTimer();
- }
- },
- icon: const Icon(Icons.more_vert, color: Colors.white),
- ),
- );
- }
- Widget _buildSubtitles(BuildContext context, Subtitles subtitles) {
- if (!_subtitleOn) {
- return const SizedBox();
- }
- final currentSubtitle = subtitles.getByPosition(_subtitlesPosition);
- if (currentSubtitle.isEmpty) {
- return const SizedBox();
- }
- if (chewieController.subtitleBuilder != null) {
- return chewieController.subtitleBuilder!(
- context,
- currentSubtitle.first!.text,
- );
- }
- return Padding(
- padding: EdgeInsets.all(marginSize),
- child: Container(
- padding: const EdgeInsets.all(5),
- decoration: BoxDecoration(
- color: const Color(0x96000000),
- borderRadius: BorderRadius.circular(10.0),
- ),
- child: Text(
- currentSubtitle.first!.text.toString(),
- style: const TextStyle(fontSize: 18),
- textAlign: TextAlign.center,
- ),
- ),
- );
- }
- AnimatedOpacity _buildBottomBar(BuildContext context) {
- final iconColor = Theme.of(context).textTheme.labelLarge!.color;
- return AnimatedOpacity(
- opacity: notifier.hideStuff ? 0.0 : 1.0,
- duration: const Duration(milliseconds: 300),
- child: Container(
- height: barHeight + (chewieController.isFullScreen ? 10.0 : 0),
- padding: EdgeInsets.only(
- left: 20,
- right: 20,
- bottom: !chewieController.isFullScreen ? 10.0 : 0,
- ),
- child: SafeArea(
- top: false,
- bottom: chewieController.isFullScreen,
- minimum: chewieController.controlsSafeAreaMinimum,
- child: Column(
- mainAxisSize: MainAxisSize.min,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Flexible(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: <Widget>[
- if (chewieController.isLive)
- const Expanded(child: Text('LIVE'))
- else
- _buildPosition(iconColor),
- if (chewieController.allowMuting)
- _buildMuteButton(controller),
- const Spacer(),
- if (chewieController.allowFullScreen) _buildExpandButton(),
- ],
- ),
- ),
- SizedBox(height: chewieController.isFullScreen ? 15.0 : 0),
- if (!chewieController.isLive)
- Expanded(
- child: Container(
- // padding: const EdgeInsets.symmetric(horizontal: 20),
- child: Row(children: [_buildProgressBar()]),
- ),
- ),
- ],
- ),
- ),
- ),
- );
- }
- GestureDetector _buildMuteButton(VideoPlayerController controller) {
- return GestureDetector(
- onTap: () {
- _cancelAndRestartTimer();
- if (_latestValue.volume == 0) {
- controller.setVolume(_latestVolume ?? 0.5);
- } else {
- _latestVolume = controller.value.volume;
- controller.setVolume(0.0);
- }
- },
- child: AnimatedOpacity(
- opacity: notifier.hideStuff ? 0.0 : 1.0,
- duration: const Duration(milliseconds: 300),
- child: ClipRect(
- child: Container(
- height: barHeight,
- padding: const EdgeInsets.only(left: 6.0),
- child: Icon(
- _latestValue.volume > 0 ? Icons.volume_up : Icons.volume_off,
- color: Colors.white,
- ),
- ),
- ),
- ),
- );
- }
- GestureDetector _buildExpandButton() {
- return GestureDetector(
- onTap: _onExpandCollapse,
- child: AnimatedOpacity(
- opacity: notifier.hideStuff ? 0.0 : 1.0,
- duration: const Duration(milliseconds: 300),
- child: Container(
- height: barHeight + (chewieController.isFullScreen ? 15.0 : 0),
- margin: const EdgeInsets.only(right: 12.0),
- padding: const EdgeInsets.only(left: 8.0, right: 8.0),
- child: Center(
- child: Icon(
- chewieController.isFullScreen
- ? Icons.fullscreen_exit
- : Icons.fullscreen,
- color: Colors.white,
- ),
- ),
- ),
- ),
- );
- }
- Widget _buildHitArea() {
- final bool isFinished =
- (_latestValue.position >= _latestValue.duration) &&
- _latestValue.duration.inSeconds > 0;
- final bool showPlayButton =
- widget.showPlayButton && !_dragging && !notifier.hideStuff;
- return GestureDetector(
- onTap: () {
- if (_latestValue.isPlaying) {
- if (_chewieController?.pauseOnBackgroundTap ?? false) {
- _playPause();
- _cancelAndRestartTimer();
- } else {
- if (_displayTapped) {
- setState(() {
- notifier.hideStuff = true;
- });
- } else {
- _cancelAndRestartTimer();
- }
- }
- } else {
- _playPause();
- setState(() {
- notifier.hideStuff = true;
- });
- }
- },
- child: Container(
- alignment: Alignment.center,
- color: Colors.transparent,
- // The Gesture Detector doesn't expand to the full size of the container without this; Not sure why!
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- // 快进按钮
- // if (!isFinished && !chewieController.isLive)
- // CenterSeekButton(
- // iconData: Icons.replay_10,
- // backgroundColor: Colors.black54,
- // iconColor: Colors.white,
- // show: showPlayButton,
- // fadeDuration: chewieController.materialSeekButtonFadeDuration,
- // iconSize: chewieController.materialSeekButtonSize,
- // onPressed: _seekBackward,
- // ),
- // 中间的播放、暂停按钮
- Container(
- margin: EdgeInsets.symmetric(horizontal: marginSize),
- child: _buildActionBtn(showPlayButton),
- ),
- // 第三方库中原始的播放、暂停按钮
- // Container(
- // margin: EdgeInsets.symmetric(
- // horizontal: marginSize,
- // ),
- // child: CenterPlayButton(
- // backgroundColor: Colors.black54,
- // iconColor: Colors.white,
- // isFinished: isFinished,
- // isPlaying: controller.value.isPlaying,
- // show: showPlayButton,
- // onPressed: _playPause,
- // ),
- // ),
- // 快退按钮
- // if (!isFinished && !chewieController.isLive)
- // CenterSeekButton(
- // iconData: Icons.forward_10,
- // backgroundColor: Colors.black54,
- // iconColor: Colors.white,
- // show: showPlayButton,
- // fadeDuration: chewieController.materialSeekButtonFadeDuration,
- // iconSize: chewieController.materialSeekButtonSize,
- // onPressed: _seekForward,
- // ),
- ],
- ),
- ),
- );
- }
- /// 操作按钮
- Widget _buildActionBtn(bool showPlayButton) {
- return Visibility(
- // 是否显示操作按钮
- visible: showPlayButton,
- child: Stack(
- children: [
- Visibility(
- visible: !controller.value.isPlaying,
- child: _buildPlayBtn(),
- ),
- Visibility(
- visible: controller.value.isPlaying,
- child: _buildPauseBtn(),
- ),
- ],
- ),
- );
- }
- /// 播放按钮
- Widget _buildPlayBtn() {
- return GestureDetector(
- onTap: () {
- _playPause();
- },
- child: Assets.images.iconPlay.image(width: 52.w, height: 52.w),
- );
- }
- /// 暂停按钮
- Widget _buildPauseBtn() {
- return GestureDetector(
- onTap: () {
- _playPause();
- },
- child: Assets.images.iconPause.image(width: 52.w, height: 52.w),
- );
- }
- Future<void> _onSpeedButtonTap() async {
- _hideTimer?.cancel();
- final chosenSpeed = await showModalBottomSheet<double>(
- context: context,
- isScrollControlled: true,
- useRootNavigator: chewieController.useRootNavigator,
- builder:
- (context) => PlaybackSpeedDialog(
- speeds: chewieController.playbackSpeeds,
- selected: _latestValue.playbackSpeed,
- ),
- );
- if (chosenSpeed != null) {
- controller.setPlaybackSpeed(chosenSpeed);
- }
- if (_latestValue.isPlaying) {
- _startHideTimer();
- }
- }
- Widget _buildPosition(Color? iconColor) {
- final position = _latestValue.position;
- final duration = _latestValue.duration;
- return RichText(
- text: TextSpan(
- text: '${formatDuration(position)} ',
- children: <InlineSpan>[
- TextSpan(
- text: '/ ${formatDuration(duration)}',
- style: TextStyle(
- fontSize: 14.0,
- color: Colors.white.withOpacityCompat(.75),
- fontWeight: FontWeight.normal,
- ),
- ),
- ],
- style: const TextStyle(
- fontSize: 14.0,
- color: Colors.white,
- fontWeight: FontWeight.bold,
- ),
- ),
- );
- }
- Widget _buildSubtitleToggle() {
- //if don't have subtitle hiden button
- if (chewieController.subtitle?.isEmpty ?? true) {
- return const SizedBox();
- }
- return GestureDetector(
- onTap: _onSubtitleTap,
- child: Container(
- height: barHeight,
- color: Colors.transparent,
- padding: const EdgeInsets.only(left: 12.0, right: 12.0),
- child: Icon(
- _subtitleOn
- ? Icons.closed_caption
- : Icons.closed_caption_off_outlined,
- color: _subtitleOn ? Colors.white : Colors.grey[700],
- ),
- ),
- );
- }
- void _onSubtitleTap() {
- setState(() {
- _subtitleOn = !_subtitleOn;
- });
- }
- void _cancelAndRestartTimer() {
- _hideTimer?.cancel();
- _startHideTimer();
- setState(() {
- notifier.hideStuff = false;
- _displayTapped = true;
- });
- }
- Future<void> _initialize() async {
- _subtitleOn =
- chewieController.showSubtitles &&
- (chewieController.subtitle?.isNotEmpty ?? false);
- controller.addListener(_updateState);
- _updateState();
- if (controller.value.isPlaying || chewieController.autoPlay) {
- _startHideTimer();
- }
- if (chewieController.showControlsOnInitialize) {
- _initTimer = Timer(const Duration(milliseconds: 200), () {
- setState(() {
- notifier.hideStuff = false;
- });
- });
- }
- }
- void _onExpandCollapse() {
- setState(() {
- notifier.hideStuff = true;
- chewieController.toggleFullScreen();
- _showAfterExpandCollapseTimer = Timer(
- const Duration(milliseconds: 300),
- () {
- setState(() {
- _cancelAndRestartTimer();
- });
- },
- );
- });
- }
- void _playPause() {
- final bool isFinished =
- (_latestValue.position >= _latestValue.duration) &&
- _latestValue.duration.inSeconds > 0;
- setState(() {
- if (controller.value.isPlaying) {
- notifier.hideStuff = false;
- _hideTimer?.cancel();
- controller.pause();
- } else {
- _cancelAndRestartTimer();
- if (!controller.value.isInitialized) {
- controller.initialize().then((_) {
- controller.play();
- });
- } else {
- if (isFinished) {
- controller.seekTo(Duration.zero);
- }
- controller.play();
- }
- }
- });
- }
- void _seekRelative(Duration relativeSeek) {
- _cancelAndRestartTimer();
- final position = _latestValue.position + relativeSeek;
- final duration = _latestValue.duration;
- if (position < Duration.zero) {
- controller.seekTo(Duration.zero);
- } else if (position > duration) {
- controller.seekTo(duration);
- } else {
- controller.seekTo(position);
- }
- }
- void _seekBackward() {
- _seekRelative(const Duration(seconds: -10));
- }
- void _seekForward() {
- _seekRelative(const Duration(seconds: 10));
- }
- void _startHideTimer() {
- final hideControlsTimer =
- chewieController.hideControlsTimer.isNegative
- ? ChewieController.defaultHideControlsTimer
- : chewieController.hideControlsTimer;
- _hideTimer = Timer(hideControlsTimer, () {
- setState(() {
- notifier.hideStuff = true;
- });
- });
- }
- void _bufferingTimerTimeout() {
- _displayBufferingIndicator = true;
- if (mounted) {
- setState(() {});
- }
- }
- void _updateState() {
- if (!mounted) return;
- final bool buffering = getIsBuffering(controller);
- // display the progress bar indicator only after the buffering delay if it has been set
- if (chewieController.progressIndicatorDelay != null) {
- if (buffering) {
- _bufferingDisplayTimer ??= Timer(
- chewieController.progressIndicatorDelay!,
- _bufferingTimerTimeout,
- );
- } else {
- _bufferingDisplayTimer?.cancel();
- _bufferingDisplayTimer = null;
- _displayBufferingIndicator = false;
- }
- } else {
- _displayBufferingIndicator = buffering;
- }
- setState(() {
- _latestValue = controller.value;
- _subtitlesPosition = controller.value.position;
- });
- }
- Widget _buildProgressBar() {
- return Expanded(
- child: MaterialVideoProgressBar(
- controller,
- onDragStart: () {
- setState(() {
- _dragging = true;
- });
- _hideTimer?.cancel();
- },
- onDragUpdate: () {
- _hideTimer?.cancel();
- },
- onDragEnd: () {
- setState(() {
- _dragging = false;
- });
- _startHideTimer();
- },
- colors:
- chewieController.materialProgressColors ??
- ChewieProgressColors(
- playedColor: Theme.of(context).colorScheme.secondary,
- handleColor: Theme.of(context).colorScheme.secondary,
- bufferedColor: Theme.of(
- context,
- ).colorScheme.surface.withOpacityCompat(0.5),
- backgroundColor: Theme.of(
- context,
- ).disabledColor.withOpacityCompat(.5),
- ),
- draggableProgressBar: chewieController.draggableProgressBar,
- ),
- );
- }
- }
|