啥?Flutter也能整3D了吗?我靠,竟然是这样的操作👀

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言:在网上看到了一个3D展示的效果,属实强😎,于是我准备拿Flutter复刻一个,可是晚上找了半天也没有发现Flutter怎么使用3D模型,但是直到我看了这个节目,话不多说,先上效果图!

灵感来源:www.youtube.com/watch?v=FCy…

视频讲解了很多复杂UI的处理制作

屏幕截图 2021-10-28 124641.jpg

效果图:

tt0.top-433400.gif

先分析一下原理,分为3个部分:

1.背景的旋转处理

2.文字的旋转动画处理

3.吉他(贝斯,写文章的时候才发现不是吉他)的旋转处理与吉他背景阴影的处理

1.背景的旋转处理

在复杂的动画中,常用的便是:Transform,Stack,AnimationController

这里也是一样:

tt0.top-133272.gif

_buildBackground() => Positioned.fill(
      top: -_extraHeight,
      bottom: -_extraHeight,
      child: AnimatedBuilder(
        animation: _animator,
        builder: (context, widget) => Transform.translate(
          offset: Offset(_maxSlide * _animator.value, 0),
          //重点是这里
          child: Transform(
            transform: Matrix4.identity() //单位矩阵,倾斜的角度
              ..setEntry(3, 2, 0.001)
              ..rotateY((pi / 2 + 0.1) * -_animator.value),
            alignment: Alignment.centerLeft, //相对于坐标系原点的对齐方式
            child: widget, 
          ),
        ),
        //以下就是普通的Widget
        child: Container(
          color: Color(0xffe8dfce),
          child: Stack(
            overflow: Overflow.visible,
            children: <Widget>[
              //Fender word
              Positioned(
                top: _extraHeight + 0.1 * _screen.height,
                left: 80,
                child: Transform.rotate(
                  angle: 90 * (pi / 180),
                  alignment: Alignment.centerLeft,
                  child: Text(
                    "HELLO",
                    style: TextStyle(
                      fontSize: 100,
                      color: Color(0xFFc7c0b2),
                      shadows: [
                        Shadow(
                          color: Colors.black26,
                          blurRadius: 5,
                          offset: Offset(2.0, 0.0),
                        ),
                      ],
                      fontWeight: FontWeight.w900,
                    ),
                  ),
                ),
              ),
              //给切换时加上一个黑色的背景动画,使动画更加立体
              AnimatedBuilder(
                animation: _animator,
                builder: (_, __) => Container(
                  color: Colors.black.withAlpha(
                    (150 * _animator.value).floor(),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );

2.文字的旋转动画处理

1635400145(1).png

  • 顶部文字
    //适配异形屏
    SafeArea(
          child: AnimatedBuilder(
              animation: _animator,
              builder: (_, __) {
                return Transform.translate(
                    //平移动画
                  offset: Offset((_screen.width - 60) * _animator.value, 0),
                  child: ...);
        )
    )
    
  • 底部文字

    这里的平移动画需要跟随背景动画,在此基础上加了一个透明度,一个平移动画,更加立体

    Opacity(
        //透明度
      opacity: 1 - _animator.value,
        //与背景动画相同,在其基础上加了一个平移动画
      child: Transform.translate(
        offset: Offset((_maxSlide + 50) * _animator.value, 0),
        child: Transform(
          transform: Matrix4.identity()
            ..setEntry(3, 2, 0.001)
            ..rotateY((pi / 2 + 0.1) * -_animator.value),
          alignment: Alignment.centerLeft,
          child: widget,
        ),
      ),
    )
    
  • 左侧文字

    此处的文字动画与上面相差不大,只在参数上有区别

    Transform.translate(
      offset: Offset(_maxSlide * (_animator.value - 1), 0),
      child: Transform(
        transform: Matrix4.identity()
          ..setEntry(3, 2, 0.001)
          ..rotateY(pi * (1 - _animator.value) / 2),
        alignment: Alignment.centerRight,
        child: widget,
      ),
    )
    

3.贝斯的旋转处理与贝斯背景阴影的处理

这部分是本文的重点,先说一下3D效果的原理。我在这里找了120张贝斯的图,分别是每移动一点所对应的图片,当移动时,传入移动的距离,显示不同的图片

在这里封装了一个ImageSequenceAnimator,有个插件叫image_sequence_animator,但是功能不满足我自定义的需求,就把它的源码拿出来改了改。

注意事项:图片的命名

屏幕截图 2021-10-28 141224.jpg

在这里只给大家分析最重要的点了,源代码中我注释的很全~

屏幕截图 2021-10-28 141122.jpg

  • 对于图片名称的补齐:

    String _getSuffix(String value) {
      while (value.length < suffixCount) value = "0" + value;
      return value;
    }
    
  • 获取到图片的路径:

    String _getDirectory() {
      return folderName +
          "/" +
          fileName +
          _getSuffix((suffixStart + _previousFrame).toString()) +
          "." +
          fileFormat;
    }
    
  • UI处理:

    @override
    Widget build(BuildContext context) {
      if (widget.frame != null) {
        if (currentFrame == null ||
            widget.frame != _previousFrame ||
            colorChanged) {
          colorChanged = false;
          _previousFrame = widget.frame;
          if (_previousFrame < frameCount)
            currentFrame = Image.asset(
              _getDirectory(),
              color: color,
              gaplessPlayback: true,
            );
        }
        return currentFrame;
      }
      return ValueListenableBuilder(
        builder: (BuildContext context, int change, Widget cachedChild) {
          if (currentFrame == null ||
              animationController.value.floor() != _previousFrame ||
              colorChanged) {
            colorChanged = false;
            _previousFrame = animationController.value.floor();
            if (_previousFrame < frameCount)
              currentFrame = Image.asset(
                _getDirectory(),
                color: color,
                gaplessPlayback: true,
              );
          }
    //currentFrame是Image
          return currentFrame;
        },
        valueListenable: changeNotifier,
      );
    }
    ​
    
  • 使用方法:

    ImageSequenceAnimator(
      "assets/guitarSequence", //folderName
      "", //fileName
      1, //suffixStart
      4, //suffixCount
      "png", //fileFormat
      120, //frameCount
      fps: 60, 
      isLooping: false,
      isBoomerang: true,
      isAutoPlay: false,
      frame: (_objAnimator.value * 120).ceil(),
      // fullPaths: [(_objAnimator.value * 120).ceil().toString()],//官方插件的使用
    )
    
  • 贝斯阴影处理

    Positioned(
        //无需动画,用Stack叠在贝斯照片下面就行
      top: _extraHeight + 0.13 * _screen.height,
      bottom: _extraHeight + 0.24 * _screen.height,
      left: _maxSlide - 0.41 * _screen.width,
      right: _screen.width * 1.06 - _maxSlide,
      child: Column(
        children: <Widget>[
            //绘制了一个贝斯的形状
          Flexible(
            child: FractionallySizedBox(
              widthFactor: 0.2,
              child: Container(
                decoration: BoxDecoration(
                  boxShadow: [
                    BoxShadow(
                      blurRadius: 50,
                      color: Colors.black38,
                    )
                  ],
                  borderRadius: BorderRadius.circular(50),
                ),
              ),
            ),
          ),
          Flexible(
            child: Container(
              decoration: BoxDecoration(
                boxShadow: [
                  BoxShadow(
                    blurRadius: 50,
                    color: Colors.black26,
                  )
                ],
                borderRadius: BorderRadius.circular(50),
              ),
            ),
          ),
        ],
      ),
    ),
    

Flutter复杂的3D就这样很简单就完成啦~

源代码在这里哦,这里人才汇聚~