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

Flutter实现SingleChildScrollView点击跳转与滚动高亮需求

Flutter 导航滚动联动实现方案

要实现点击导航滚动到对应容器、滚动时导航自动高亮的效果,需要对原代码做以下核心修改:

  • 将组件改为StatefulWidget,管理当前激活的导航项状态
  • 使用ScrollController监听滚动位置,判断当前视图区域
  • 通过GlobalKey定位目标容器,实现精准滚动跳转

修改后的完整代码

import 'package:flutter/material.dart';

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

  @override
  State<ExperienceCustomDrawer> createState() => _ExperienceCustomDrawerState();
}

class _ExperienceCustomDrawerState extends State<ExperienceCustomDrawer> {
  // 滚动控制器,用于监听滚动和控制滚动位置
  final ScrollController _scrollController = ScrollController();
  // 当前激活的导航项索引:0=Details,1=Menus
  int _activeIndex = 0;
  // 两个目标容器的全局key,用于获取它们的位置信息
  final GlobalKey _detailsKey = GlobalKey();
  final GlobalKey _menusKey = GlobalKey();

  @override
  void initState() {
    super.initState();
    // 添加滚动监听
    _scrollController.addListener(_onScroll);
  }

  @override
  void dispose() {
    // 销毁时释放控制器
    _scrollController.dispose();
    super.dispose();
  }

  // 滚动监听回调,判断当前激活的导航项
  void _onScroll() {
    final scrollPosition = _scrollController.offset;
    
    // 获取Details容器的位置信息
    final detailsRenderBox = _detailsKey.currentContext?.findRenderObject() as RenderBox?;
    final detailsOffset = detailsRenderBox?.localToGlobal(Offset.zero).dy ?? 0;
    
    // 获取Menus容器的位置信息
    final menusRenderBox = _menusKey.currentContext?.findRenderObject() as RenderBox?;
    final menusOffset = menusRenderBox?.localToGlobal(Offset.zero).dy ?? 0;
    
    // 判断当前滚动位置属于哪个容器区域,更新激活索引
    setState(() {
      if (scrollPosition >= menusOffset - kToolbarHeight) {
        _activeIndex = 1;
      } else if (scrollPosition >= detailsOffset - kToolbarHeight) {
        _activeIndex = 0;
      }
    });
  }

  // 点击导航项时滚动到对应容器
  void _scrollToContainer(GlobalKey key) {
    final context = key.currentContext;
    if (context != null) {
      Scrollable.ensureVisible(
        context,
        duration: const Duration(milliseconds: 300), // 滚动动画时长
        curve: Curves.easeInOut, // 动画曲线
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Drawer(
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.only(
          topRight: Radius.circular(20),
          bottomRight: Radius.circular(20),
        ),
      ),
      width: MediaQuery.of(context).size.width * 0.6,
      child: Container(
        color: Constants.black,
        child: SingleChildScrollView(
          controller: _scrollController, // 绑定滚动控制器
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Padding(
                padding: const EdgeInsets.fromLTRB(0, 15, 30, 15),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.end,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    InkWell(
                      onTap: () {
                        Navigator.of(context).pop();
                      },
                      child: const Icon(Icons.close, color: Constants.orange),
                    )
                  ],
                ),
              ),
              Padding(
                padding: const EdgeInsets.fromLTRB(30, 15, 30, 15),
                child: FittedBox(
                  fit: BoxFit.scaleDown,
                  child: Text(
                    "Sea Food Experience",
                    style: TextStyle(
                      color: Constants.white,
                      fontSize: 28,
                    ),
                  ),
                ),
              ),

              // 导航栏:根据激活状态切换颜色
              Container(
                width: MediaQuery.of(context).size.width * 0.6,
                height: 70,
                color: Constants.grey,
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.start,
                  children: [
                    // Details导航项
                    InkWell(
                      onTap: () => _scrollToContainer(_detailsKey),
                      child: Padding(
                        padding: const EdgeInsets.symmetric(vertical: 15.0, horizontal: 30),
                        child: Column(
                          children: [
                            Flexible(
                              child: Text(
                                "Details",
                                style: TextStyle(
                                  color: _activeIndex == 0 ? Constants.red : Constants.orange,
                                  fontSize: 16,
                                ),
                              ),
                            ),
                            Flexible(
                              child: Padding(
                                padding: const EdgeInsets.only(top: 8.0),
                                child: CircleAvatar(
                                  radius: 3,
                                  backgroundColor: _activeIndex == 0 ? Constants.red : Constants.orange,
                                ),
                              ),
                            )
                          ],
                        ),
                      ),
                    ),

                    // Menus导航项
                    InkWell(
                      onTap: () => _scrollToContainer(_menusKey),
                      child: Padding(
                        padding: const EdgeInsets.symmetric(vertical: 15.0),
                        child: Column(
                          children: [
                            Flexible(
                              child: Text(
                                "Menus",
                                style: TextStyle(
                                  color: _activeIndex == 1 ? Constants.red : Constants.orange,
                                  fontSize: 16,
                                ),
                              ),
                            ),
                            Flexible(
                              child: Padding(
                                padding: const EdgeInsets.only(top: 8.0),
                                child: CircleAvatar(
                                  radius: 3,
                                  backgroundColor: _activeIndex == 1 ? Constants.red : Constants.orange,
                                ),
                              ),
                            )
                          ],
                        ),
                      ),
                    ),
                  ],
                ),
              ),

              // 第一个容器:绑定全局key
              Container(
                key: _detailsKey,
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    // 替换为你的Details实际内容
                    const SizedBox(height: 300),
                    Text("Details Content", style: TextStyle(color: Constants.white)),
                  ],
                ),
              ),

              // 第二个容器:绑定全局key
              Container(
                key: _menusKey,
                child: MenuCard(),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// 假设你已有的Constants类和MenuCard组件
class Constants {
  static const Color black = Colors.black;
  static const Color orange = Colors.orange;
  static const Color white = Colors.white;
  static const Color grey = Colors.grey;
  static const Color red = Colors.red; // 新增红色常量
}

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 替换为你的Menu实际内容
        const SizedBox(height: 400),
        Text("Menu Content", style: TextStyle(color: Constants.white)),
      ],
    );
  }
}

关键说明

  1. 滚动控制器与状态管理:通过ScrollController监听滚动位置,_activeIndex记录当前激活的导航项,触发UI更新。
  2. 全局key定位:GlobalKey用于获取目标容器的渲染位置,实现点击导航时的精准滚动。
  3. 滚动区域判断:在_onScroll中计算每个容器的顶部偏移,对比当前滚动位置,确定激活的导航项。
  4. 颜色切换逻辑:导航项的文字和圆点颜色根据_activeIndex动态切换,激活项显示红色,未激活项显示橙色。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 13:35:22