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

如何在Flutter中实现标题旁的搜索按钮(非AppBar场景)

Got it, let's build that smooth expandable search effect you're after! Your initial Stack approach is perfect—let's add animations to make the search field slide in and cover the title when the magnifying glass is tapped. Here's a complete, working implementation:

Step-by-Step Implementation

First, we'll use a StatefulWidget with an AnimationController to handle the smooth transitions. We'll animate three key elements:

  • The search field's width (from 0 to full width)
  • The title's opacity (from fully visible to hidden)
  • The magnifying glass button's position (sliding left as the search field expands)

Full Code Example

import 'package:flutter/material.dart';

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

  @override
  State<ExpandableSearchHeader> createState() => _ExpandableSearchHeaderState();
}

class _ExpandableSearchHeaderState extends State<ExpandableSearchHeader>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _searchWidthAnim;
  late Animation<double> _titleOpacityAnim;
  late Animation<double> _iconPositionAnim;
  bool _isSearchActive = false;

  @override
  void initState() {
    super.initState();
    // Initialize animation controller with a smooth 300ms duration
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );

    // Animate search field width from 0 to full available space
    _searchWidthAnim = Tween<double>(begin: 0, end: double.infinity).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
    );

    // Fade out title as search field expands
    _titleOpacityAnim = Tween<double>(begin: 1, end: 0).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
    );

    // Slide icon button left to sit next to the expanded search field
    _iconPositionAnim = Tween<double>(begin: 0, end: 16).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _toggleSearch() {
    setState(() => _isSearchActive = !_isSearchActive);
    // Play or reverse the animation based on search state
    _isSearchActive ? _animationController.forward() : _animationController.reverse();
  }

  Widget _buildTitle(BuildContext context) {
    return FadeTransition(
      opacity: _titleOpacityAnim,
      child: const Text(
        "Your Page Title",
        style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
      ),
    );
  }

  Widget _buildSearchField(BuildContext context) {
    return AnimatedBuilder(
      animation: _searchWidthAnim,
      builder: (context, child) {
        return SizedBox(
          width: _searchWidthAnim.value,
          child: TextField(
            decoration: const InputDecoration(
              hintText: "Search...",
              border: InputBorder.none,
              contentPadding: EdgeInsets.symmetric(horizontal: 12),
            ),
            autofocus: _isSearchActive, // Auto-focus when search opens
            onSubmitted: (value) {
              // Add your search logic here
              _toggleSearch(); // Optional: Close search after submission
            },
          ),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      color: Theme.of(context).scaffoldBackgroundColor,
      child: Stack(
        alignment: Alignment.centerLeft,
        children: [
          // Title sits at the bottom, fades out when search expands
          _buildTitle(context),
          // Search field overlays the title, slides in from the right
          _buildSearchField(context),
          // Magnifying glass button stays on top, slides left with animation
          AnimatedBuilder(
            animation: _iconPositionAnim,
            builder: (context, child) {
              return Positioned(
                right: _iconPositionAnim.value,
                child: IconButton(
                  icon: Icon(_isSearchActive ? Icons.close : Icons.search),
                  onPressed: _toggleSearch,
                ),
              );
            },
          ),
        ],
      ),
    );
  }
}

// How to use it in your app
void main() {
  runApp(const MaterialApp(home: Scaffold(body: ExpandableSearchHeader())));
}

Key Details to Note

  • AnimationController: Manages the timing and direction of all animations, using SingleTickerProviderStateMixin to sync with the app's refresh rate.
  • CurvedAnimation: Adds natural easing to the transitions, so the animation doesn't feel robotic. You can swap Curves.easeInOut with others like Curves.fastOutSlowIn for different feels.
  • AnimatedBuilder: Efficiently rebuilds only the animated parts of the widget, avoiding unnecessary full-widget rebuilds.
  • Auto-focus: The search field automatically gets focus when expanded, making it seamless for users to start typing.

Quick Optimizations You Can Add

  • Wrap the entire header in a GestureDetector to close the search field when tapping outside the search area.
  • Add a subtle border to the search field when expanded for better visual separation.
  • Adjust the animation duration or curves to match your app's design system.

内容的提问来源于stack exchange,提问作者Leonidas Yopan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 07:17:29