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

如何在Flutter中实现类似YouTube的视频迷你播放器并保活状态?

Flutter 跨页面持久化迷你播放器实现方案

核心思路

要实现YouTube式的跨页面迷你播放器,关键是让播放器实例脱离页面路由层级,通过全局状态管理持有播放器控制器,同时将迷你播放器组件固定在全局布局的顶层,不受页面切换影响。

步骤1:全局播放器状态管理

创建单例状态管理器,统一持有播放器控制器、播放状态、迷你播放器显示状态等:

import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';

class VideoPlayerManager extends ChangeNotifier {
  VideoPlayerController? _controller;
  bool _isMiniPlayerVisible = false;
  double _progress = 0.0;
  bool _isPlaying = false;
  String? _currentVideoUrl;

  // 对外暴露状态和控制器
  VideoPlayerController? get controller => _controller;
  bool get isMiniPlayerVisible => _isMiniPlayerVisible;
  double get progress => _progress;
  bool get isPlaying => _isPlaying;
  String? get currentVideoUrl => _currentVideoUrl;

  // 初始化播放器(支持切换视频)
  void initializePlayer(String videoUrl) async {
    // 同一视频直接切换播放状态
    if (_currentVideoUrl == videoUrl && _controller != null) {
      togglePlay();
      return;
    }

    // 释放旧控制器资源
    _controller?.dispose();

    _currentVideoUrl = videoUrl;
    _controller = VideoPlayerController.networkUrl(Uri.parse(videoUrl));
    await _controller!.initialize();
    _controller!.addListener(_updateState);
    _isPlaying = true;
    _controller!.play();
    _isMiniPlayerVisible = true;
    notifyListeners();
  }

  // 播放/暂停切换
  void togglePlay() {
    if (_controller == null) return;
    _isPlaying ? _controller!.pause() : _controller!.play();
    _isPlaying = !_isPlaying;
    notifyListeners();
  }

  // 更新播放进度
  void updateProgress() {
    if (_controller == null) return;
    _progress = _controller!.value.position.inMilliseconds / _controller!.value.duration.inMilliseconds;
    notifyListeners();
  }

  // 隐藏迷你播放器
  void hideMiniPlayer() {
    _isMiniPlayerVisible = false;
    _controller?.pause();
    notifyListeners();
  }

  // 监听播放器状态变化
  void _updateState() {
    _isPlaying = _controller!.value.isPlaying;
    updateProgress();
    notifyListeners();
  }

  // 页面销毁时释放资源
  @override
  void dispose() {
    _controller?.dispose();
    super.dispose();
  }
}

步骤2:全局布局配置

将迷你播放器放在Navigator上层的Stack中,确保页面切换时播放器不被销毁:

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => VideoPlayerManager(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Stack(
          children: [
            // 主页面路由容器
            const Navigator(
              initialRoute: '/home',
              onGenerateRoute: (settings) {
                switch (settings.name) {
                  case '/home':
                    return MaterialPageRoute(builder: (_) => const HomePage());
                  case '/library':
                    return MaterialPageRoute(builder: (_) => const LibraryPage());
                  case '/subscriptions':
                    return MaterialPageRoute(builder: (_) => const SubscriptionsPage());
                  default:
                    return MaterialPageRoute(builder: (_) => const HomePage());
                }
              },
            ),
            // 迷你播放器组件(固定在底部)
            Positioned(
              bottom: 20,
              left: 20,
              right: 20,
              child: Consumer<VideoPlayerManager>(
                builder: (context, manager, child) {
                  if (!manager.isMiniPlayerVisible || manager.controller == null) {
                    return const SizedBox.shrink();
                  }
                  return GestureDetector(
                    // 点击跳转到全屏播放页面
                    onTap: () => Navigator.push(
                      context,
                      MaterialPageRoute(builder: (_) => FullScreenPlayerPage(manager: manager)),
                    ),
                    child: Container(
                      height: 80,
                      decoration: BoxDecoration(
                        color: Colors.black87,
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: Row(
                        children: [
                          // 视频预览区域
                          SizedBox(
                            width: 120,
                            child: VideoPlayer(manager.controller!),
                          ),
                          // 视频信息和进度条
                          Expanded(
                            child: Padding(
                              padding: const EdgeInsets.symmetric(horizontal: 12),
                              child: Column(
                                mainAxisAlignment: MainAxisAlignment.center,
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: [
                                  const Text(
                                    '当前播放视频',
                                    style: TextStyle(color: Colors.white, fontSize: 14),
                                    maxLines: 1,
                                    overflow: TextOverflow.ellipsis,
                                  ),
                                  const SizedBox(height: 4),
                                  LinearProgressIndicator(
                                    value: manager.progress,
                                    backgroundColor: Colors.grey[700],
                                    valueColor: const AlwaysStoppedAnimation<Color>(Colors.red),
                                  ),
                                ],
                              ),
                            ),
                          ),
                          // 播放/暂停按钮
                          IconButton(
                            icon: Icon(manager.isPlaying ? Icons.pause : Icons.play_arrow, color: Colors.white),
                            onPressed: manager.togglePlay,
                          ),
                          // 关闭按钮
                          IconButton(
                            icon: const Icon(Icons.close, color: Colors.white),
                            onPressed: manager.hideMiniPlayer,
                          ),
                        ],
                      ),
                    ),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

步骤3:页面中调用播放器

在任意页面(如首页、库、订阅页)中,通过Provider获取管理器实例,触发播放操作:

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('首页'),
        actions: [
          IconButton(icon: const Icon(Icons.video_library), onPressed: () => Navigator.pushNamed(context, '/library')),
          IconButton(icon: const Icon(Icons.subscriptions), onPressed: () => Navigator.pushNamed(context, '/subscriptions')),
        ],
      ),
      body: ListView.builder(
        itemCount: 10,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text('视频 $index'),
            subtitle: const Text('点击播放'),
            onTap: () {
              // 调用管理器初始化播放器
              Provider.of<VideoPlayerManager>(context, listen: false)
                  .initializePlayer('https://example.com/video$index.mp4');
            },
          );
        },
      ),
    );
  }
}

// 全屏播放页面示例
class FullScreenPlayerPage extends StatelessWidget {
  final VideoPlayerManager manager;

  const FullScreenPlayerPage({super.key, required this.manager});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: AspectRatio(
          aspectRatio: manager.controller!.value.aspectRatio,
          child: VideoPlayer(manager.controller!),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: manager.togglePlay,
        child: Icon(manager.isPlaying ? Icons.pause : Icons.play_arrow),
      ),
    );
  }
}

关键注意事项

  • 资源管理:播放器控制器由VideoPlayerManager统一持有和释放,避免页面销毁时重复释放资源
  • 状态同步:通过ChangeNotifier通知所有监听组件更新UI,确保迷你播放器和页面中的播放状态一致
  • 路由隔离:迷你播放器脱离页面路由层级,放在全局Stack中,不受页面push/pop影响

内容的提问来源于stack exchange,提问作者Noob Developer

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 15:41:12