如何在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
相关产品推荐
相关产品推荐

