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

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

效果示意图

你原有的枚举驱动状态的逻辑基础是没问题的,觉得效果不稳定主要是没有过渡动画、组件硬切容易出现布局抖动,再加上逻辑和页面耦合太高,后续迭代容易出问题。优化可以从以下几个方向落地:

  1. 用AnimatedSwitcher包裹AppBar的各模块,切换状态时自带淡入淡出过渡效果,消除生硬跳变的观感
  2. 补充TextEditingController管理搜索框内容,切换状态时可以自主控制保留/清空输入内容,避免状态异常
  3. 把动态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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 04:06:02