Flutter custom layout, decide widget's size based on its child

1,257

This should resolve your issue. We remove the constraints passed down by using the UnconstrainedBox. We only need to remove the vertical so that the AspectRatio uses the width constraint. Then we ask the sub-tree to be sized by its intrinsic size. This is, of course, now easy to overflow because it will never wrap and will always use its intrinsic width. However you can wrap the UnconstrainedBox in a FittedBox and it will never overflow and instead scale to fit a parent that might be smaller than it's intrinsic dimension. You might also want to try setting the axis parameter on the UnconstrainedBox to horizontal. Then the IntrinsicWidth will be constrained to the parent width.

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());
}

Share:
1,257
Gazihan Alankus
Author by

Gazihan Alankus

Flutter/Dart GDE, Assistant Professor, coder, wannabe entrepreneur.

Updated on December 22, 2022

Comments

  • Gazihan Alankus
    Gazihan Alankus over 1 year

    The understanding constraints article is very useful in understanding how layout works. However, I'm having trouble creating a custom layout that works the way it is explained there.

    The article says that size flows up, first we obtain the sizes of the children and then we can decide on our own size based on the sizes of the children.

    It seems CustomSingleChildLayout is the way to create a custom layout with one child. In it, based on the article I mentioned above, I would like to get my child's size, and then decide on my own size based on that. However, this seems to be impossible. getSize() method is called with the constraints, therefore I'm supposed to decide on my widgets size based on the constraints that I receive from above. I am able to obtain the child's size in getPositionForChild(), but this called after I give my size in getSize(). It made sense to trigger a relayout at this point, but it seems I'm not allowed to do that from a function called by the layout system.

    So, the question is, what's the best way to implement a custom layout, with the layout behavior of constraints flowing down and sizes flowing up, as explained in understanding constraints?

    Edit: Here is some code to explain what I really want.

    This is the look that I'm going for. Here, I'm manually supplying the width and height of the blue square. I don't want to do that. I want the child's size to determine the size of the square. I want the size to flow up from the text and the blue square should decide on an edge length of 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());
    }
    
    

    It was suggested that I could wrap it with Align. Here I tried, but Align grows up. Align's widthFactor and heightFactor make use of the child's size, but can't make it a square that way. Removing the blue Container has no effect:

    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());
    }
    
    

    Here I'm trying to use AspectRatio to make the square, but it does not care about the child's size and just grows:

    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());
    }
    
    

    And here I'm trying to do a custom layout, but I don't get to use the child's size to decide on my size. I have to decide on my size before receiving my child's. Trying to force a relayout once I learn my child's size throws an exception, which is at the end of the code. Codepen fails silently.

    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
    
     */