You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 21:00:58