[Story of Your Layout] Visualize main axis spaces (#1378) * Visualize main axis spaces * Refactor class name and remove commented code * Remove before/after space and mix it into leadingSpace * turn off experimental flag * update goldens
diff --git a/packages/devtools_app/lib/src/inspector/flutter/inspector_data_models.dart b/packages/devtools_app/lib/src/inspector/flutter/inspector_data_models.dart index 65de1b5..b51db20 100644 --- a/packages/devtools_app/lib/src/inspector/flutter/inspector_data_models.dart +++ b/packages/devtools_app/lib/src/inspector/flutter/inspector_data_models.dart
@@ -257,16 +257,55 @@ double get crossAxisDimension => dimension(crossAxisDirection); - List<Size> childrenRenderSizes({ + /// render properties for laying out rendered Flex & Flex children widgets + /// the computation is similar to [RenderFlex].performLayout() method + List<RenderProperties> childrenRenderProperties({ @required double smallestRenderWidth, @required double largestRenderWidth, @required double smallestRenderHeight, @required double largestRenderHeight, - @required double maxWidthAvailable, - @required double maxHeightAvailable, + @required double Function(Axis) maxSizeAvailable, }) { - double maxSizeAvailable(Axis axis) { - return axis == Axis.horizontal ? maxWidthAvailable : maxHeightAvailable; + /// calculate the render empty spaces + final freeSpace = dimension(direction) - sum(childrenDimensions(direction)); + + double leadingSpace(double freeSpace) { + if (children.isEmpty) return 0.0; + switch (mainAxisAlignment) { + case MainAxisAlignment.start: + case MainAxisAlignment.end: + return freeSpace; + case MainAxisAlignment.center: + return freeSpace * 0.5; + case MainAxisAlignment.spaceBetween: + return 0.0; + case MainAxisAlignment.spaceAround: + final spaceBetweenChildren = freeSpace / children.length; + return spaceBetweenChildren * 0.5; + case MainAxisAlignment.spaceEvenly: + return freeSpace / (children.length + 1); + default: + return 0.0; + } + } + + double betweenSpace(double freeSpace) { + if (children.isEmpty) return 0.0; + switch (mainAxisAlignment) { + case MainAxisAlignment.start: + case MainAxisAlignment.end: + case MainAxisAlignment.center: + return 0.0; + case MainAxisAlignment.spaceBetween: + if (children.length == 1) return freeSpace; + return freeSpace / (children.length - 1); + case MainAxisAlignment.spaceAround: + return freeSpace / children.length; + case MainAxisAlignment.spaceEvenly: + return freeSpace / (children.length + 1); + default: + return 0.0; + } } double smallestRenderSize(Axis axis) { @@ -284,6 +323,10 @@ List<double> renderSizes(Axis axis) { final sizes = childrenDimensions(axis); + if (freeSpace > 0.0 && axis == direction) { + /// include free space in the computation + sizes.add(freeSpace); + } final smallestSize = min(sizes); final largestSize = max(sizes); if (axis == direction || @@ -308,93 +351,117 @@ final widths = renderSizes(Axis.horizontal); final heights = renderSizes(Axis.vertical); - assert(widths.length == heights.length); - return [ - for (var i = 0; i < widths.length; ++i) Size(widths[i], heights[i]) - ]; - } - double _calculateCrossAxisOffset(double maxDimension, double usedDimension) { - if (crossAxisAlignment == CrossAxisAlignment.start || - crossAxisAlignment == CrossAxisAlignment.stretch || - maxDimension == usedDimension) return 0.0; - final emptySpace = math.max(0.0, maxDimension - usedDimension); - if (crossAxisAlignment == CrossAxisAlignment.end) return emptySpace; - return emptySpace * 0.5; - } + final renderFreeSpace = freeSpace > 0.0 + ? (isMainAxisHorizontal ? widths.last : heights.last) + : 0.0; - List<Offset> childrenRenderOffsets({ - @required List<Size> childrenRenderSizes, - @required double maxWidthAvailable, - @required double maxHeightAvailable, - }) { - final offsets = <Offset>[]; - for (var i = 0; i < children.length; ++i) { - double dx, dy; - if (direction == Axis.horizontal) { - dx = i == 0 ? 0.0 : offsets.last.dx + childrenRenderSizes[i - 1].width; - dy = _calculateCrossAxisOffset( - maxHeightAvailable, childrenRenderSizes[i].height); - } else { - dy = i == 0 ? 0.0 : offsets.last.dy + childrenRenderSizes[i - 1].height; - dx = _calculateCrossAxisOffset( - maxWidthAvailable, childrenRenderSizes[i].width); - } - offsets.add(Offset(dx, dy)); + final renderLeadingSpace = leadingSpace(renderFreeSpace); + final renderBetweenSpace = betweenSpace(renderFreeSpace); + + final childrenRenderProps = <RenderProperties>[]; + + double lastMainAxisOffset() { + if (childrenRenderProps.isEmpty) return 0.0; + return childrenRenderProps.last.mainAxisOffset; } - return offsets; + + double lastMainAxisDimension() { + if (childrenRenderProps.isEmpty) return 0.0; + return childrenRenderProps.last.mainAxisDimension; + } + + double space(int index) { + if (index == 0) { + if (mainAxisAlignment == MainAxisAlignment.start) return 0.0; + return renderLeadingSpace; + } + return renderBetweenSpace; + } + + double calculateMainAxisOffset(int i) { + return lastMainAxisOffset() + lastMainAxisDimension() + space(i); + } + + double calculateCrossAxisOffset(int i) { + final maxDimension = maxSizeAvailable(crossAxisDirection); + final usedDimension = + crossAxisDirection == Axis.horizontal ? widths[i] : heights[i]; + + if (crossAxisAlignment == CrossAxisAlignment.start || + crossAxisAlignment == CrossAxisAlignment.stretch || + maxDimension == usedDimension) return 0.0; + final emptySpace = math.max(0.0, maxDimension - usedDimension); + if (crossAxisAlignment == CrossAxisAlignment.end) return emptySpace; + return emptySpace * 0.5; + } + + for (var i = 0; i < children.length; ++i) { + childrenRenderProps.add( + RenderProperties( + axis: direction, + size: Size(widths[i], heights[i]), + offset: Offset.zero, + realSize: children[i].size, + ) + ..mainAxisOffset = calculateMainAxisOffset(i) + ..crossAxisOffset = calculateCrossAxisOffset(i), + ); + } + + final spaces = <RenderProperties>[]; + final actualLeadingSpace = leadingSpace(freeSpace); + final actualBetweenSpace = betweenSpace(freeSpace); + final renderPropsWithFullCrossAxisDimension = + RenderProperties(axis: direction, isFreeSpace: true) + ..crossAxisDimension = maxSizeAvailable(crossAxisDirection) + ..crossAxisRealDimension = dimension(crossAxisDirection) + ..crossAxisOffset = 0.0; + if (actualLeadingSpace > 0.0 && + mainAxisAlignment != MainAxisAlignment.start) { + spaces.add(renderPropsWithFullCrossAxisDimension.clone() + ..mainAxisOffset = 0.0 + ..mainAxisDimension = renderLeadingSpace + ..mainAxisRealDimension = actualLeadingSpace); + } + if (actualBetweenSpace > 0.0) + for (var i = 0; i < childrenRenderProps.length - 1; ++i) { + final child = childrenRenderProps[i]; + spaces.add(renderPropsWithFullCrossAxisDimension.clone() + ..mainAxisDimension = renderBetweenSpace + ..mainAxisRealDimension = actualBetweenSpace + ..mainAxisOffset = child.mainAxisOffset + child.mainAxisDimension); + } + if (actualLeadingSpace > 0.0 && + mainAxisAlignment != MainAxisAlignment.end) { + spaces.add(renderPropsWithFullCrossAxisDimension.clone() + ..mainAxisOffset = childrenRenderProps.last.mainAxisDimension + + childrenRenderProps.last.mainAxisOffset + ..mainAxisDimension = renderLeadingSpace + ..mainAxisRealDimension = actualLeadingSpace); + } + return [...childrenRenderProps, ...spaces]; } - List<RenderInfo> childrenRenderInformation({ - @required double smallestRenderWidth, - @required double largestRenderWidth, - @required double smallestRenderHeight, - @required double largestRenderHeight, - @required double maxWidthAvailable, - @required double maxHeightAvailable, - }) { - final renderSizes = childrenRenderSizes( - smallestRenderWidth: smallestRenderWidth, - largestRenderWidth: largestRenderWidth, - smallestRenderHeight: smallestRenderHeight, - largestRenderHeight: largestRenderHeight, - maxWidthAvailable: maxWidthAvailable, - maxHeightAvailable: maxHeightAvailable, - ); - final renderOffsets = childrenRenderOffsets( - childrenRenderSizes: renderSizes, - maxWidthAvailable: maxWidthAvailable, - maxHeightAvailable: maxHeightAvailable, - ); - return [ - for (var i = 0; i < children.length; ++i) - RenderInfo( - direction, renderSizes[i], renderOffsets[i], children[i].size) - ]; - } - - List<RenderInfo> crossAxisSpaces({ - List<RenderInfo> childrenRenderInfo, - double maxWidthAvailable, - double maxHeightAvailable, + List<RenderProperties> crossAxisSpaces({ + @required List<RenderProperties> childrenRenderProps, + @required double Function(Axis) maxSizeAvailable, }) { if (crossAxisAlignment == CrossAxisAlignment.stretch) return []; - final spaces = <RenderInfo>[]; - final maxSizeAvailable = crossAxisDirection == Axis.horizontal - ? maxWidthAvailable - : maxHeightAvailable; + final spaces = <RenderProperties>[]; for (var i = 0; i < children.length; ++i) { if (dimension(crossAxisDirection) == children[i].dimension(crossAxisDirection) || - childrenRenderInfo[i].crossAxisDimension == maxSizeAvailable) - continue; + childrenRenderProps[i].crossAxisDimension == + maxSizeAvailable(crossAxisDirection)) continue; - final renderInfo = childrenRenderInfo[i]; + final renderInfo = childrenRenderProps[i]; final space = renderInfo.clone(); space.crossAxisRealDimension = crossAxisDimension - space.crossAxisRealDimension; - space.crossAxisDimension = maxSizeAvailable - space.crossAxisDimension; + space.crossAxisDimension = + maxSizeAvailable(crossAxisDirection) - space.crossAxisDimension; if (crossAxisAlignment == CrossAxisAlignment.center) { space.crossAxisDimension *= 0.5; @@ -428,17 +495,23 @@ EnumUtils<TextBaseline>(TextBaseline.values); } -class RenderInfo { - RenderInfo(this.axis, Size size, Offset offset, Size realSize) - : width = size.width, - height = size.height, - realWidth = realSize.width, - realHeight = realSize.height, - dx = offset.dx, - dy = offset.dy; +class RenderProperties { + RenderProperties({ + @required this.axis, + Size size, + Offset offset, + Size realSize, + this.isFreeSpace = false, + }) : width = size?.width, + height = size?.height, + realWidth = realSize?.width, + realHeight = realSize?.height, + dx = offset?.dx, + dy = offset?.dy; final Axis axis; + bool isFreeSpace; double dx, dy; double width, height; double realWidth, realHeight; @@ -505,7 +578,13 @@ realWidth = newVal; } - RenderInfo clone() { - return RenderInfo(axis, size, offset, realSize); + RenderProperties clone() { + return RenderProperties( + axis: axis, + size: size, + offset: offset, + realSize: realSize, + isFreeSpace: isFreeSpace, + ); } }
diff --git a/packages/devtools_app/lib/src/inspector/flutter/story_of_your_layout/flex.dart b/packages/devtools_app/lib/src/inspector/flutter/story_of_your_layout/flex.dart index 46fb672..2a2a11b 100644 --- a/packages/devtools_app/lib/src/inspector/flutter/story_of_your_layout/flex.dart +++ b/packages/devtools_app/lib/src/inspector/flutter/story_of_your_layout/flex.dart
@@ -25,7 +25,7 @@ const arrowStrokeWidth = 1.5; /// Hardcoded sizes for scaling the flex children widget properly. -const minRenderWidth = 225.0; +const minRenderWidth = 215.0; const minRenderHeight = 275.0; const defaultMaxRenderWidth = 300.0; const defaultMaxRenderHeight = 300.0; @@ -74,6 +74,8 @@ const crossAxisTextColor = ThemedColor(crossAxisLightTextColor, crossAxisDarkTextColor); +const freeSpaceAssetName = 'assets/img/story_of_layout/empty_space.png'; + class StoryOfYourFlexWidget extends StatefulWidget { const StoryOfYourFlexWidget( this.properties, { @@ -280,16 +282,31 @@ final maxWidth = constraints.maxWidth; final maxHeight = constraints.maxHeight; - final renderInfo = properties.childrenRenderInformation( + double maxSizeAvailable(Axis axis) { + return axis == Axis.horizontal ? maxWidth : maxHeight; + } + + final childrenAndMainAxisSpacesRenderProps = + properties.childrenRenderProperties( smallestRenderWidth: minRenderWidth, largestRenderWidth: defaultMaxRenderWidth, smallestRenderHeight: minRenderHeight, largestRenderHeight: defaultMaxRenderHeight, - maxWidthAvailable: maxWidth, - maxHeightAvailable: maxHeight, + maxSizeAvailable: maxSizeAvailable, ); - final widgetChildren = <Widget>[ + final renderProps = childrenAndMainAxisSpacesRenderProps + .where((renderProps) => !renderProps.isFreeSpace) + .toList(); + final mainAxisSpaces = childrenAndMainAxisSpacesRenderProps + .where((renderProps) => renderProps.isFreeSpace) + .toList(); + final crossAxisSpaces = properties.crossAxisSpaces( + childrenRenderProps: renderProps, + maxSizeAvailable: maxSizeAvailable, + ); + + final childrenRenderWidgets = <Widget>[ for (var i = 0; i < children.length; i++) _visualizeChild( backgroundColor: @@ -299,25 +316,24 @@ childProperties: children[i], borderColor: i.isOdd ? mainAxisColor : crossAxisColor, textColor: i.isOdd ? null : const Color(0xFF303030), - renderSize: renderInfo[i].size, - renderOffset: renderInfo[i].offset, + renderSize: renderProps[i].size, + renderOffset: renderProps[i].offset, ) ]; - final crossAxisSpaces = <Widget>[ - for (var spaceRenderInfo in properties.crossAxisSpaces( - childrenRenderInfo: renderInfo, - maxWidthAvailable: maxWidth, - maxHeightAvailable: maxHeight, - )) + final freeSpacesWidgets = <Widget>[ + for (var renderProperties in [ + ...mainAxisSpaces, + ...crossAxisSpaces + ]) Positioned( - top: spaceRenderInfo.dy, - left: spaceRenderInfo.dx, + top: renderProperties.dy, + left: renderProperties.dx, child: EmptySpaceVisualizerWidget( - width: spaceRenderInfo.realWidth, - height: spaceRenderInfo.realHeight, - renderWidth: spaceRenderInfo.width, - renderHeight: spaceRenderInfo.height, + width: renderProperties.realWidth, + height: renderProperties.realHeight, + renderWidth: renderProperties.width, + renderHeight: renderProperties.height, ), ) ]; @@ -329,14 +345,27 @@ minWidth: maxWidth, minHeight: maxHeight, maxWidth: direction == Axis.horizontal - ? sum(renderInfo.map((renderSize) => renderSize.width)) + ? sum(childrenAndMainAxisSpacesRenderProps + .map((renderSize) => renderSize.width)) : maxWidth, maxHeight: direction == Axis.vertical - ? sum(renderInfo.map((renderSize) => renderSize.height)) + ? sum(childrenAndMainAxisSpacesRenderProps + .map((renderSize) => renderSize.height)) : maxHeight, ), child: Stack( - children: [...widgetChildren, ...crossAxisSpaces], + children: [ + Positioned.fill( + child: Image.asset( + freeSpaceAssetName, + width: maxWidth, + height: maxHeight, + fit: BoxFit.fill, + ), + ), + ...childrenRenderWidgets, + ...freeSpacesWidgets + ], ), ), ); @@ -546,6 +575,7 @@ quarterTurns: 3, child: Text( properties.verticalDirectionDescription, + overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, textScaleFactor: largeTextScaleFactor, style: @@ -574,6 +604,7 @@ child: FittedBox( child: Text( properties.horizontalDirectionDescription, + overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, textScaleFactor: largeTextScaleFactor, style: @@ -677,12 +708,14 @@ ), color: backgroundColor, ), - margin: const EdgeInsets.all(1.0), ); } } class EmptySpaceVisualizerWidget extends StatelessWidget { + // width and height to be displayed on Text + // width and height for rendering/sizing the widget + const EmptySpaceVisualizerWidget({ Key key, @required this.width, @@ -691,16 +724,11 @@ @required this.renderHeight, }) : super(key: key); - // width and height to be displayed on Text final double width; final double height; - - // width and height for rendering/sizing the widget final double renderWidth; final double renderHeight; - static const assetName = 'assets/img/story_of_layout/empty_space.png'; - @override Widget build(BuildContext context) { return Container( @@ -708,14 +736,6 @@ height: renderHeight, child: Stack( children: <Widget>[ - Positioned.fill( - child: Image.asset( - assetName, - width: renderWidth, - height: renderHeight, - fit: BoxFit.fill, - ), - ), Align( alignment: Alignment.bottomCenter, child: Container( @@ -744,6 +764,7 @@ quarterTurns: 1, child: Text( 'h=${toStringAsFixed(height)}', + overflow: TextOverflow.ellipsis, ), ), arrowColor: heightIndicatorColor,
diff --git a/packages/devtools_app/pubspec.yaml b/packages/devtools_app/pubspec.yaml index 1a68ccc..dc3bc41 100644 --- a/packages/devtools_app/pubspec.yaml +++ b/packages/devtools_app/pubspec.yaml
@@ -98,18 +98,9 @@ - web/icons/inspector/ - web/icons/memory/ - web/icons/perf/ - - assets/img/story_of_layout/main_axis_alignment/center.png - - assets/img/story_of_layout/main_axis_alignment/end.png - - assets/img/story_of_layout/main_axis_alignment/spaceAround.png - - assets/img/story_of_layout/main_axis_alignment/spaceBetween.png - - assets/img/story_of_layout/main_axis_alignment/spaceEvenly.png - - assets/img/story_of_layout/main_axis_alignment/start.png - - assets/img/story_of_layout/cross_axis_alignment/start.png - - assets/img/story_of_layout/cross_axis_alignment/end.png - - assets/img/story_of_layout/cross_axis_alignment/center.png - - assets/img/story_of_layout/cross_axis_alignment/stretch.png - - assets/img/story_of_layout/cross_axis_alignment/baseline.png - - assets/img/story_of_layout/empty_space.png + - assets/img/story_of_layout/ + - assets/img/story_of_layout/main_axis_alignment/ + - assets/img/story_of_layout/cross_axis_alignment/ # See https://github.com/flutter/flutter/wiki/Desktop-shells#fonts # TODO(https://github.com/flutter/devtools/issues/1339): Include a monospaced font.
diff --git a/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_column_layout.png b/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_column_layout.png index a7295c0..eb62a60 100644 --- a/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_column_layout.png +++ b/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_column_layout.png Binary files differ
diff --git a/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_row_layout.png b/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_row_layout.png index fcb9763..1332cd9 100644 --- a/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_row_layout.png +++ b/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_row_layout.png Binary files differ