Flutter VideoPlayer实现类Twitter视频播放器效果需求
问题描述
我有一个用于展示视频的Flutter VideoPlayer Widget,希望实现类似Twitter应用的视频播放器效果:
- 当视频为9:16的竖屏视频时,非全屏状态下需居中缩放以填充播放器的全部可用宽度(允许裁剪)
- 进入Fullscreen模式后,视频需完整填充屏幕且不被裁剪
当前代码已能正确显示横屏等其他比例的视频。我正在使用FlutterFlow,也可编写原生Flutter代码,以下是我的自定义Widget代码:
import "package:flutter/material.dart"; import "package:video_player/video_player.dart"; class Videoplayer extends StatefulWidget { const Videoplayer({ Key? key, this.width, this.height, this.videopath, }) : super(key: key); final double? width; final double? height; final String? videopath; @override _VideoplayerState createState() => _VideoplayerState(); } class _VideoplayerState extends State<Videoplayer> { late VideoPlayerController _controller; bool _isFullScreen = false; @override void initState() { super.initState(); _controller = VideoPlayerController.asset(widget.videopath!) ..initialize().then((_) { setState(() { // Ensure that the video starts playing when it's initialized _controller.play(); }); }); // Enable video controls _controller.setVolume(1.0); // Set volume to maximum _controller.setLooping(true); // Enable looping _controller.setPlaybackSpeed(1.0); // Set playback speed to normal } @override void dispose() { super.dispose(); _controller.dispose(); } void _toggleFullScreen() { setState(() { _isFullScreen = !_isFullScreen; }); if (_isFullScreen) { _controller.pause(); // Pause video when entering fullscreen } else { _controller.play(); // Resume video when exiting fullscreen } } @override Widget build(BuildContext context) { return GestureDetector( onTap: _toggleFullScreen, child: _isFullScreen ? AspectRatio( aspectRatio: _controller.value.aspectRatio, child: VideoPlayer(_controller), ) : _buildVideoPreview(), ); } Widget _buildVideoPreview() { return Center( child: AspectRatio( aspectRatio: _controller.value.aspectRatio, child: Stack( alignment: Alignment.center, children: <Widget>[ VideoPlayer(_controller), VideoPlayerControls(controller: _controller), ], ), ), ); } } class VideoPlayerControls extends StatefulWidget { final VideoPlayerController controller; const VideoPlayerControls({Key? key, required this.controller}) : super(key: key); @override _VideoPlayerControlsState createState() => _VideoPlayerControlsState(); } class _VideoPlayerControlsState extends State<VideoPlayerControls> { late bool _isPlaying; @override void initState() { super.initState(); _isPlaying = widget.controller.value.isPlaying; widget.controller.addListener(_listener); } void _listener() { if (!mounted) return; setState(() { _isPlaying = widget.controller.value.isPlaying; }); } @override Widget build(BuildContext context) { return Stack( children: <Widget>[ AnimatedSwitcher( duration: Duration(milliseconds: 300), reverseDuration: Duration(milliseconds: 300), child: _isPlaying ? SizedBox.shrink() : Container( color: Colors.black.withOpacity(0.6), child: Center( child: IconButton( icon: Icon(Icons.play_arrow, color: Colors.white), onPressed: () { widget.controller.play(); }, ), ), ), ), GestureDetector( onTap: () { setState(() { if (_isPlaying) { widget.controller.pause(); } else { widget.controller.play(); } }); }, ), ], ); } @override void dispose() { widget.controller.removeListener(_listener); super.dispose(); } }

解决方案
要实现需求,核心是根据视频比例和全屏状态动态调整视频的适配逻辑,以下是修改后的完整代码:
import "package:flutter/material.dart"; import "package:video_player/video_player.dart"; class Videoplayer extends StatefulWidget { const Videoplayer({ Key? key, this.width, this.height, this.videopath, }) : super(key: key); final double? width; final double? height; final String? videopath; @override _VideoplayerState createState() => _VideoplayerState(); } class _VideoplayerState extends State<Videoplayer> { late VideoPlayerController _controller; bool _isFullScreen = false; // 定义9:16竖屏比例的判断阈值 static const double _nineSixteenRatio = 9 / 16; @override void initState() { super.initState(); _controller = VideoPlayerController.asset(widget.videopath!) ..initialize().then((_) { setState(() { _controller.play(); }); }); _controller.setVolume(1.0); _controller.setLooping(true); _controller.setPlaybackSpeed(1.0); } @override void dispose() { _controller.dispose(); super.dispose(); } void _toggleFullScreen() { setState(() { _isFullScreen = !_isFullScreen; }); // 全屏切换时保持播放(可根据需求调整为暂停/继续) _isFullScreen ? _controller.play() : _controller.play(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: _toggleFullScreen, child: _isFullScreen ? _buildFullScreenVideo() : _buildVideoPreview(), ); } Widget _buildVideoPreview() { final videoRatio = _controller.value.aspectRatio; final containerWidth = widget.width ?? MediaQuery.of(context).size.width; return Container( width: containerWidth, height: widget.height, child: Center( child: videoRatio == _nineSixteenRatio ? // 9:16竖屏视频:填充宽度,居中裁剪 FittedBox( fit: BoxFit.cover, alignment: Alignment.center, child: SizedBox( width: containerWidth, height: containerWidth / videoRatio, child: Stack( alignment: Alignment.center, children: [ VideoPlayer(_controller), VideoPlayerControls(controller: _controller), ], ), ), ) : // 其他比例视频:保持原比例显示 AspectRatio( aspectRatio: videoRatio, child: Stack( alignment: Alignment.center, children: [ VideoPlayer(_controller), VideoPlayerControls(controller: _controller), ], ), ), ), ); } Widget _buildFullScreenVideo() { return Scaffold( backgroundColor: Colors.black, body: OrientationBuilder( builder: (context, orientation) { return Center( child: FittedBox( fit: BoxFit.contain, alignment: Alignment.center, child: SizedBox( width: orientation == Orientation.portrait ? MediaQuery.of(context).size.width : MediaQuery.of(context).size.height * _controller.value.aspectRatio, height: orientation == Orientation.portrait ? MediaQuery.of(context).size.width / _controller.value.aspectRatio : MediaQuery.of(context).size.height, child: Stack( alignment: Alignment.center, children: [ VideoPlayer(_controller), VideoPlayerControls(controller: _controller), ], ), ), ), ); }, ), ); } } class VideoPlayerControls extends StatefulWidget { final VideoPlayerController controller; const VideoPlayerControls({Key? key, required this.controller}) : super(key: key); @override _VideoPlayerControlsState createState() => _VideoPlayerControlsState(); } class _VideoPlayerControlsState extends State<VideoPlayerControls> { late bool _isPlaying; @override void initState() { super.initState(); _isPlaying = widget.controller.value.isPlaying; widget.controller.addListener(_listener); } void _listener() { if (!mounted) return; setState(() { _isPlaying = widget.controller.value.isPlaying; }); } @override Widget build(BuildContext context) { return Stack( children: <Widget>[ AnimatedSwitcher( duration: const Duration(milliseconds: 300), reverseDuration: const Duration(milliseconds: 300), child: _isPlaying ? const SizedBox.shrink() : Container( color: Colors.black.withOpacity(0.6), child: Center( child: IconButton( icon: const Icon(Icons.play_arrow, color: Colors.white), onPressed: () { widget.controller.play(); }, ), ), ), ), GestureDetector( onTap: () { setState(() { _isPlaying ? widget.controller.pause() : widget.controller.play(); }); }, ), ], ); } @override void dispose() { widget.controller.removeListener(_listener); super.dispose(); } }
关键修改说明
- 非全屏竖屏适配:通过判断视频比例是否为9:16,使用
FittedBox的BoxFit.cover模式让视频填充容器宽度,超出部分居中裁剪,匹配Twitter的展示效果。 - 全屏模式处理:用
Scaffold作为全屏容器,结合OrientationBuilder监听屏幕方向,通过BoxFit.contain确保视频完整显示,无裁剪。 - 容器约束优化:非全屏时明确指定容器宽度,保证
FittedBox能准确计算填充范围,避免布局异常。
内容的提问来源于stack exchange,提问作者Mark
相关产品推荐
相关产品推荐

