Flutter:从具有正确重叠的矩阵变换中布局具有不同 z 坐标的多个子项

Flutter: Layout multiple children with differing z coordinates from matrix transformations with correct overlap

我正在尝试构建 threejs Periodic Table Helix 视图的 Flutter Web 版本(参见此处:https://mrdoob.com/lab/javascript/threejs/css3d/periodictable/ 并单击“Helix”)

目前,我将所有元素块放在一个堆栈中,并使用矩阵变换来定位和旋转它们。见以下代码:

  @override
  Widget build(BuildContext context) {
    double initialRotateX = (widget.atomicNumber - 1) *
        (math.pi / Constants.numberOfElementsPerHalfCircle);
    return Transform(
      transform: Matrix4.identity()
        ..translate(
          math.sin((widget.atomicNumber - 1) *
                  (math.pi / Constants.numberOfElementsPerHalfCircle)) *
              Constants.radius,
          widget.atomicNumber * 4,
          -math.cos((widget.atomicNumber - 1) *
                  (math.pi / Constants.numberOfElementsPerHalfCircle)) *
              Constants.radius,
        )
        ..translate(Constants.elementCardHalfSize)
        ..rotateY(-initialRotateX)
        ..translate(-Constants.elementCardHalfSize),
      child: Container(
        decoration: BoxDecoration(
            color: Constants.elementCardColor,
            border: Border.all(width: 1, color: Colors.white.withOpacity(0.3))),
        child: SizedBox(
          width: Constants.elementCardWidth.toDouble(),
          height: Constants.elementCardHeight.toDouble(),
          child: ElementText()
          
        ),
      ),
    );

然后将所有这些卡片放置在另一个 class 的堆栈小部件中,然后旋转该小部件,如下所示:

return AnimatedBuilder(
      animation: widget.animationControllerX,
      builder: (context, _) {
        double rotateX =
            widget.animationControllerX.value / Constants.turnResolution;
        double rotateY =
            widget.animationControllerY.value / Constants.turnResolution;
        return Transform(
          transform: Matrix4.identity()
            ..setEntry(3, 2, 0.001)
            ..rotateY(-rotateX)
            ..rotateX(rotateY),
          alignment: Alignment.center,
          child: Container(
            child: Stack(
              children: List<Widget>.generate(
                Constants.numberOfElements,
                (index) => ElementCard(
                  atomicNumber:
                      Constants.elements[(index + 1).toString()].atomicNumber,
                ),
              ),
            ),
          ),
        );
      },
    );

如您所见,应用了“..rotateY”转换,这使得卡片在旋转堆叠时看起来向前移动。

然而,当旋转时,应该在后面的卡片变小了,但是在应该在前面的卡片上涂漆。 (毫无疑问,卡片的位置是正确的,因为我可以沿 x 轴旋转它们并亲眼看到,但在绘画时,背面的卡片被涂在正面卡片的文字上。我相信这是因为堆栈总是根据它们提供的顺序定位其小部件的高度,并且顺序在列表中是固定的。有什么办法可以解决这个问题吗?

截图解释我的意思:

我可以更改图块的不透明度以使问题更加明显:

如您所见,较大的卡片更靠近屏幕,因为这就是它们的变换方式,但它们是在较小的卡片后面绘制的。关于如何实现这一点有什么想法吗?

所以我找到了解决办法。它并不完美,但到目前为止它是有效的。

问题是 Flutter 会按照它们出现的顺序绘制小部件。由于较大的卡片在列表中较高,即使它们的 z 坐标更近,它们也会先绘制,然后较小的卡片在它们上面绘制。

解决方案是使用 Flow() 小部件来动态更改卡片绘制的顺序。在我通常构建它们的子部件中,我将它们的转换存储在地图中。然后,在流委托函数中,访问这些变换,计算变换后每个小部件的 z 坐标(变换矩阵中的第 3 行,第 3 列(1 索引))。按 z 坐标对小部件进行排序,然后按该顺序在屏幕上绘制它们。

这是相同的代码:

带有根 Flow 小部件的部分:

@override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: widget.animationControllerX,
      builder: (context, _) {
        double rotateX =
            widget.animationControllerX.value / Constants.turnResolution;
        double rotateY =
            widget.animationControllerY.value / Constants.turnResolution;
        return Container(
          transform: Matrix4.identity()
            ..translate(
              MediaQuery.of(context).size.width / 2,
            ),
          child: Flow(
            clipBehavior: Clip.none,
            delegate: CustomFlowDelegate(rotateY: -rotateX, rotateX: rotateY),
            children: List<Widget>.generate(
              Constants.numberOfElements,
              (index) => ElementCard(
                atomicNumber:
                    Constants.elements[(index + 1).toString()].atomicNumber,
              ),
            ),
          ),
        );
      },
    );
  }

小部件的 FlowDelegate:

class CustomFlowDelegate extends FlowDelegate {
  CustomFlowDelegate({this.rotateX, this.rotateY});
  final rotateY, rotateX;
  @override
  void paintChildren(FlowPaintingContext context) {
    Map<int, double> zValue = {};
    for (int i = 0; i < context.childCount; i++) {
      var map = Constants.transformationsMap[i + 1];
      Matrix4 transformedMatrix = Matrix4.identity()
        ..setEntry(3, 2, 0.001)
        ..setEntry(3, 1, 0.001)
        ..rotateY(this.rotateY)
        ..rotateX(this.rotateX)
        ..translate(
            map['translateOneX'], map['translateOneY'], map['translateOneZ'])
        ..translate(map['translateTwoX'])
        ..rotateY(map['rotateThreeY'])
        ..translate(map['translateFourX']);
      zValue[i + 1] = transformedMatrix.getRow(2)[2];
    }
    var sortedKeys = zValue.keys.toList(growable: false)
      ..sort((k1, k2) => zValue[k1].compareTo(zValue[k2]));
    LinkedHashMap sortedMap = new LinkedHashMap.fromIterable(sortedKeys,
        key: (k) => k, value: (k) => zValue[k]);
    for (int key in sortedMap.keys) {
      var map = Constants.transformationsMap[key];
      context.paintChild(
        key - 1,
        transform: Matrix4.identity()
          ..setEntry(3, 2, 0.001)
          ..setEntry(3, 1, 0.001)
          ..rotateY(this.rotateY)
          ..rotateX(this.rotateX)
          ..translate(
              map['translateOneX'], map['translateOneY'], map['translateOneZ'])
          ..translate(map['translateTwoX'])
          ..rotateY(map['rotateThreeY'])
          ..translate(map['translateFourX']),
      );
    }
  }

  @override
  bool shouldRepaint(covariant FlowDelegate oldDelegate) {
    return true;
  }
}

结果如下:

稍微调高不透明度,这样您就可以真正看到修复的效果:

是的,我知道将转换存储在地图中然后从另一个页面对它们进行排序并不是最佳做法。

希望这对某人有所帮助。