CubixD,一个 3D 立方体

3d cube mooving from right to left

安装

将 cubixd 添加到您的 pubspec.yaml 依赖项中

dependencies:
  cubixd: ^0.1.1

然后导入它

import 'package:cubixd/cubixd.dart';

功能

  • 将此添加到您的 Flutter 应用中即可获得一个 3D 立方体!

入门

此软件包包含 2 个小部件

  1. AnimatedCubixD
  2. CubixD

AnimatedCubixD 是动画化的 3D 立方体,此小部件使用 3 个控制器(AnimationController)来实现 3 种不同的动画。包括阴影、彩色星星、所有动画以及选择面的功能

CubixD 是静态 3D 立方体,此小部件包括选择面的功能(无动画)

您在开头看到的示例可以使用以下代码实现

import 'package:cubixd/cubixd.dart';

Center(
  child: AnimatedCubixD(
    onSelected: ((SelectedSide opt) => opt == SelectedSide.bottom ? false : true),
    size: 200.0,
    left: Container(
      decoration: const BoxDecoration(
        image: DecorationImage(
          image: AssetImage("assets/images/graphql.png"),
          fit: BoxFit.cover,
        ),
      ),
    ),
    front: Container(
      decoration: const BoxDecoration(
        image: DecorationImage(
          image: AssetImage("assets/images/nestjs.png"),
          fit: BoxFit.cover,
        ),
      ),
    ),
    back: Container(
      decoration: const BoxDecoration(
        image: DecorationImage(
          image: AssetImage("assets/images/mongodb.png"),
          fit: BoxFit.cover,
        ),
      ),
    ),
    top: ...,
    bottom: ...,
    right: ...,
  ),
),

AnimatedCubixD

参数

参数 类型 默认值 描述
advancedXYposAnim AnimRequirements 高级 XY 位置动画。如果您想对 AnimationController 和所需的 2 种动画进行更多控制,可以设置此参数。请注意,当您设置此选项时,AnimationController 将不会自动前进和处置。您可以在下方阅读更多信息和示例
afterRestDel 持续时间 Duration(miliseconds: 50) 恢复后延迟。此参数表示 cubixd 恢复动画执行后,为使主动画再次出现而产生的延迟
afterSelDel 持续时间 Duration(seconds: 4) 选择后延迟。此参数表示 cubixd 在面被选中后等待的持续时间,之后将 cubixd 恢复到主动画
back * Widget 应显示在背面一侧的小部件
bottom * Widget 应显示在底部一侧的小部件
buildOnSelect Widget Function(double, AnimationController) 如果您不喜欢用户选择面时触发的默认星星动画。您可以使用此参数,以极大的自由度设置不同的动画。这个参数相当复杂,所以您可以在下方阅读更多相关信息
debounceTime 持续时间 Duration(miliseconds: 500) 防抖时间。cubixd 使用防抖器,这意味着当您不断移动 cubixd 以选择一个面时,它不会执行选择,直到您将其保持静止且面有效,并等待此处指定的持续时间,它将触发选择,否则如果在时间运行之前移动它,它将“反弹”选择并从 0 开始重新计算此指定时间
front * Widget 应显示在正面一侧的小部件
left * Widget 应显示在左侧一侧的小部件
onPanUpdate void Function() 更新时调用。这是用户移动 cubixd 以选择面时执行的回调
onRestCurve Curve Curves.fastOutSlowIn 恢复曲线。此参数设置恢复动画应具有的曲线。将恢复动画理解为选择面后执行的动画,用于将 cubixd 恢复到其初始位置
onSelecCurve Curve Curves.fastOutSlowIn 选择曲线。此参数设置选择动画应具有的曲线。将选择动画理解为防抖计时器结束时触发并触发选择的动画
onSelect bool Function(SelectedSide) 选择时调用。应在用户选择面时触发的回调
restDuration 持续时间 Duration(miliseconds: 800) 恢复持续时间。恢复动画应花费的持续时间
right * Widget 应显示在右侧一侧的小部件
selDuration 持续时间 Duration(miliseconds: 400) 选择持续时间。选择动画应花费的持续时间。将选择动画理解为防抖器触发后立即发生的动画
sensitivityFac 双精度 1.0 灵敏度因子。就像鼠标移动时有灵敏度一样。cubixd 也有灵敏度。理想情况下,此值应接近 1,而不是 0 或更低。其值越大,灵敏度也越高
shadow 布尔值 阴影。定义 cubixd 是否应具有阴影。请注意,如果没有阴影,cubixd 将无法很好地上下移动(并且此小部件占据的最终高度将减小)
simplePosAnim SimpleAnimRequirements SimpleAnimRequirements(duration: const Duration(seconds: 10), xBegin: -pi / 4, xEnd: (7*pi)/4, yBegin: pi / 4, yEnd: pi / 4, reverseWhenDone: false, infinite: true) 如果您不想使用 AnimationController 设置高级选项,可以使用此参数设置一些参数以使 cubixd 移动 c:您可以在下方阅读更多信息
size * 双精度 每个面应具有的宽度和高度
stars 布尔值 选择面后是否应出现彩色星星
top * Widget 应显示在顶面一侧的小部件

onSelect

一个 3D 立方体应始终有 6 个面,但也许您只需要 5 个面。您可以准备 5 个面以供选择,而 1 个面处于停用状态。这正是考虑此参数时所想到的:一个回调,发送被选中的面,如果此回调返回 false,则该面无法被选中,否则可以。

AnimatedCubixD(
    ...
    onSelected: (SelectedSide opt) {
        switch (opt) {
            case SelectedSide.back:
                return true;
            case SelectedSide.top:
                return true;
            case SelectedSide.front:
                return true;
            case SelectedSide.bottom:
                return false; // out of service
            case SelectedSide.right:
                return true;
            case SelectedSide.left:
                return true;
            case SelectedSide.none:
                // You can do something else
                return false; // Nothing will happend if you return true at this point
            default:
                throw Exception("Unimplemented option");
        }
    }
    ...
),

如果未设置此参数(null),则用户将无法移动 cubixd

结果是

Mooving a 3d cube with the mouse

advancedXYposAnim

AnimRequirements

参数 类型 默认值 描述
controller * AnimationController 应在主动画中使用 AnimationController。您可以在此处设置主动画的持续时间
xAnimation * 动画 应在水平轴上使用的动画。您可以在此处设置 x 的起始角度和结束角度(以弧度为单位)
yAnimation * 动画 应在垂直轴上使用的动画。您可以在此处设置 y 的起始角度和结束角度(以弧度为单位)

示例

...
late final AnimationController _mainCtrl;
late final Animation<double> _xAnimation;
late final Animation<double> _yAnimation;
...
@override
void initState(){
    _mainCtrl   = AnimationController(vsync: this, duration: const Duration(seconds: 10));

    _xAnimation = Tween<double>(begin: -pi / 4, end: pi * 2 - pi / 4).animate(_mainCtrl);
    _yAnimation = Tween<double>(begin: pi / 4, end: pi / 4).animate(_mainCtrl);

    _mainCtrl.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _mainCtrl.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _mainCtrl.forward();
      }
      print(status);
    });

    _mainCtrl.forward();
    super.initState();
}
...
AnimatedCubixD(
    ...
    advancedXYposAnim: AnimRequirements(
        controller: _mainCtrl,
        xAnimation: _xAnimation,
        yAnimation: _yAnimation,
    ),
    ...
),
...
@override
void dispose(){
    _mainCtrl.dispose();
    super.dispose();
}
...

buildOnSelect

这可能是此软件包中最复杂的参数,因此我建议您根据需要多次阅读此部分

如果您想在选择面时获得另一个启动动画怎么办?通过此参数,您可以做到。当用户选择一个面时,另一个动画正在运行,将 cubixd 放置到选定的面,我称之为“选择动画”,此动画与主动画完全不同,因此它具有另一个 AnimationController。

AnimatedCubixD 使用 3 个不同的控制器来实现 3 种不同的动画

  1. 主动画。它使用您可能已从 advancedXYposAnim 参数传递给 AnimatedCubixD 的控制器,或者未传递任何控制器。如果您根本没有传递任何控制器,它将自己创建控制器并自动执行 forward 和 dispose 方法。

  2. 选择动画。仅当 onSelect 参数不为 null 时,它才会创建自己的控制器。这用于执行播放以调整选定面的精确角度的动画

  3. 恢复动画。仅当 onSelect 参数不为 null 时,它才会创建自己的控制器。这用于执行播放以在用户选择面后将 cubixd 调整到其初始位置的动画

考虑到这一点,此参数的回调会发送 2 个参数:size(double)和 select 动画控制器(AnimationController),此回调期望您返回一个将在用户选择面后显示的小部件

import 'dart:math';

import 'package:cubixd/cubixd.dart';
...
AnimatedCubixD(
    ...
    buildOnSelect: (double size, AnimationController ctrl) => CircleStar(ctrl: ctrl, size: size),
    stars: false,
    ...
),
...
class _Animations {
  final Animation<double> xAnim;
  final Animation<double> yAnim;
  final double size;

  _Animations(this.xAnim, this.yAnim, this.size);
}

class CircleStar extends StatelessWidget {
  final CurvedAnimation _curvedA;
  final double overflowQ            = 0.4;
  final List<_Animations> _starsA   = [];
  final List<int> _minMax           = [20, 35];

  CircleStar({
    Key? key,
    required AnimationController ctrl,
    required double size,
  })  : _curvedA = CurvedAnimation(parent: ctrl, curve: Curves.easeOutCubic),
        super(key: key) {
    _initParams(size);
    ctrl.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _initParams(size);
      }
    });
  }

  void _initParams(double size) {
    _starsA.clear();

    final int length        = Random().nextInt(_minMax[1] - _minMax[0]) + _minMax[0];
    final double overflow   = overflowQ * size;

    for (int i = 0; i < length; i++) {
      final double shapeSize = Random().nextDouble() * size * 0.8;

      final double lPos = Random().nextDouble() * size;
      final double tPos = Random().nextDouble() * size;

      final double xEnd;
      final double yEnd;

      if (-lPos.abs() % size < -tPos.abs() % size) {
        xEnd = lPos > size / 2 ? size + overflow : -overflow;
        yEnd = xEnd * (tPos / xEnd);
      } else {
        yEnd = tPos > size / 2 ? size + overflow : -overflow;
        xEnd = yEnd / (tPos / lPos);
      }
      _starsA.add(_Animations(
        Tween<double>(begin: lPos, end: xEnd).animate(_curvedA),
        Tween<double>(begin: tPos, end: yEnd).animate(_curvedA),
        shapeSize,
      ));
    }
  }

  List<Widget> get _buildList {
    final List<Widget> list = [];
    final Color color       = Color((Random().nextDouble() * 0xFFFFFF).toInt());

    for (int i = 0; i < _starsA.length; i++) {
      list.add(Positioned(
        left: 0,
        top: 0,
        child: Transform.translate(
          offset: Offset(_starsA[i].xAnim.value, _starsA[i].yAnim.value),
          child: Transform.rotate(
            angle: -4 * pi * _curvedA.value,
            child: ClipPath(
              clipper: _CircleStarClip(),
              child: Container(
                color: color.withOpacity(1 - _curvedA.value),
                height: _starsA[i].size,
                width: _starsA[i].size,
              ),
            ),
          ),
        ),
      ));
    }
    return list;
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _curvedA,
      builder: (_, __) {
        return Stack(children: _buildList);
      },
    );
  }
}

class _CircleStarClip extends CustomClipper<Path> {
  static const _starShrink  = 2;
  static const _starSides   = 5;
  static const _deg90       = pi / 2;

  @override
  Path getClip(Size size) {
    final double bigRad   = size.width / 2;

    final double centerX  = size.width / 2;
    final double centerY  = size.height / 2;

    final double smallRad = bigRad / _starShrink;

    const double sides    = 2 * pi / _starSides;
    final Path path       = Path()..moveTo(size.width / 2, 0);

    for (int i = 0; i < _starSides + 1; i++) {
      path.lineTo(cos(sides * i + _deg90) * bigRad + centerX,
          sin(sides * i + _deg90) * bigRad + centerY);

      path.lineTo(cos(sides * i + _deg90) * smallRad + centerX,
          sin(sides * i + _deg90) * smallRad + centerY);
    }
    return path..close();
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}
...

提示

  1. 您可以拥有您的自定义动画和默认动画(星星)一起运行
  2. 您可以将自定义动画与 StatefulWidget 而不是 StatelessWidget 编码,并使用更传统的方法

这是结果的慢动作

Circle stars splashing when selecting

simplePosAnim

之前,我们提到此参数的默认值是

import 'package:cubixd/cubixd.dart';
...
simplePosAnim: SimpleAnimRequirements(
    duration: const Duration(seconds: 10),
    infinite: true,
    reverseWhenDone: false,
    xBegin: -pi / 4,
    xEnd: (7*pi)/4,
    yBegin: pi / 4,
    yEnd: pi / 4,
),
...

仅当未设置此参数(simplePosAnim)和 advancedXYposAnim 时,cubixd 才将这些值作为默认动画

另一个例子

import 'package:cubixd/cubixd.dart';
...
simplePosAnim: SimpleAnimRequirements(
    duration: const Duration(seconds: 11),
    infinite: true,
    reverseWhenDone: true,
    xBegin: pi / 4,
    xCurve: Curves.ease,
    xEnd: 2 * pi,
    yBegin: -pi / 4,
    yCurve: Curves.ease,
    yEnd: 4 * pi,
),
...

SimpleAnimRequirements

参数 类型 默认值 描述
duration * 持续时间 主动画应具有的持续时间
infinite 布尔值 主动画是否应无限播放
reverseWhenDone 布尔值 完成时 cubixd 是否应向后播放
xBegin * 双精度 动画开始时应设置的水平角度(以弧度为单位)
xCurve Curve Curves.linear 主动画在水平轴上应具有的曲线
xEnd * 双精度 动画结束时应设置的水平角度(以弧度为单位)
yBegin * 双精度 动画开始时应设置的垂直角度(以弧度为单位)
yCurve Curve Curves.linear 主动画在垂直轴上应具有的曲线
yEnd * 双精度 动画结束时应设置的垂直角度(以弧度为单位)

CubixD

CubixD 是显示 3D 立方体的小部件。阴影和旋转动画不是此小部件的一部分,但选择面是此小部件的一部分(几乎),除了用于将 cubixd 放置到其精确正确位置的动画不是此小部件的一部分

参数

参数 类型 默认值 描述
back * Widget 应显示在背面一侧的小部件
bottom * Widget 应显示在底部一侧的小部件
debounceTime 持续时间 持续时间(毫秒: 500) 防抖时间。cubixd 使用防抖器,这意味着当您不断移动 cubixd 以选择一个面时,它不会执行选择,直到您将其保持静止且面有效,并等待此处指定的持续时间,它将触发选择,否则如果在时间运行之前移动它,它将“反弹”选择并从 0 开始重新计算此指定时间
delta * Vector2 cubixd 的水平和垂直角度(以弧度为单位)。您可以在下方阅读更多相关信息
front * Widget 应显示在正面一侧的小部件
left * Widget 应显示在左侧一侧的小部件
onPanUpdate VoidCallback 更新时调用。这是用户移动 cubixd 以选择面时执行的回调
onSelected void Function(SelectedSide opt, Vector2 delta) 选中时调用。应在用户选择面时触发的回调
right * Widget 应显示在右侧一侧的小部件
sensitivityFac 双精度 1.0 灵敏度因子。就像鼠标移动时有灵敏度一样。cubixd 也有灵敏度。理想情况下,此值应接近 1,而不是 0 或更低。其值越大,灵敏度也越高
size * 双精度 每个面应具有的宽度和高度
top * Widget 应显示在顶面一侧的小部件

delta

此参数表示 cubixd 的水平和垂直角度(以弧度为单位)。AnimatedCubixD 使用此参数与 AnimatedBuilder 一起运行动画,通过更新其各自的控制器每次指示它

import 'package:vector_math/vector_math_64.dart' show Vector2;
import 'package:cubixd/cubixd.dart';
...
CubixD(
    ...
    delta: Vector2(verticalAngle, horizontalAngle)
    ...
),
...

这是一个例子

import 'package:cubixd/cubixd.dart';
...
CubixD(
  size: 200.0,
  delta: Vector2(pi / 4, pi / 4),
  onSelected: (SelectedSide opt, Vector2 delta) {
    print('On selected callback:\n\topt = ${opt}\n\tdelta = ${delta}');
  },
  front: ...,
  back: ...,
  right: ...,
  left: ...,
  top: ...,
  bottom: ...,
),
...

结果是

Mooving a 3d cube with the mouse

附加

SelectedSide

SelectedSide 是一个枚举,有助于了解哪个面被选中

enum SelectedSide { front, back, right, left, top, bottom, none }

计算不精确

请注意,当您想获得特定角度时。水平角度会根据垂直角度“改变”方向,例如

如果垂直角度在 -90° 和 90° 之间。大于 0(正)的水平角度方向是从右到左。

否则,如果垂直角度大于 90°。大于 0(正)的水平角度方向是从左到右。

3d cube rotating on itself showing its angles of rotation

待办事项

  1. 也许使用小部件列表 List<Widget> 来附加前面、后面、右面、左面、上面和下面的小部件会更好

  2. 提供控制上下动画的可能性

GitHub

查看 Github