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

先看下完成后的效果:

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

一.FloatingActionButton旋转动画

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

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {

    ... ...

    /// 手动控制动画的控制器
    late final AnimationController floatButtonAnimController;

    /// 手动控制
    late final Animation<double> floatButtonAnimation;

    @override
    void initState() {
        super.initState();
        /// 不设置重复,使用代码控制进度
        floatButtonAnimController = AnimationController(
          duration: const Duration(milliseconds: 500),
          vsync: this,
        );
        floatButtonAnimation = Tween<double>(
            begin: 0,
            end: 0.5
        ).animate(floatButtonAnimController);
    }

    ... ...
}

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

      floatingActionButton: Container(
        margin: const EdgeInsets.only(bottom: 16),
        child: RotationTransition(
          turns: floatButtonAnimation,
          child: FloatingActionButton(
            backgroundColor: const Color.fromARGB(255, 30, 136, 229),
            onPressed: () {//点击事件
                ... ...
                var animValue = floatButtonAnimController.value;
                if (animValue == 0.25) {
                  floatButtonAnimController.animateTo(0);//逆时针
                } else {
                  floatButtonAnimController.animateTo(0.25);//顺时针
                }
            },
            child: const Icon(Icons.add),
          ),
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,

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

二.平移缩放动画

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

_controller = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 200)
);

_animation = CurvedAnimation( //贝塞尔曲线动画插值器
    parent: _controller,
    curve: Curves.easeIn,
);

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

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

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

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

body: Stack(
        alignment: Alignment.bottomCenter,
        children: <Widget> [
          _widgetOptions[_curIndex],
          Positioned( ///中间的
            bottom: 68,
            child: Opacity(
                opacity: 0,//设置为透明 这里是为了知道放大动画结束后icon应该摆放的位置,所以不需要展示也不需要响应事件
                child: AfterLayout(
                  callback: (v) => childBig1Rect = _getRect(v),
                  child: childBig1,
                )
            )
          ),
          Positioned( ///右边的
              bottom: 32,
              right: 80,
              child: Opacity(
                  opacity: 0,
                  child: AfterLayout(
                    callback: (v) => childBig2Rect = _getRect(v),
                    child: childBig2,
                  )
              )
          ),
          Positioned( ///左边的
              bottom: 32,
              left: 80,
              child: Opacity(
                  opacity: 0,
                  child: AfterLayout(
                    callback: (v) => childBig3Rect = _getRect(v),
                    child: childBig3,
                  )
              )
          ),

... ...


//我们需要获取的是AfterLayout子组件相对于Stack的Rect,通过_getRect方法转换一下
Rect _getRect(RenderAfterLayout renderAfterLayout) {
    return renderAfterLayout.localToGlobal(
      Offset.zero,
      ///找到Stack对应的 RenderObject 对象
      ancestor: context.findRenderObject(),
    ) & renderAfterLayout.size;
}

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

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

  //是否展示小图标
  bool showChild1 = !_animating && _lastAnimationStatus != AnimationStatus.forward;
  //执行动画时的目标组件;如果是从小图变为大图,则目标组件是大图;反之则是小图
  Widget targetWidget1;
  Widget targetWidget2;
  Widget targetWidget3;
  if (showChild1 || _controller.status == AnimationStatus.reverse) {
    targetWidget1 = childSmall1;
    targetWidget2 = childSmall2;
    targetWidget3 = childSmall3;
  } else {
    targetWidget1 = childBig1;
    targetWidget2 = childBig2;
    targetWidget3 = childBig3;
  }

    ... ...
          showChild1 ? AfterLayout(
              callback: (v) => child1Rect = _getRect(v),
              child: childSmall1
          ) : AnimatedBuilder(
            animation: _animation,
            builder: (context, child) {
              //rect 估值器
              final rect = Rect.lerp(
                child1Rect,
                childBig1Rect,
                _animation.value,
              );
              // 通过 Positioned 设置组件大小和位置
              return Positioned.fromRect(rect: rect!, child: child!);
            },
            child: targetWidget1,
          ),

          showChild1 ? AfterLayout(
              callback: (v) => child1Rect = _getRect(v),
              child: childSmall2
          ) : AnimatedBuilder(
            animation: _animation,
            builder: (context, child) {
              final rect = Rect.lerp(
                child1Rect,
                childBig2Rect,
                _animation.value,
              );
              return Positioned.fromRect(rect: rect!, child: child!);
            },
            child: targetWidget2,
          ),

          showChild1 ? AfterLayout(
              callback: (v) => child1Rect = _getRect(v),
              child: childSmall3
          ) : AnimatedBuilder(
            animation: _animation,
            builder: (context, child) {
              final rect = Rect.lerp(
                child1Rect,
                childBig3Rect,
                _animation.value,
              );
              return Positioned.fromRect(rect: rect!, child: child!);
            },
            child: targetWidget3,
          ),

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

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

      floatingActionButton: Container( 
        margin: const EdgeInsets.only(bottom: 16),
        child: RotationTransition(
          turns: floatButtonAnimation,
          child: FloatingActionButton(
            backgroundColor: const Color.fromARGB(255, 30, 136, 229),
            onPressed: () {
              /// 平移缩放
              setState(() {//通过setState方法重置动画状态完成逆向执行
                _animating = true;
                if (isSmallToBig) {
                  isSmallToBig = false;
                  _controller.forward();
                } else {
                  isSmallToBig = true;
                  _controller.reverse();
                }
              });
              /// 旋转
              var animValue = floatButtonAnimController.value;
              if (animValue == 0.25) {
                floatButtonAnimController.animateTo(0);
              } else {
                floatButtonAnimController.animateTo(0.25);
              }
            },
            child: const Icon(Icons.add),
          ),
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,

Flutter 底部弹出动画可以通过使用 BottomSheet widget 和 AnimatedContainer widget 来实现。 首先,创建一个 StatefulWidget,包含一个 bool 变量用于控制 BottomSheet 的显示和隐藏。 ``` class BottomSheetDemo extends StatefulWidget { @override _BottomSheetDemoState createState() => _BottomSheetDemoState(); } class _BottomSheetDemoState extends State<BottomSheetDemo> { bool _isVisible = false; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Bottom Sheet Demo'), ), body: Center( child: RaisedButton( child: Text('Show Bottom Sheet'), onPressed: () { setState(() { _isVisible = true; }); }, ), ), bottomSheet: _isVisible ? Container( decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), blurRadius: 5.0, spreadRadius: 1.0, offset: Offset(0.0, -1.0), ), ], ), child: SafeArea( child: AnimatedContainer( duration: Duration(milliseconds: 300), height: _isVisible ? 200.0 : 0.0, child: Center( child: Text('This is a Bottom Sheet'), ), ), ), ) : null, ); } } ``` 在上面的代码中,我们使用了一个 RaisedButton 来触发 Bottom Sheet 的显示,当用户点击按钮后,我们将 _isVisible 变量设置为 true,Bottom Sheet 就会显示出来。 Bottom Sheet 的内容是一个 AnimatedContainer,它的高度可以通过修改 _isVisible 变量来控制。在 AnimatedContainer 中,我们设置了一个动画时长为 300 毫秒,当 _isVisible 变量变化时,高度会从 0.0 到 200.0 进行动画过渡。 在 Bottom Sheet 的外部,我们使用了一个 Container 来包装它,并设置了一些阴影效果和背景颜色。我们还使用了 SafeArea 来确保 Bottom Sheet 不会被设备的导航栏遮挡。 通过这种方式,我们可以很容易地实现一个底部弹出动画效果。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我们间的空白格

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值