如何在Flutter中实现Figma的Smart Animate过渡效果?
在Flutter中实现类似Figma Smart Animate的引导页平滑过渡效果
要实现Figma Smart Animate那种元素级的平滑过渡(而非普通页面堆叠导航),核心思路是将所有引导页元素放在同一个页面的层级中,通过滚动偏移控制元素的动画参数,而不是使用独立页面的Navigator导航。以下是具体实现方案:
方案一:PageView + 自定义元素动画控制
这种方式通过监听PageView的滚动进度,实时调整各个引导页元素的位置、透明度和缩放,模拟Smart Animate的连贯过渡效果。
步骤1:定义引导页数据模型
class OnboardingItem { final String title; final String description; final String iconAsset; OnboardingItem({ required this.title, required this.description, required this.iconAsset, }); }
步骤2:实现带动画的引导页页面
import 'package:flutter/material.dart'; class SmartAnimateOnboarding extends StatefulWidget { const SmartAnimateOnboarding({super.key}); @override State<SmartAnimateOnboarding> createState() => _SmartAnimateOnboardingState(); } class _SmartAnimateOnboardingState extends State<SmartAnimateOnboarding> { final PageController _pageController = PageController(); double _currentPage = 0.0; final List<OnboardingItem> _onboardingItems = [ OnboardingItem( title: "高效管理", description: "一键整理你的日常任务,让效率翻倍", iconAsset: "assets/ic_task.png", ), OnboardingItem( title: "智能提醒", description: "基于你的习惯推送精准提醒,绝不遗漏重要事项", iconAsset: "assets/ic_remind.png", ), OnboardingItem( title: "数据同步", description: "多设备实时同步,随时随地访问你的任务数据", iconAsset: "assets/ic_sync.png", ), ]; @override void initState() { super.initState(); // 监听PageView滚动,更新当前页面偏移 _pageController.addListener(() { setState(() => _currentPage = _pageController.page!); }); } @override void dispose() { _pageController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ // 空的PageView用于控制滚动逻辑 PageView.builder( controller: _pageController, itemCount: _onboardingItems.length, itemBuilder: (context, index) => const SizedBox.shrink(), ), // 动画元素层:根据滚动偏移控制元素过渡 AnimatedBuilder( animation: _pageController, builder: (context, child) { int currentIndex = _currentPage.floor(); double offset = _currentPage - currentIndex; // 处理向右滑动的情况(offset为负时调整索引) if (offset < 0) { currentIndex -= 1; offset += 1; } return Stack( children: [ // 上一页元素(向右滑动时显示过渡动画) if (currentIndex > 0) _buildPageContent(_onboardingItems[currentIndex - 1], 1 - offset), // 当前页面元素 _buildPageContent(_onboardingItems[currentIndex], offset), // 下一页元素(向左滑动时显示过渡动画) if (currentIndex < _onboardingItems.length - 1) _buildPageContent(_onboardingItems[currentIndex + 1], offset - 1), ], ); }, ), // 底部导航控件 Positioned( bottom: 40, left: 20, right: 20, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ TextButton( onPressed: _currentPage > 0 ? () => _pageController.previousPage( duration: const Duration(milliseconds: 500), curve: Curves.easeInOut, ) : null, child: const Text("上一页"), ), // 页面指示器 Row( children: List.generate(_onboardingItems.length, (index) { final isActive = index == _currentPage.floor(); return Container( width: isActive ? 16 : 12, height: 12, margin: const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( shape: BoxShape.circle, color: isActive ? Colors.blue : Colors.grey.shade300, ), ); }), ), TextButton( onPressed: _currentPage < _onboardingItems.length - 1 ? () => _pageController.nextPage( duration: const Duration(milliseconds: 500), curve: Curves.easeInOut, ) : () => Navigator.pushReplacementNamed(context, "/home"), child: Text(_currentPage < 2 ? "下一页" : "完成"), ), ], ), ), ], ), ); } // 构建单个引导页的内容,根据动画值调整过渡效果 Widget _buildPageContent(OnboardingItem item, double animationValue) { return Opacity( opacity: 1 - animationValue.abs(), child: Transform( transform: Matrix4.translationValues( animationValue * MediaQuery.of(context).size.width, 0, 0, )..scale(1 - animationValue.abs() * 0.1), alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( item.iconAsset, width: 150, height: 150, ), const SizedBox(height: 30), Text( item.title, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Text( item.description, textAlign: TextAlign.center, style: const TextStyle(fontSize: 16, color: Colors.grey), ), ), ], ), ), ); } }
关键原理说明
- 摒弃独立页面导航:普通的
Navigator.push是堆叠页面,无法实现元素级的平滑过渡,因此我们将所有引导页内容放在同一个页面的Stack中。 - 滚动驱动动画:通过
PageController监听滚动偏移,将偏移值转化为元素的动画参数(平移距离、透明度、缩放比例),实现类似Figma Smart Animate的连贯过渡。 - 多元素协同动画:通过
AnimatedBuilder统一控制所有元素的动画进度,确保切换时各个元素的过渡同步且自然。
如果你需要更精细的元素动画(比如不同元素使用不同的动画曲线或延迟),可以在_buildPageContent中为每个元素单独设置动画参数,进一步贴近Figma的效果。
内容的提问来源于stack exchange,提问作者Deyo
相关产品推荐
相关产品推荐

