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

如何在Flutter应用栏中实现自定义Navigation Rail?

Flutter音乐应用导航栏实现问题

我是Flutter新手,正在开发一款音乐应用。已经完成首页App Bar的布局,但想要添加设计图中的导航栏,不确定在Flutter中是否可行,也没找到实现方法。

  • 设计图:导航栏为底部居中排列的图标样式,带有选中状态的高亮效果
  • 首页截图:已完成透明背景的大尺寸AppBar,背景为全屏图片

首页代码

import 'dart:convert';
import 'package:aurora_music_v01/screens/login_screen.dart';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:http/http.dart' as http;
import 'package:version/version.dart';
import 'package:url_launcher/url_launcher.dart';

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

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  late bool isDarkMode;
  bool isWelcomeBackVisible = true;
  bool isAuroraMusicVisible = false;
  User? user;
  Version? latestVersion;

  @override
  void initState() {
    super.initState();
    user = FirebaseAuth.instance.currentUser;

    Future.delayed(const Duration(milliseconds: 1500), () {
      setState(() {
        isWelcomeBackVisible = false;
      });
    });

    Future.delayed(const Duration(milliseconds: 2500), () {
      setState(() {
        isAuroraMusicVisible = true;
      });
    });
    
    checkForNewVersion();
  }

  Future<void> checkForNewVersion() async {
    try {
      final response = await http.get(Uri.parse('api_github_link'));
      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        final versionString = data['tag_name'];
        final regex = RegExp(r'^v?(\d+\.\d+\.\d+(-\S+)?)$');
        final match = regex.firstMatch(versionString);
        if (match!= null && match.groupCount > 0) {
          final versionString = match.group(1)!;
          setState(() {
            latestVersion = Version.parse(versionString);
          });
        }
      }
    } catch (e) {
      print('Error fetching latest version: $e');
    }
  }

  void launchURL(String url) async {
    if (await canLaunch(url)) {
      try {
        await launch(url);
      } catch (e) {
        print('Error launching URL: $e');
      }
    } else {
      throw 'Could not launch $url';
    }
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    isDarkMode = MediaQuery.of(context).platformBrightness == Brightness.dark;
  }

  @override
  Widget build(BuildContext context) {
    final regex = RegExp(r'^v?(\d+\.\d+\.\d+)(-[a-zA-Z]+)?$');
    final match = regex.firstMatch('v0.0.5-alpha');
    final currentVersion = Version.parse(match!.group(1)!);
    final isUpdateAvailable = latestVersion!= null && latestVersion!.compareTo(currentVersion) > 0;

    if (isUpdateAvailable) {
      Future.delayed(Duration.zero, () {
        showDialog(
          context: context,
          builder: (context) {
            return AlertDialog(
              title: const Text('New version available'),
              content: const Text('A new version of Aurora Music is available. Would you like to download it now?'),
              actions: [
                TextButton(
                  onPressed: () {
                    Navigator.pop(context);
                  },
                  child: const Text('Cancel'),
                ),
                TextButton(
                  onPressed: () async {
                    Navigator.pop(context);
                    await launch('github_link');
                  },
                  child: const Text('Download'),
                ),
              ],
            );
          },
        );
      });
    }

    return user == null
       ? LoginPage()
        : Stack(
            children: [
              Container(
                width: double.infinity,
                decoration: BoxDecoration(
                  color: Colors.transparent,
                  image: DecorationImage(
                    image: AssetImage(isDarkMode
                       ? 'assets/images/background/dark_back.jpg'
                        : 'assets/images/background/light_back.jpg'),
                    fit: BoxFit.cover,
                  ),
                ),
              ),
              AppBar(
                backgroundColor: Colors.transparent,
                elevation: 0.0,
                toolbarHeight: 230,
                automaticallyImplyLeading: false,
                title: Center(
                  child: Stack(
                    children: [
                      AnimatedOpacity(
                        opacity: isWelcomeBackVisible? 1.0 : 0.0,
                        duration: const Duration(seconds: 1),
                        child: const Text(
                          ' Welcome Back',
                          style: TextStyle(
                              fontFamily: 'Outfit',
                              fontStyle: FontStyle.normal,
                              color: Colors.white,
                              fontSize: 34,
                              fontWeight: FontWeight.normal),
                        ),
                      ),
                      AnimatedOpacity(
                        opacity: isAuroraMusicVisible? 1.0 : 0.0,
                        duration: const Duration(seconds: 1),
                        child: const Text(
                          '   Aurora Music',
                          style: TextStyle(
                              fontFamily: 'Outfit',
                              fontStyle: FontStyle.normal,
                              color: Colors.white,
                              fontSize: 34,
                              fontWeight: FontWeight.normal),
                        ),
                      ),
                    ],
                  ),
                ),
             ),
              const SizedBox(
                height: 50,
              ),
              const Center(
                child: Text(
                  'App is under construction. Come back later!',
                  style: TextStyle(
                      fontFamily: 'Outfit',
                      fontStyle: FontStyle.normal,
                      color: Colors.white,
                      fontSize: 18,
                      fontWeight: FontWeight.normal),
                ),
              ),
              Align(
                alignment: Alignment.bottomCenter,
                child: ElevatedButton(
                  onPressed: () async {
                    await FirebaseAuth.instance.signOut();
                    Navigator.pushReplacement(
                      context,
                      MaterialPageRoute(builder: (context) => LoginPage()),
                    );
                  },
                  child: Text(
                    'Log out',
                    style: TextStyle(
                      fontFamily: 'Outfit',
                      fontStyle: FontStyle.normal,
                      color: Color.fromARGB(255, 123, 14, 213),
                      fontSize: 18,
                      fontWeight: FontWeight.normal,
                    ),
                  ),
                ),
              ),
            ],
          );
  }
}
解决方案

当然可行,你可以通过自定义底部组件或使用Material3的NavigationBar来实现设计图中的样式,以下是具体实现方案:

1. 实现思路

设计图的导航栏是底部居中排列的图标,带选中高亮效果,核心是:

  • 维护选中索引状态,切换时更新图标样式
  • 使用透明背景的容器包裹图标,适配首页的图片背景
  • 可选择自定义布局或用官方组件快速实现

2. 代码实现(自定义布局版)

在_HomeScreenState中添加选中状态:

int _selectedNavIndex = 0;

然后在Stack的children末尾添加导航栏组件(可替换原退出按钮位置或调整布局):

Align(
  alignment: Alignment.bottomCenter,
  child: Padding(
    padding: const EdgeInsets.only(bottom: 20),
    child: Container(
      padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 12),
      decoration: BoxDecoration(
        color: isDarkMode? Colors.black54 : Colors.white54,
        borderRadius: BorderRadius.circular(30),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          _buildNavItem(Icons.home, 0),
          _buildNavItem(Icons.search, 1),
          _buildNavItem(Icons.library_music, 2),
          _buildNavItem(Icons.person, 3),
        ],
      ),
    ),
  ),
),

添加_buildNavItem方法构建单个导航图标:

Widget _buildNavItem(IconData icon, int index) {
  return GestureDetector(
    onTap: () {
      setState(() {
        _selectedNavIndex = index;
        // 这里可添加页面切换逻辑,比如使用PageView或Navigator跳转
      });
    },
    child: Icon(
      icon,
      color: _selectedNavIndex == index
         ? const Color.fromARGB(255, 123, 14, 213) // 选中时的紫色
          : isDarkMode? Colors.white70 : Colors.grey[700],
      size: 32,
    ),
  );
}

3. Material3官方组件版(更简洁)

如果你使用Material3,可直接用NavigationBar实现,自带动画效果:

Align(
  alignment: Alignment.bottomCenter,
  child: Padding(
    padding: const EdgeInsets.only(bottom: 20, left: 40, right: 40),
    child: NavigationBar(
      backgroundColor: isDarkMode? Colors.black54 : Colors.white54,
      elevation: 0,
      labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
      selectedIndex: _selectedNavIndex,
      onDestinationSelected: (index) {
        setState(() {
          _selectedNavIndex = index;
        });
      },
      destinations: const [
        NavigationDestination(icon: Icon(Icons.home), label: ''),
        NavigationDestination(icon: Icon(Icons.search), label: ''),
        NavigationDestination(icon: Icon(Icons.library_music), label: ''),
        NavigationDestination(icon: Icon(Icons.person), label: ''),
      ],
    ),
  ),
),

4. 调整建议

  • 根据设计图调整容器的圆角、背景透明度、图标间距等参数
  • 若需要页面切换,可结合PageView实现左右滑动,或用Navigator跳转到对应页面
  • 原有的退出按钮可以整合到导航栏的个人中心图标逻辑里,或调整位置避免重叠

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 17:30:54