Flutter自定义布局,根据其子控件决定小部件的大小

9
了解约束一文对了解布局的工作原理非常有用。但是,我在创建按照这里所述方式工作的自定义布局时遇到了问题。
这篇文章说尺寸是向上流动的,首先我们获取子元素的大小,然后我们可以根据子元素的大小决定自己的大小。
看起来CustomSingleChildLayout是创建具有一个子元素的自定义布局的方法。在其中,基于我提到的文章,我想要获得我的子元素的大小,然后根据它来决定自己的大小。然而,这似乎是不可能的。getSize()方法以约束为参数调用,因此我应该根据从上面接收到的约束决定我的小部件的大小。我能够在getPositionForChild()中获取子元素的大小,但这是在我在getSize()中给出我的大小之后调用的。这时触发重新布局是有道理的,但似乎我不允许从布局系统调用的函数中这样做。
因此,问题是,按照了解约束中解释的约束向下流动和尺寸向上流动的布局行为,实现自定义布局的最佳方式是什么?
编辑:以下是一些代码,以解释我真正想要的内容。
这就是我要实现的外观。在这里,我手动提供了蓝色正方形的宽度和高度。我不想这么做。我希望子元素的大小确定正方形的大小。我希望大小从文本流动,蓝色正方形应该决定边长为max(width, height)https://codepen.io/gazialankus/pen/abdKyVR
import 'package:flutter/material.dart';

class AppRoot extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo', // Appears in app switcher
      theme: ThemeData(
        primaryColor: Colors.grey.shade300,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyPage(),
    );
  }
}

class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          color: Colors.yellow,
          child: SizedBox(
            width: 180,
            height: 180,
            child: Container(
              color: Colors.blue,
              child: AspectRatio(
                aspectRatio: 1,
                child: Text(
                  "I want to be in a tight square.",
                  style: TextStyle(
                    backgroundColor: Colors.red
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

void main() {
  runApp(AppRoot());
}


建议使用 Align 进行包装。我尝试过,但是 Align 会变大。 AlignwidthFactorheightFactor 利用了子元素的大小,但不能使其成为正方形。移除蓝色的 Container 没有影响。
参考链接:https://codepen.io/gazialankus/pen/MWKXvpO
import 'package:flutter/material.dart';

class AppRoot extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo', // Appears in app switcher
      theme: ThemeData(
        primaryColor: Colors.grey.shade300,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyPage(),
    );
  }
}

class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          color: Colors.yellow,
          child: Align(
            alignment: Alignment.center,
            child: Container(
              color: Colors.blue,
              child: Text(
                "I want to be in a square.",
                style: TextStyle(
                  backgroundColor: Colors.red
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

void main() {
  runApp(AppRoot());
}

在这里,我尝试使用 AspectRatio 来制作正方形,但它并不关心子元素的大小,只是单纯的扩大:

https://codepen.io/gazialankus/pen/KKVevXv

import 'package:flutter/material.dart';

class AppRoot extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo', // Appears in app switcher
      theme: ThemeData(
        primaryColor: Colors.grey.shade300,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyPage(),
    );
  }
}

class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          color: Colors.yellow,
          child: Align(
            alignment: Alignment.center,
            child: Container(
              color: Colors.blue,
              child: AspectRatio(
                aspectRatio: 1,
                child: Text(
                  "I want to be in a tight square.",
                  style: TextStyle(
                    backgroundColor: Colors.red
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

void main() {
  runApp(AppRoot());
}

我想要做一个自定义布局,但是我无法使用子元素的尺寸来确定我的尺寸。在接收到子元素之前,我必须决定我的尺寸。尝试在了解子元素的尺寸后强制重新布局会引发异常,代码末尾有详细信息。Codepen会默默失败。

https://codepen.io/gazialankus/pen/LYGrjxM

import 'package:flutter/material.dart';

class AppRoot extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo', // Appears in app switcher
      theme: ThemeData(
        primaryColor: Colors.grey.shade300,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyPage(),
    );
  }
}

class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          color: Colors.yellow,
          child: CustomSingleChildLayout(
            delegate: MyDelegate(relayout: ValueNotifier<int>(0)),
            child: Text(
              "I want to be in a square.",
              style: TextStyle(
                backgroundColor: Colors.red
              ),
            ),
          ),
        ),
      ),
    );
  }
}


class MyDelegate extends SingleChildLayoutDelegate {
  ValueNotifier<int> relayout;
  MyDelegate({ this.relayout }) : super(relayout: relayout);

  @override
  bool shouldRelayout(SingleChildLayoutDelegate oldDelegate) {
    return desiredWidth == 100;
  }

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    print('getConstraintsForChild');
    return super.getConstraintsForChild(constraints);
  }

  double desiredWidth = 300;

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    print('getPositionForChild');
    print(size);
    print(childSize);
    if (size.width > childSize.width) {
      desiredWidth = childSize.width;
      relayout.value++;
      // ^trying to force a relayout
      // throws exception, output is given at the bottom in comments
      print("RELAYOUT");
    }
    return super.getPositionForChild(size, childSize);
  }

  @override
  Size getSize(BoxConstraints constraints) {
    print('getSize');
    return Size(desiredWidth, 100);
  }
}

void main() {
  runApp(AppRoot());
}

/*
Performing hot reload...
Syncing files to device LG H990...
I/flutter (19850): getSize
I/flutter (19850): getConstraintsForChild
I/flutter (19850): getPositionForChild
I/flutter (19850): Size(300.0, 100.0)
I/flutter (19850): Size(148.0, 16.0)

════════ Exception caught by foundation library ════════════════════════════════════════════════════
The following assertion was thrown while dispatching notifications for ValueNotifier<int>:
'package:flutter/src/rendering/object.dart': Failed assertion: line 1527 pos 12: '_debugCanPerformMutations': is not true.


Either the assertion indicates an error in the framework itself, or we should provide substantially more information in this error message to help you determine and fix the underlying cause.
In either case, please report this assertion by filing a bug on GitHub:
  https://github.com/flutter/flutter/issues/new?template=BUG.md

When the exception was thrown, this was the stack: 
#2      RenderObject.markNeedsLayout (package:flutter/src/rendering/object.dart:1527:12)
#3      RenderBox.markNeedsLayout (package:flutter/src/rendering/box.dart:2053:11)
#4      ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:207:21)
#5      ValueNotifier.value= (package:flutter/src/foundation/change_notifier.dart:274:5)
#6      MyDelegate.getPositionForChild (package:m_health/ui/app_root.dart:64:16)
...
The ValueNotifier<int> sending notification was: ValueNotifier<int>#ac2fb(1)
════════════════════════════════════════════════════════════════════════════════════════════════════
Reloaded 2 of 522 libraries in 1,390ms.
I/flutter (19850): RELAYOUT

 */
2个回答

9

这应该可以解决您的问题。我们使用UnconstrainedBox来移除传递下来的约束条件,只需要移除垂直方向上的约束,以便AspectRatio使用宽度约束条件。然后要求子树按其固有大小调整大小。当然,这很容易溢出,因为它永远不会换行,并且总是使用其固有宽度,但是您可以将UnconstrainedBox包装在FittedBox中,这样它就永远不会溢出,而是缩放以适应可能小于其固有尺寸的父级。您可能还想尝试在UnconstrainedBox上设置轴参数为水平。然后,IntrinsicWidth将受到父宽度的限制。

import 'package:flutter/material.dart';

class AppRoot extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo', // Appears in app switcher
      theme: ThemeData(
        primaryColor: Colors.grey.shade300,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyPage(),
    );
  }
}

class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          color: Colors.yellow,
          child: UnconstrainedBox(
            child: IntrinsicWidth(
              child: Container(
                color: Colors.blue,
                child: AspectRatio(
                  aspectRatio: 1,
                  child: Text(
                    "I want to be in a tight square.",
                    style: TextStyle(backgroundColor: Colors.red),
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

void main() {
  runApp(AppRoot());
}


1
不错!谢谢。很好的 UnconstrainedBoxIntrinsicWidth 结合,肯定解决了我的问题。等我有时间,我会尝试跟进我的原始任务,并想出一种类似于“理解约束”文章的创建自定义小部件的方法。 - Gazihan Alankus

3

多年以后...

Boxy库可以帮助您构建“尺寸向上流动”的小部件。请参见下面的简单示例,它可以缩放小部件并使其在布局中占用更多/更少的空间。

如果您对第三方库过敏,并且需要此功能,则不幸的是,您必须实现一堆似乎很困难的低级内容。它不在内置库中。

import 'package:boxy/boxy.dart';

class Zoom extends StatelessWidget {
  const Zoom({required this.child, required this.scale, Key? key})
      : super(key: key);

  final Widget child;
  final double scale;

  @override
  Widget build(BuildContext context) {
    return CustomBoxy(delegate: ZoomLayoutDelegate(scale), children: [
      Transform.scale(scale: scale, alignment: Alignment.topLeft, child: child)
    ]);
  }
}

class ZoomLayoutDelegate extends BoxyDelegate {
  ZoomLayoutDelegate(this.scale);

  double scale;

  @override
  Size layout() {

    // Get Reference To Child
    var child = getChild(0);

    // Lay out the child with the incoming constraints
    var invScale = 1 / scale;
    var childConstraints = BoxConstraints(
        maxHeight: constraints.maxHeight * invScale,
        minHeight: constraints.minHeight * invScale,
        maxWidth: constraints.maxWidth * invScale,
        minWidth: constraints.minWidth * invScale);
    var childSize = child.layout(childConstraints);
    child.position(Offset.zero);

    // Return the size of this widget which is based on
    // the child's size
    return Size(
      childSize.width * scale, childSize.height * scale
    );
  }
}

1
哇,太棒了,我真的很感激你的回答。我找到时间后会试着操作一下。 - Gazihan Alankus
1
那是我的包!顺便说一下,你可以直接使用比例将BoxConstraints分割,而不必为每个字段手动计算。 - PixelToast
@PixelToast 喜欢这个库,感谢你的编写和分享。 - PeterM

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接