• Flutter旋转平移缩放动画实例 -- 手动实现底部FloatingActionButton弹入弹出动画


    先看下完成后的效果:

     这个动画效果在app中很常见,由底部蓝色的FloatingActionButton旋转动画和另外的三个白色按钮的弹入弹出平移缩放动画组成。先看旋转动画的实现:

    一.FloatingActionButton旋转动画

    蓝色按钮在相邻次数的点击时分别对应了顺时针和逆时针的两次45度旋转,所以我们需要声明一个补间动画进行控制:

    1. class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
    2. ... ...
    3. /// 手动控制动画的控制器
    4. late final AnimationController floatButtonAnimController;
    5. /// 手动控制
    6. late final Animation floatButtonAnimation;
    7. @override
    8. void initState() {
    9. super.initState();
    10. /// 不设置重复,使用代码控制进度
    11. floatButtonAnimController = AnimationController(
    12. duration: const Duration(milliseconds: 500),
    13. vsync: this,
    14. );
    15. floatButtonAnimation = Tween(
    16. begin: 0,
    17. end: 0.5
    18. ).animate(floatButtonAnimController);
    19. }
    20. ... ...
    21. }

    声明动画控制器为500ms执行一次,插值器不特殊指定使用默认的线性插值器,并且不指定执行重复次数,由实际代码进行控制:

    1. floatingActionButton: Container(
    2. margin: const EdgeInsets.only(bottom: 16),
    3. child: RotationTransition(
    4. turns: floatButtonAnimation,
    5. child: FloatingActionButton(
    6. backgroundColor: const Color.fromARGB(255, 30, 136, 229),
    7. onPressed: () {//点击事件
    8. ... ...
    9. var animValue = floatButtonAnimController.value;
    10. if (animValue == 0.25) {
    11. floatButtonAnimController.animateTo(0);//逆时针
    12. } else {
    13. floatButtonAnimController.animateTo(0.25);//顺时针
    14. }
    15. },
    16. child: const Icon(Icons.add),
    17. ),
    18. ),
    19. ), // This trailing comma makes auto-formatting nicer for build methods.
    20. floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,

    可以看到在布局中使用了一个旋转动画的Widget即RotationTransition,给他的turns属性指定动画实例即floatButtonAnimation,点击时会判断controller当前的value来执行顺时针/逆时针动画。整体上没有特殊的地方需要注意,很简单的一个补间动画实现。下面来主要说说平移和缩放的实现:

    二.平移缩放动画

    由于三个白色按钮的动画是同时执行和结束的,所以三个组合动画可以直接共用同一个控制器就可以了:

    1. _controller = AnimationController(
    2. vsync: this,
    3. duration: const Duration(milliseconds: 200)
    4. );
    5. _animation = CurvedAnimation( //贝塞尔曲线动画插值器
    6. parent: _controller,
    7. curve: Curves.easeIn,
    8. );

    对于平移动画,最主要的事情就是确定Button移动的起点和终点。终点其实就是放大后三个按钮各自在坐标系中的位置,他们分别对应各自的三个位置,而至于起点是他们三个缩小后的位置,这个位置从理论上来说是同一个并且是蓝色按钮的中心位置,但是在实际绘制布局时仍然需要声明三个重叠的起点,因为他们各自的动画是不尽相同的且是同时执行的,我们无法对同一个Widget同时进行三个不同的动画。

    确定Button移动的起点和终点我们需要用到flukit三方库(pubspec.yaml文件中引入flukit: ^3.0.1)中的一个AfterLayout组件,他是专门用来获取组件大小和相对于屏幕的坐标的:

    1. AfterLayout(
    2. callback: (RenderAfterLayout ral) {
    3. print(ral.size); //子组件的大小
    4. print(ral.offset);// 子组件在屏幕中坐标
    5. },
    6. child: Text('flutter@wendux'),
    7. ),

    首先我们在堆叠布局Stack下声明三个透明布局用来定位,并使用Positioned布局来调整位置:

    1. body: Stack(
    2. alignment: Alignment.bottomCenter,
    3. children: [
    4. _widgetOptions[_curIndex],
    5. Positioned( ///中间的
    6. bottom: 68,
    7. child: Opacity(
    8. opacity: 0,//设置为透明 这里是为了知道放大动画结束后icon应该摆放的位置,所以不需要展示也不需要响应事件
    9. child: AfterLayout(
    10. callback: (v) => childBig1Rect = _getRect(v),
    11. child: childBig1,
    12. )
    13. )
    14. ),
    15. Positioned( ///右边的
    16. bottom: 32,
    17. right: 80,
    18. child: Opacity(
    19. opacity: 0,
    20. child: AfterLayout(
    21. callback: (v) => childBig2Rect = _getRect(v),
    22. child: childBig2,
    23. )
    24. )
    25. ),
    26. Positioned( ///左边的
    27. bottom: 32,
    28. left: 80,
    29. child: Opacity(
    30. opacity: 0,
    31. child: AfterLayout(
    32. callback: (v) => childBig3Rect = _getRect(v),
    33. child: childBig3,
    34. )
    35. )
    36. ),
    37. ... ...
    38. //我们需要获取的是AfterLayout子组件相对于Stack的Rect,通过_getRect方法转换一下
    39. Rect _getRect(RenderAfterLayout renderAfterLayout) {
    40. return renderAfterLayout.localToGlobal(
    41. Offset.zero,
    42. ///找到Stack对应的 RenderObject 对象
    43. ancestor: context.findRenderObject(),
    44. ) & renderAfterLayout.size;
    45. }

    我们拿到定位后的RenderAfterLayout对象后需要通过_getRect方法来转为在Stack下的Rect,而这个Rect一定意义来说就是坐标。

    接着还需要声明三个动画组件作为三个按钮的初始位置:

    1. //是否展示小图标
    2. bool showChild1 = !_animating && _lastAnimationStatus != AnimationStatus.forward;
    3. //执行动画时的目标组件;如果是从小图变为大图,则目标组件是大图;反之则是小图
    4. Widget targetWidget1;
    5. Widget targetWidget2;
    6. Widget targetWidget3;
    7. if (showChild1 || _controller.status == AnimationStatus.reverse) {
    8. targetWidget1 = childSmall1;
    9. targetWidget2 = childSmall2;
    10. targetWidget3 = childSmall3;
    11. } else {
    12. targetWidget1 = childBig1;
    13. targetWidget2 = childBig2;
    14. targetWidget3 = childBig3;
    15. }
    16. ... ...
    17. showChild1 ? AfterLayout(
    18. callback: (v) => child1Rect = _getRect(v),
    19. child: childSmall1
    20. ) : AnimatedBuilder(
    21. animation: _animation,
    22. builder: (context, child) {
    23. //rect 估值器
    24. final rect = Rect.lerp(
    25. child1Rect,
    26. childBig1Rect,
    27. _animation.value,
    28. );
    29. // 通过 Positioned 设置组件大小和位置
    30. return Positioned.fromRect(rect: rect!, child: child!);
    31. },
    32. child: targetWidget1,
    33. ),
    34. showChild1 ? AfterLayout(
    35. callback: (v) => child1Rect = _getRect(v),
    36. child: childSmall2
    37. ) : AnimatedBuilder(
    38. animation: _animation,
    39. builder: (context, child) {
    40. final rect = Rect.lerp(
    41. child1Rect,
    42. childBig2Rect,
    43. _animation.value,
    44. );
    45. return Positioned.fromRect(rect: rect!, child: child!);
    46. },
    47. child: targetWidget2,
    48. ),
    49. showChild1 ? AfterLayout(
    50. callback: (v) => child1Rect = _getRect(v),
    51. child: childSmall3
    52. ) : AnimatedBuilder(
    53. animation: _animation,
    54. builder: (context, child) {
    55. final rect = Rect.lerp(
    56. child1Rect,
    57. childBig3Rect,
    58. _animation.value,
    59. );
    60. return Positioned.fromRect(rect: rect!, child: child!);
    61. },
    62. child: targetWidget3,
    63. ),

    通过布尔型变量showChild1来判断当前是应该显示起点还是应该展示动画,平移和缩放动画的执行是通过Rect自带的估值器完成的,最后用Positioned的fromRect方法刷新位置。

    最后就是点击FloatingActionButton按钮执行弹出弹入动画:

    1. floatingActionButton: Container(
    2. margin: const EdgeInsets.only(bottom: 16),
    3. child: RotationTransition(
    4. turns: floatButtonAnimation,
    5. child: FloatingActionButton(
    6. backgroundColor: const Color.fromARGB(255, 30, 136, 229),
    7. onPressed: () {
    8. /// 平移缩放
    9. setState(() {//通过setState方法重置动画状态完成逆向执行
    10. _animating = true;
    11. if (isSmallToBig) {
    12. isSmallToBig = false;
    13. _controller.forward();
    14. } else {
    15. isSmallToBig = true;
    16. _controller.reverse();
    17. }
    18. });
    19. /// 旋转
    20. var animValue = floatButtonAnimController.value;
    21. if (animValue == 0.25) {
    22. floatButtonAnimController.animateTo(0);
    23. } else {
    24. floatButtonAnimController.animateTo(0.25);
    25. }
    26. },
    27. child: const Icon(Icons.add),
    28. ),
    29. ),
    30. ), // This trailing comma makes auto-formatting nicer for build methods.
    31. floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,

    代码自提入口:

    https://github.com/HAND-jiaming/flutter_demo

  • 相关阅读:
    Android开发笔记——快速入门(从入门SQLlite到Room放肆)2
    卷妹带你回顾Java基础(一)每日更新Day2
    什么是机器学习中的监督学习和无监督学习,举例说明
    HDFS工作流程和机制
    编译和链接
    【Java练习题第二期】:用Java实现链表内指定区域的反转
    PXE网络批量装机(centos7)
    【Vue】数据校验插件开发实例
    Linux系统进程的个人理解和解释
    Python3 VSCode 配置
  • 原文地址:https://blog.csdn.net/qq_37159335/article/details/126362481