在这里插入图片描述

主框架是整个App的骨架,它决定了App的基本结构和导航方式。音乐播放器通常采用底部导航栏的设计,用户可以在首页、发现、音乐库、我的四个主要模块之间切换。同时,底部还需要一个迷你播放器,让用户在浏览其他页面时也能控制音乐播放。本篇我们来实现这个主框架页面。

功能分析

主框架页面需要实现以下功能:底部导航栏包含4个Tab、点击Tab切换对应页面、页面切换时保持状态不丢失、底部导航栏上方悬浮迷你播放器、迷你播放器显示当前播放歌曲信息、播放控制按钮。

核心技术点

本篇涉及的核心技术包括:IndexedStack实现Tab页面切换并保持状态、BottomNavigationBar底部导航栏、Stack和Positioned实现悬浮迷你播放器、GetX响应式状态管理。

对应代码文件

lib/pages/main_page.dart

完整代码实现

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'home/home_page.dart';
import 'discover/discover_page.dart';
import 'music/my_music_page.dart';
import 'profile/profile_page.dart';
import 'player/player_page.dart';

class MainPage extends StatefulWidget {
  const MainPage({super.key});

  
  State<MainPage> createState() => _MainPageState();
}

上面这段代码导入了必要的依赖包和子页面。MainPage使用StatefulWidget是因为需要管理Tab索引和播放状态。GetX用于响应式状态管理和路由导航。

class _MainPageState extends State<MainPage> {
  int _currentIndex = 0;
  final RxBool _isPlaying = false.obs;
  final RxString _currentSong = '未播放'.obs;
  final RxString _currentArtist = ''.obs;

  final List<Widget> _pages = [
    const HomePage(),
    const DiscoverPage(),
    const MyMusicPage(),
    const ProfilePage(),
  ];

  final List<BottomNavigationBarItem> _navItems = const [
    BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
    BottomNavigationBarItem(icon: Icon(Icons.explore), label: '发现'),
    BottomNavigationBarItem(icon: Icon(Icons.library_music), label: '音乐库'),
    BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
  ];

_currentIndex记录当前选中的Tab索引。RxBool和RxString是GetX的响应式变量,.obs后缀将普通变量转换为可观察对象。_pages列表存放四个子页面实例,_navItems定义底部导航栏的图标和文字。

  
  void initState() {
    super.initState();
    _currentSong.value = '当前播放歌曲';
    _currentArtist.value = '歌手名称';
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          IndexedStack(index: _currentIndex, children: _pages),
          Positioned(
            left: 0,
            right: 0,
            bottom: 0,
            child: _buildMiniPlayer(context),
          ),
        ],
      ),
      bottomNavigationBar: _buildBottomNavBar(),
    );
  }

initState中初始化歌曲信息。build方法返回Scaffold脚手架,body使用Stack叠加IndexedStack和迷你播放器。IndexedStack会同时持有所有子页面但只显示当前索引对应的页面,这样切换Tab时页面状态不会丢失。

  Widget _buildBottomNavBar() {
    return BottomNavigationBar(
      currentIndex: _currentIndex,
      onTap: (index) => setState(() => _currentIndex = index),
      type: BottomNavigationBarType.fixed,
      items: _navItems,
    );
  }

  Widget _buildMiniPlayer(BuildContext context) {
    return GestureDetector(
      onTap: () => Get.to(() => const PlayerPage()),
      onVerticalDragEnd: (details) {
        if (details.primaryVelocity! < 0) {
          Get.to(() => const PlayerPage());
        }
      },

BottomNavigationBar的currentIndex指定当前选中项,onTap处理点击切换Tab。type设为fixed让所有Tab等宽显示。迷你播放器用GestureDetector包裹,支持点击和上滑手势打开全屏播放器,primaryVelocity为负表示向上滑动。

      child: Container(
        margin: const EdgeInsets.fromLTRB(8, 0, 8, 60),
        padding: const EdgeInsets.all(8),
        decoration: BoxDecoration(
          color: Theme.of(context).cardColor,
          borderRadius: BorderRadius.circular(12),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.2),
              blurRadius: 10,
              offset: const Offset(0, -2),
            ),
          ],
        ),
        child: Row(
          children: [
            _buildAlbumCover(),
            const SizedBox(width: 12),
            _buildSongInfo(),
            _buildControlButtons(),
          ],
        ),
      ),
    );
  }

Container的margin底部设为60为导航栏留空间。decoration添加卡片背景色、圆角和阴影让迷你播放器悬浮显示。Row水平排列专辑封面、歌曲信息和控制按钮三个区域。

  Widget _buildAlbumCover() {
    return Container(
      width: 48,
      height: 48,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(8),
        gradient: const LinearGradient(
          colors: [Color(0xFFE91E63), Color(0xFF9C27B0)],
        ),
      ),
      child: const Icon(Icons.music_note, color: Colors.white),
    );
  }

  Widget _buildSongInfo() {
    return Expanded(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisSize: MainAxisSize.min,
        children: [
          Obx(() => Text(
            _currentSong.value,
            style: const TextStyle(fontWeight: FontWeight.w500),
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
          )),

专辑封面使用48x48尺寸,渐变背景与品牌色保持一致。歌曲信息区域使用Expanded占据剩余空间,Obx包裹Text组件监听响应式变量变化自动更新UI,maxLines和overflow确保文字过长时显示省略号。

          const SizedBox(height: 2),
          Obx(() => Text(
            _currentArtist.value,
            style: const TextStyle(color: Colors.grey, fontSize: 12),
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
          )),
        ],
      ),
    );
  }

  Widget _buildControlButtons() {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        IconButton(
          icon: const Icon(Icons.skip_previous),
          onPressed: _playPrevious,
          iconSize: 28,
        ),

歌手名使用灰色小字显示在歌曲名下方。控制按钮区域使用Row水平排列,mainAxisSize设为min让Row只占必要宽度。上一首按钮使用skip_previous图标,iconSize设为28。

        Obx(() => IconButton(
          icon: Icon(
            _isPlaying.value ? Icons.pause_circle_filled : Icons.play_circle_filled,
            size: 40,
            color: const Color(0xFFE91E63),
          ),
          onPressed: _togglePlay,
        )),
        IconButton(
          icon: const Icon(Icons.skip_next),
          onPressed: _playNext,
          iconSize: 28,
        ),
      ],
    );
  }

播放按钮使用Obx监听_isPlaying状态,根据播放状态切换暂停或播放图标。播放按钮使用较大尺寸40和粉色突出显示,是整个控制区的视觉焦点。下一首按钮与上一首按钮样式一致。

  void _togglePlay() {
    _isPlaying.toggle();
  }

  void _playPrevious() {
    Get.snackbar(
      '提示',
      '播放上一首',
      snackPosition: SnackPosition.TOP,
      duration: const Duration(seconds: 1),
    );
  }

  void _playNext() {
    Get.snackbar(
      '提示',
      '播放下一首',
      snackPosition: SnackPosition.TOP,
      duration: const Duration(seconds: 1),
    );
  }
}

_togglePlay使用GetX的toggle方法切换播放状态。_playPrevious和_playNext目前只显示提示,实际项目中需要调用播放器服务切换歌曲。Get.snackbar是GetX提供的便捷提示方法。

IndexedStack与PageView对比

IndexedStack会同时构建所有子页面,但只显示当前选中的那个。优点是切换Tab时页面状态不丢失,缺点是首次加载时会构建所有页面。PageView支持滑动切换,但默认只保留当前页面和相邻页面的状态。如果需要保持所有页面状态,需要配合AutomaticKeepAliveClientMixin使用。对于底部导航这种场景,IndexedStack是更简单直接的选择。

GetX响应式变量

GetX的响应式变量通过.obs后缀创建,使用Obx组件包裹需要响应变化的Widget。当响应式变量的值发生变化时,Obx会自动重建其子Widget。这种方式比setState更加精细,只会重建真正需要更新的部分,性能更好。RxBool、RxString、RxInt等是常用的响应式类型。

迷你播放器设计要点

迷你播放器是音乐App的标配组件。设计时需要注意以下几点:位置要固定在底部导航栏上方,不能遮挡导航栏;要支持点击和上滑手势打开全屏播放器;要显示当前播放的歌曲信息;要提供基本的播放控制按钮;样式要与整体App风格协调。

进阶优化建议

在实际项目中,你可能还需要考虑以下几点:

播放状态全局管理:将播放状态提取到GetX Controller中,这样在任何页面都可以方便地访问和修改播放状态,实现真正的全局状态管理。

迷你播放器动画:可以添加进度条动画显示当前播放进度,也可以添加专辑封面旋转动画增加视觉效果,让用户直观感受到音乐正在播放。

手势优化:除了上滑打开全屏播放器,还可以支持左右滑动切换歌曲,提供更便捷的操作方式。

播放队列:点击迷你播放器右侧可以弹出播放队列,方便用户管理播放列表,查看即将播放的歌曲。

小结

本篇实现了音乐播放器的主框架页面,通过IndexedStack实现Tab页面切换并保持状态,通过BottomNavigationBar实现底部导航。迷你播放器悬浮在底部导航栏上方,显示当前播放的歌曲信息和控制按钮,支持点击和上滑手势打开全屏播放器。这个主框架为后续的功能开发奠定了基础,用户可以在四个主要模块之间自由切换,同时随时控制音乐播放。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐