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 createState() { return _CustomMaterialControlsState(); } } class _CustomMaterialControlsState extends State 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(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: [ 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 _buildOptions(BuildContext context) { final options = [ 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( 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: [ 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 _onSpeedButtonTap() async { _hideTimer?.cancel(); final chosenSpeed = await showModalBottomSheet( 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: [ 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 _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, ), ); } }