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)), ], ); } }
关键说明
- 滚动控制器与状态管理:通过
ScrollController监听滚动位置,_activeIndex记录当前激活的导航项,触发UI更新。 - 全局key定位:
GlobalKey用于获取目标容器的渲染位置,实现点击导航时的精准滚动。 - 滚动区域判断:在
_onScroll中计算每个容器的顶部偏移,对比当前滚动位置,确定激活的导航项。 - 颜色切换逻辑:导航项的文字和圆点颜色根据
_activeIndex动态切换,激活项显示红色,未激活项显示橙色。
内容的提问来源于stack exchange,提问作者farhan khan
相关产品推荐
相关产品推荐

