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

如何实现Flutter自定义底部导航栏滚动显隐(仿X/Twitter)

实现X(原Twitter)风格的滚动触发底部导航栏

当前问题

现有自定义底部导航栏CustomBottomNavbar无法实现目标效果,当前表现:

  • 应用启动后自动执行弹出动画
  • 向上或向下滚动都会隐藏导航栏
  • 隐藏后约3秒自动弹出

期望效果

导航栏默认保持隐藏,仅在用户向上滚动时带动画弹出;向下滚动时立即隐藏,完全匹配X(原Twitter)的底部导航栏行为。

解决方案

通过添加滚动监听、区分滚动方向,并调整状态控制逻辑来实现:

修改要点

  1. 初始状态设为隐藏,移除启动时自动显示的延迟逻辑
  2. 为父级滚动组件添加滚动监听,记录滚动位置变化判断方向
  3. 仅在向上滚动时设置导航栏可见,向下滚动时设置隐藏
  4. 优化动画参数,确保过渡流畅自然

修改后的完整代码

import 'package:flutter/material.dart';

class CustomBottomNavbar extends StatefulWidget {
  final int currentIndex;
  final void Function(int index) onTap;
  // 添加滚动控制器参数,用于监听滚动事件
  final ScrollController scrollController;

  const CustomBottomNavbar({
    Key? key,
    required this.currentIndex,
    required this.onTap,
    required this.scrollController,
  }) : super(key: key);

  @override
  _CustomBottomNavbarState createState() => _CustomBottomNavbarState();
}

class _CustomBottomNavbarState extends State<CustomBottomNavbar> {
  bool _isVisible = false;
  int _tappedIndex = -1;
  double _previousScrollOffset = 0;

  @override
  void initState() {
    super.initState();
    // 初始隐藏导航栏
    _isVisible = false;
    // 添加滚动监听
    widget.scrollController.addListener(_handleScroll);
  }

  @override
  void dispose() {
    // 移除监听,避免内存泄漏
    widget.scrollController.removeListener(_handleScroll);
    super.dispose();
  }

  void _handleScroll() {
    final currentOffset = widget.scrollController.offset;
    // 判断滚动方向:向上滚动(currentOffset < _previousScrollOffset)
    // 同时排除滚动到顶部的情况
    if (currentOffset < _previousScrollOffset && currentOffset > 0) {
      // 向上滚动,显示导航栏
      if (!_isVisible) {
        setState(() {
          _isVisible = true;
        });
      }
    } else if (currentOffset > _previousScrollOffset) {
      // 向下滚动,隐藏导航栏
      if (_isVisible) {
        setState(() {
          _isVisible = false;
        });
      }
    }
    // 更新上一次滚动位置
    _previousScrollOffset = currentOffset;
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedPositioned(
      duration: const Duration(milliseconds: 300),
      curve: Curves.easeOut,
      bottom: _isVisible ? 0 : -80, // 调整隐藏距离为导航栏实际高度,避免过度偏移
      left: 0,
      right: 0,
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 48.0),
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 8.0),
          width: double.infinity,
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(50),
            color: Colors.grey[200]!.withOpacity(0.95),
          ),
          height: 60.0,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              buildNavItem(Icons.home, 0),
              buildNavItem(Icons.explore, 1),
              buildNavItem(Icons.notifications, 2),
              buildNavItem(Icons.account_circle, 3),
            ],
          ),
        ),
      ),
    );
  }

  Widget buildNavItem(IconData icon, int index) {
    final isSelected = index == widget.currentIndex;
    double iconSize = _tappedIndex == index ? 40.0 : 32.0;

    Color iconColor = isSelected
        ? Colors.grey[900]!
        : (_tappedIndex == index ? Colors.black : Colors.grey[500]!);

    return GestureDetector(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          SizedBox(
            width: 60.0,
            height: 60.0,
            child: AnimatedSwitcher(
              duration: const Duration(milliseconds: 1200),
              child: Icon(
                icon,
                key: ValueKey<IconData>(icon),
                color: iconColor,
                size: iconSize,
              ),
            ),
          ),
        ],
      ),
      onTapDown: (_) {
        setState(() {
          _tappedIndex = index;
        });
      },
      onTapUp: (_) {
        setState(() {
          _tappedIndex = -1;
        });
        widget.onTap(index);
      },
      onTapCancel: () {
        setState(() {
          _tappedIndex = -1;
        });
      },
    );
  }
}

使用说明

在使用该导航栏的页面中,需要创建一个ScrollController并传递给导航栏,同时将其绑定到页面的滚动组件(如ListView、SingleChildScrollView等):

class HomePage extends StatelessWidget {
  final ScrollController _scrollController = ScrollController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        controller: _scrollController,
        // 页面内容
      ),
      bottomNavigationBar: CustomBottomNavbar(
        currentIndex: 0,
        onTap: (index) {
          // 导航切换逻辑
        },
        scrollController: _scrollController,
      ),
    );
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 06:25:57