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

如何在Flutter中实现Zomato搜索框的滑动过渡动画?

实现Zomato搜索框的滑动过渡动画

我是Flutter新手,正在练习组件构建,想实现Zomato应用里搜索框的滑动过渡动画。目前UI已经完成,但没法实现目标动画——现在只能做到提示文本从Offset(0, 1)到Offset(0, 0)或者Offset(0, 0)到Offset(0, -1)的滑动过渡,求实现思路。

当前实现代码

import 'dart:async';

import 'package:flutter/material.dart';

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

  @override
  State<ZomatoTextField> createState() => _ZomatoTextFieldState();
}

class _ZomatoTextFieldState extends State<ZomatoTextField>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late Animation<Offset> slideAnimation;
  // late List<Text> textWidgetList;
  // final List<String> hintList = ['Ice Cream', 'Samosa', 'Biryani'];
  @override
  void initState() {
    controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
    slideAnimation = Tween<Offset>(
      begin: const Offset(0, 1.5),
      end: Offset.zero,
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: Curves.ease,
      ),
    );
    Timer(const Duration(milliseconds: 2000), () {
      controller.forward();
    });
    // textWidgetList = List.generate(
    //     hintList.length,
    //     (index) => Text(
    //           hintList[index],
    //           style: const TextStyle(color: Color(0xff767C8F), fontSize: 16),
    //         ));
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      clipBehavior: Clip.hardEdge,
      height: 48,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: const BorderRadius.all(Radius.circular(16)),
        boxShadow: [
          BoxShadow(
            blurRadius: 2,
            color: Colors.grey.withOpacity(0.2),
            offset: const Offset(0, 0),
            spreadRadius: 3,
          ),
        ],
      ),
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      child: Row(
        children: [
          const Icon(
            Icons.search,
            color: Color(0xffE95161),
          ),
          const SizedBox(
            width: 8,
          ),
          Expanded(
            child: SlideTransition(
              position: slideAnimation,
              child: const Text(
                'Search \"Ice Cream\"',
                style: const TextStyle(color: Color(0xff767C8F), fontSize: 16),
              ),
            ),
          ),
          // Expanded(
          //   child: Container(
          //     // color: Colors.red,
          //     child: Column(
          //       crossAxisAlignment: CrossAxisAlignment.start,
          //       children: [...textWidgetList],
          //     ),
          //   ),
          // ),
          const VerticalDivider(
            endIndent: 5,
            indent: 5,
          ),
          const Icon(
            Icons.mic_none_outlined,
            color: Color(0xffE95161),
          ),
        ],
      ),
    );
    // TextField(
    //   controller: TextEditingController(),
    //   showCursor: false,

    //   decoration: const InputDecoration(
    //       contentPadding: EdgeInsets.all(4),
    //       hintText: 'Search \"Ice Cream\"',
    //       border: OutlineInputBorder(

    //         borderRadius: BorderRadius.all(Radius.circular(16)),
    //       ),
    //       prefixIcon: Icon(
    //         Icons.search,
    //       ),
    //       suffixIcon: Row(
    //         mainAxisSize: MainAxisSize.min,
    //         children: [
    //           Icon(Icons.mic),
    //         ],
    //       )),
    // );
  }
}

实现思路

要实现Zomato那种循环滚动的提示文本动画,核心是让多个提示文本在垂直方向循环滑入滑出,而非单个文本的单次滑动,具体调整步骤如下:

  • 多文本垂直布局:把所有提示文本放在一个Column中,每个文本的高度和搜索框的文本区域高度一致,确保每次只有一个文本处于可视区域。
  • 循环动画控制:利用AnimationController的状态回调,在一次滑出动画结束后,重置偏移到下一个文本的起始位置,再执行滑入动画;通过Timer.periodic定时触发切换逻辑,实现循环效果。
  • 裁剪可视区域:用ClipRect包裹文本滑动区域,避免文本滑动时超出搜索框边界。
  • 优化动画曲线:使用Curves.easeInOut让动画过渡更自然,匹配Zomato的视觉效果。

修改后的示例代码

import 'dart:async';
import 'package:flutter/material.dart';

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

  @override
  State<ZomatoTextField> createState() => _ZomatoTextFieldState();
}

class _ZomatoTextFieldState extends State<ZomatoTextField>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late Animation<Offset> slideAnimation;
  final List<String> hintList = ['Ice Cream', 'Samosa', 'Biryani'];
  int currentIndex = 0;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
    slideAnimation = Tween<Offset>(
      begin: Offset.zero,
      end: const Offset(0, -1),
    ).animate(CurvedAnimation(parent: controller, curve: Curves.easeInOut));

    _startCycleAnimation();
  }

  void _startCycleAnimation() {
    Timer.periodic(const Duration(seconds: 2), (timer) {
      if (!mounted) {
        timer.cancel();
        return;
      }
      // 执行滑出动画
      controller.forward().then((_) {
        setState(() {
          currentIndex = (currentIndex + 1) % hintList.length;
        });
        // 重置控制器,准备下一次滑入
        controller.reset();
      });
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Container(
      clipBehavior: Clip.hardEdge,
      height: 48,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: const BorderRadius.all(Radius.circular(16)),
        boxShadow: [
          BoxShadow(
            blurRadius: 2,
            color: Colors.grey.withOpacity(0.2),
            offset: const Offset(0, 0),
            spreadRadius: 3,
          ),
        ],
      ),
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      child: Row(
        children: [
          const Icon(
            Icons.search,
            color: Color(0xffE95161),
          ),
          const SizedBox(width: 8),
          Expanded(
            child: ClipRect(
              child: SlideTransition(
                position: slideAnimation,
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: hintList.map((hint) {
                    return SizedBox(
                      height: 40, // 与搜索框文本区域高度匹配
                      child: Align(
                        alignment: Alignment.centerLeft,
                        child: Text(
                          'Search "$hint"',
                          style: const TextStyle(color: Color(0xff767C8F), fontSize: 16),
                        ),
                      ),
                    );
                  }).toList(),
                ),
              ),
            ),
          ),
          const VerticalDivider(endIndent: 5, indent: 5),
          const Icon(Icons.mic_none_outlined, color: Color(0xffE95161)),
        ],
      ),
    );
  }
}

内容的提问来源于stack exchange,提问作者Pratik Patil

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 06:24:55