首页 > 解决方案 > Flutter:从具有正确重叠的矩阵变换中布局具有不同 z 坐标的多个子级

问题描述

我正在尝试构建 Threejs 周期表 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()
          
        ),
      ),
    );

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

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 中的堆叠元素卡片

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

Flutter 中的堆叠元素卡片(不透明)

如您所见,较大的卡片更靠近屏幕,因为它们是这样转换的,但它们是在较小的卡片后面绘制的。关于如何实现这一点的任何想法?

标签: flutterdartflutter-layoutflutter-webflutter-animation

解决方案


所以我找到了解决方案。它并不完美,但到目前为止它仍然有效。

问题是 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;
  }
}

结果如下:

元素螺旋透明

随着不透明度的提高,你可以真正看到修复的效果:

元素螺旋不透明

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

希望这可以帮助某人。


推荐阅读