Flutter中如何实现类YouTube的可拖拽展开与关闭视图?
实现类似YouTube移动端的可拖拽播放器视图
核心实现思路
不用纠结AnimatedBuilder的复杂嵌套,直接用GestureDetector监听垂直拖拽,配合动画组件实现平滑过渡,同时维护全屏、最小化、关闭三个状态,根据拖拽距离触发状态切换。
完整代码示例
import 'package:flutter/material.dart'; enum PlayerViewState { fullscreen, minimized, closed } class YouTubeStylePlayer extends StatefulWidget { const YouTubeStylePlayer({super.key}); @override State<YouTubeStylePlayer> createState() => _YouTubeStylePlayerState(); } class _YouTubeStylePlayerState extends State<YouTubeStylePlayer> with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation<double> _positionAnimation; late Animation<double> _heightAnimation; PlayerViewState _currentState = PlayerViewState.fullscreen; // 拖拽阈值:超过这个距离触发状态切换 final double _dragThreshold = 150; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 200), ); // 初始化位置和高度动画 _updateAnimations(); } void _updateAnimations() { double targetPosition = 0; double targetHeight = MediaQuery.of(context).size.height; switch (_currentState) { case PlayerViewState.fullscreen: targetPosition = 0; targetHeight = MediaQuery.of(context).size.height; break; case PlayerViewState.minimized: targetPosition = MediaQuery.of(context).size.height - 120; targetHeight = 120; break; case PlayerViewState.closed: targetPosition = MediaQuery.of(context).size.height; targetHeight = 0; break; } _positionAnimation = Tween<double>(begin: _positionAnimation.value, end: targetPosition).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeOut), ); _heightAnimation = Tween<double>(begin: _heightAnimation.value, end: targetHeight).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeOut), ); _animationController.reset(); _animationController.forward(); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: Stack( children: [ // 背景主页面内容 const Center( child: Text( "主页面内容", style: TextStyle(color: Colors.white, fontSize: 20), ), ), // 可拖拽的播放器视图 AnimatedBuilder( animation: _animationController, builder: (context, child) { return Positioned( left: 0, right: 0, top: _positionAnimation.value, child: GestureDetector( onPanUpdate: (details) { // 实时更新位置,实现拖拽跟随效果 setState(() { _positionAnimation = Tween<double>( begin: _positionAnimation.value, end: _positionAnimation.value + details.delta.dy, ).animate(_animationController); }); }, onPanEnd: (details) { // 根据拖拽距离和当前状态判断目标状态 final currentPosition = _positionAnimation.value; final screenHeight = MediaQuery.of(context).size.height; if (_currentState == PlayerViewState.fullscreen) { // 全屏下拉:超过阈值则最小化,否则回弹 _currentState = currentPosition > _dragThreshold ? PlayerViewState.minimized : PlayerViewState.fullscreen; } else if (_currentState == PlayerViewState.minimized) { // 最小化状态:上拉超阈值全屏,下拉超阈值关闭,否则回弹 if (currentPosition < screenHeight - 120 - _dragThreshold) { _currentState = PlayerViewState.fullscreen; } else if (currentPosition > screenHeight - _dragThreshold) { _currentState = PlayerViewState.closed; } else { _currentState = PlayerViewState.minimized; } } else if (_currentState == PlayerViewState.closed) { // 关闭状态上拉超阈值恢复最小化 _currentState = currentPosition < screenHeight - _dragThreshold ? PlayerViewState.minimized : PlayerViewState.closed; } _updateAnimations(); }, child: Container( height: _heightAnimation.value, color: Colors.redAccent, child: const Center( child: Text( "播放器区域", style: TextStyle(color: Colors.white, fontSize: 24), ), ), ), ), ); }, ), ], ), ); } } // 测试入口 void main() { runApp(const MaterialApp(home: YouTubeStylePlayer())); }
关键细节说明
- 状态枚举:用
PlayerViewState明确三个状态,避免逻辑混乱 - 拖拽阈值:
_dragThreshold控制触发状态切换的拖拽距离,可根据需求调整 - 动画曲线:
Curves.easeOut让过渡动画更贴近YouTube的流畅感 - 实时拖拽:在
onPanUpdate中直接更新动画终点值,实现拖拽时的实时跟随效果
内容的提问来源于stack exchange,提问作者priyank apps
相关产品推荐
相关产品推荐

