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

如何在Flutter中实现类似YouTube Music的可拖拽底部迷你播放器

实现类似YouTube Music的底部可拖拽迷你播放器(Flutter)

我想在Flutter中实现一个类似YouTube Music的底部迷你播放器,支持拖拽展开为全屏、折叠回迷你状态。这应该属于Bottom Sheet的应用场景,但网上大多是调用showModalBottomSheet()唤起底部面板的示例,而我需要的是应用启动时就存在、可由用户上下拖拽的组件。

我尝试用DraggableScrollableSheet写了示例代码,但底部面板固定不动,只有内部列表能滚动,没法实现拖拽展开/折叠的交互,想要达到YouTube那种拖拽效果。


我当前的代码

主文件(main.dart)

import 'package:flutter/material.dart';
import 'package:minitest/mini_sheet.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: MusicPlayer(),
    );
  }
}

class MusicPlayer extends StatefulWidget {
  @override
  _MusicPlayerState createState() => _MusicPlayerState();
}

class _MusicPlayerState extends State<MusicPlayer> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Mini Music Player'),
      ),
      body: Stack(
        children: [
          // Main content here (e.g., ListView)
          ListView.builder(
            itemCount: 100,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text('Item $index'),
              );
            },
          ),
          // DraggableScrollableSheet for the mini player at the bottom
          Align(
            alignment: Alignment.bottomCenter,
            child: DraggableMiniPlayer(),
          ),
        ],
      ),
    );
  }
}

迷你播放器文件(mini_sheet.dart)

import 'package:flutter/material.dart';

class DraggableMiniPlayer extends StatelessWidget {
  const DraggableMiniPlayer({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return DraggableScrollableSheet(
      initialChildSize: 0.1, // The collapsed size
      minChildSize: 0.1, // Minimum size when collapsed
      maxChildSize: 0.8, // Maximum size when expanded
      builder: (BuildContext context, ScrollController scrollController) {
        return Container(
          decoration: const BoxDecoration(
            color: Color.fromARGB(255, 189, 72, 72),
            borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
          ),
          child: Column(
            children: [
              // A drag handle at the top for better UX
              Container(
                height: 5,
                width: 60,
                margin: const EdgeInsets.symmetric(vertical: 10),
                decoration: BoxDecoration(
                  color: Colors.grey[300],
                  borderRadius: BorderRadius.circular(10),
                ),
              ),
              Expanded(
                child: ListView.builder(
                  controller: scrollController,
                  itemCount: 20,
                  itemBuilder: (context, index) {
                    return ListTile(
                      title: Text(
                        'Song $index',
                        style: const TextStyle(color: Colors.white),
                      ),
                    );
                  },
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

问题解决与优化代码

你的面板无法拖拽的核心原因:一是Align组件限制了DraggableScrollableSheet的布局范围,导致手势响应失效;二是迷你状态下内部ListView会优先抢占滚动事件。以下是修复后的代码:

修改后主文件(main.dart)

import 'package:flutter/material.dart';
import 'package:minitest/mini_sheet.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: MusicPlayer(),
    );
  }
}

class MusicPlayer extends StatefulWidget {
  @override
  _MusicPlayerState createState() => _MusicPlayerState();
}

class _MusicPlayerState extends State<MusicPlayer> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Mini Music Player'),
      ),
      body: Stack(
        children: [
          // 主内容区域
          ListView.builder(
            itemCount: 100,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text('Item $index'),
              );
            },
          ),
          // 移除Align,让DraggableScrollableSheet自动占据底部空间
          DraggableMiniPlayer(),
        ],
      ),
    );
  }
}

修改后迷你播放器文件(mini_sheet.dart)

import 'package:flutter/material.dart';

class DraggableMiniPlayer extends StatelessWidget {
  const DraggableMiniPlayer({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return DraggableScrollableSheet(
      initialChildSize: 0.1,
      minChildSize: 0.1,
      maxChildSize: 0.95,
      expand: false, // 关键:禁止强制填满父容器,让面板仅占用自身空间
      builder: (BuildContext context, ScrollController scrollController) {
        return Container(
          decoration: const BoxDecoration(
            color: Color.fromARGB(255, 189, 72, 72),
            borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
          ),
          child: NotificationListener<ScrollNotification>(
            // 拦截滚动事件,迷你状态下优先响应面板拖拽
            onNotification: (notification) {
              if (scrollController.position.pixels <= 0 && notification is ScrollStartNotification) {
                return true; // 阻止列表滚动,让面板接收拖拽手势
              }
              return false;
            },
            child: Column(
              children: [
                // 拖拽手柄
                Container(
                  height: 5,
                  width: 60,
                  margin: const EdgeInsets.symmetric(vertical: 10),
                  decoration: BoxDecoration(
                    color: Colors.grey[300],
                    borderRadius: BorderRadius.circular(10),
                  ),
                ),
                // 根据面板高度动态切换内容
                Expanded(
                  child: LayoutBuilder(
                    builder: (context, constraints) {
                      // 迷你状态显示简洁播放器信息
                      if (constraints.maxHeight < 150) {
                        return Center(
                          child: Text(
                            '正在播放:示例歌曲',
                            style: TextStyle(color: Colors.white, fontSize: 16),
                          ),
                        );
                      } else {
                        // 展开后显示歌曲列表
                        return ListView.builder(
                          controller: scrollController,
                          itemCount: 20,
                          itemBuilder: (context, index) {
                            return ListTile(
                              title: Text(
                                '歌曲 $index',
                                style: const TextStyle(color: Colors.white),
                              ),
                            );
                          },
                        );
                      }
                    },
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

关键修改说明

  1. 移除Align组件:DraggableScrollableSheet默认从屏幕底部布局,无需额外包裹Align,否则会限制手势响应范围。
  2. 设置expand: false:让面板仅占用自身所需空间,而非强制填满父容器,这是确保拖拽手势正常工作的核心配置。
  3. 拦截滚动事件:通过NotificationListener在迷你状态下阻止内部列表的滚动,让面板优先响应拖拽操作。
  4. 动态内容切换:根据面板高度显示不同界面,迷你状态展示简洁播放器信息,展开后显示完整歌曲列表,贴近YouTube Music的交互逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 10:53:15