如何在Flutter中实现高度动态的AppBar以适配PDF搜索交互场景?

你原有的枚举驱动状态的逻辑基础是没问题的,觉得效果不稳定主要是没有过渡动画、组件硬切容易出现布局抖动,再加上逻辑和页面耦合太高,后续迭代容易出问题。优化可以从以下几个方向落地:
- 用
AnimatedSwitcher包裹AppBar的各模块,切换状态时自带淡入淡出过渡效果,消除生硬跳变的观感 - 补充
TextEditingController管理搜索框内容,切换状态时可以自主控制保留/清空输入内容,避免状态异常 - 把动态AppBar单独抽成独立组件,和页面业务逻辑解耦,后续维护更方便
优化后完整代码
第一步:封装独立的动态AppBar组件
enum PdfAppBarStatus { initial, search, searchComplete } class DynamicPdfAppBar extends StatefulWidget implements PreferredSizeWidget { const DynamicPdfAppBar({ super.key, required this.status, required this.onSearchTriggered, required this.onCloseSearch, required this.onPrevResult, required this.onNextResult, }); final PdfAppBarStatus status; final Function(String keyword) onSearchTriggered; final VoidCallback onCloseSearch; final VoidCallback onPrevResult; final VoidCallback onNextResult; @override State<DynamicPdfAppBar> createState() => _DynamicPdfAppBarState(); // 继承PreferredSizeWidget必须实现,使用AppBar默认高度即可 @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); } class _DynamicPdfAppBarState extends State<DynamicPdfAppBar> { final TextEditingController _searchController = TextEditingController(); @override Widget build(BuildContext context) { return AppBar( leading: AnimatedSwitcher( duration: const Duration(milliseconds: 200), transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child), child: _buildLeading(), ), title: AnimatedSwitcher( duration: const Duration(milliseconds: 200), transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child), child: _buildTitle(), ), centerTitle: widget.status == PdfAppBarStatus.initial, actions: [ AnimatedSwitcher( duration: const Duration(milliseconds: 200), transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child), child: Row( key: ValueKey(widget.status), children: _buildActions(), ), ) ], ); } Widget _buildLeading() { if (widget.status == PdfAppBarStatus.initial) { return const SizedBox.shrink(); } return IconButton( key: const ValueKey('close_btn'), onPressed: () { _searchController.clear(); widget.onCloseSearch(); }, icon: const Icon(Icons.close_outlined), ); } Widget _buildTitle() { if (widget.status == PdfAppBarStatus.initial) { return const Text('Article PDF', key: ValueKey('title_text')); } return TextField( key: const ValueKey('search_input'), controller: _searchController, autofocus: true, decoration: const InputDecoration(hintText: '输入搜索关键词'), onSubmitted: (text) { if (text.trim().isNotEmpty) { widget.onSearchTriggered(text.trim()); } }, ); } List<Widget> _buildActions() { if (widget.status == PdfAppBarStatus.searchComplete) { return [ IconButton( onPressed: widget.onPrevResult, icon: const Icon(Icons.chevron_left_outlined), ), IconButton( onPressed: widget.onNextResult, icon: const Icon(Icons.navigate_next_outlined), ), ]; } return [ IconButton( key: const ValueKey('search_btn'), onPressed: () { widget.onSearchTriggered(''); }, icon: const Icon(Icons.search_outlined), ), ]; } @override void dispose() { _searchController.dispose(); super.dispose(); } }
第二步:页面内直接调用封装好的组件
class ViewPdfScreen extends StatefulWidget { const ViewPdfScreen({Key? key}) : super(key: key); @override State<ViewPdfScreen> createState() => _ViewPdfScreenState(); } class _ViewPdfScreenState extends State<ViewPdfScreen> { PdfAppBarStatus _currentStatus = PdfAppBarStatus.initial; @override Widget build(BuildContext context) { return SafeArea( child: Scaffold( appBar: DynamicPdfAppBar( status: _currentStatus, onSearchTriggered: (keyword) { if (keyword.isEmpty) { // 点击搜索图标进入搜索状态 setState(() => _currentStatus = PdfAppBarStatus.search); return; } // 提交搜索进入搜索完成状态 print('搜索关键词:$keyword'); setState(() => _currentStatus = PdfAppBarStatus.searchComplete); }, onCloseSearch: () { setState(() => _currentStatus = PdfAppBarStatus.initial); }, onPrevResult: () => print('上一个搜索结果'), onNextResult: () => print('下一个搜索结果'), ), body: const Center(child: Text('Test')), ), ); } }
优化优势
- 状态切换时有200ms的淡入淡出动画,不会出现生硬跳变,观感更稳定
- 搜索框内容由Controller统一管理,关闭搜索时自动清空,重新进入时为空,符合交互预期
- AppBar逻辑完全独立,后续如果要改样式、加功能只需要修改
DynamicPdfAppBar即可,不会影响页面其他逻辑 - 所有切换组件都加了
ValueKey,AnimatedSwitcher可以准确识别组件差异,不会出现重建异常
内容的提问来源于stack exchange,提问作者Luke
相关产品推荐
相关产品推荐

