diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart index 721fbb8c29..33f9e2b0bf 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -1855,7 +1855,7 @@ class _RenderChip extends RenderBox { @override double computeDistanceToActualBaseline(TextBaseline baseline) { // The baseline of this widget is the baseline of the label. - return label.computeDistanceToActualBaseline(baseline); + return label.getDistanceToActualBaseline(baseline); } Size _layoutLabel(double iconSizes, Size size) { diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index 672011a700..3caace3560 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -537,10 +537,15 @@ class _RenderDecorationLayout { // The workhorse: layout and paint a _Decorator widget's _Decoration. class _RenderDecoration extends RenderBox { _RenderDecoration({ - _Decoration decoration, - TextDirection textDirection, - }) : _decoration = decoration, - _textDirection = textDirection; + @required _Decoration decoration, + @required TextDirection textDirection, + @required TextBaseline textBaseline, + }) : assert(decoration != null), + assert(textDirection != null), + assert(textBaseline != null), + _decoration = decoration, + _textDirection = textDirection, + _textBaseline = textBaseline; final Map<_DecorationSlot, RenderBox> slotToChild = <_DecorationSlot, RenderBox>{}; final Map childToSlot = {}; @@ -654,6 +659,7 @@ class _RenderDecoration extends RenderBox { _Decoration get decoration => _decoration; _Decoration _decoration; set decoration(_Decoration value) { + assert(value != null); if (_decoration == value) return; _decoration = value; @@ -663,12 +669,23 @@ class _RenderDecoration extends RenderBox { TextDirection get textDirection => _textDirection; TextDirection _textDirection; set textDirection(TextDirection value) { + assert(value != null); if (_textDirection == value) return; _textDirection = value; markNeedsLayout(); } + TextBaseline get textBaseline => _textBaseline; + TextBaseline _textBaseline; + set textBaseline(TextBaseline value) { + assert(value != null); + if (_textBaseline == value) + return; + _textBaseline = value; + markNeedsLayout(); + } + @override void attach(PipelineOwner owner) { super.attach(owner); @@ -748,7 +765,7 @@ class _RenderDecoration extends RenderBox { if (box == null) return; box.layout(boxConstraints, parentUsesSize: true); - final double baseline = box.getDistanceToBaseline(TextBaseline.alphabetic); + final double baseline = box.getDistanceToBaseline(textBaseline); assert(baseline != null && baseline >= 0.0); boxToBaseline[box] = baseline; aboveBaseline = math.max(baseline, aboveBaseline); @@ -913,7 +930,15 @@ class _RenderDecoration extends RenderBox { width: overallWidth - _boxSize(icon).width, ); container.layout(containerConstraints, parentUsesSize: true); - final double x = textDirection == TextDirection.rtl ? 0.0 : _boxSize(icon).width; + double x; + switch (textDirection) { + case TextDirection.rtl: + x = 0.0; + break; + case TextDirection.ltr: + x = _boxSize(icon).width; + break; + } _boxParentData(container).offset = new Offset(x, 0.0); } @@ -938,7 +963,15 @@ class _RenderDecoration extends RenderBox { : layout.outlineBaseline; if (icon != null) { - final double x = textDirection == TextDirection.rtl ? overallWidth - icon.size.width : 0.0; + double x; + switch (textDirection) { + case TextDirection.rtl: + x = overallWidth - icon.size.width; + break; + case TextDirection.ltr: + x = 0.0; + break; + } centerLayout(icon, x); } @@ -1004,9 +1037,14 @@ class _RenderDecoration extends RenderBox { } if (label != null) { - decoration.borderGap.start = textDirection == TextDirection.rtl - ? _boxParentData(label).offset.dx + label.size.width - : _boxParentData(label).offset.dx; + switch (textDirection) { + case TextDirection.rtl: + decoration.borderGap.start = _boxParentData(label).offset.dx + label.size.width; + break; + case TextDirection.ltr: + decoration.borderGap.start = _boxParentData(label).offset.dx; + break; + } decoration.borderGap.extent = label.size.width * 0.75; } else { decoration.borderGap.start = null; @@ -1039,9 +1077,15 @@ class _RenderDecoration extends RenderBox { final bool isOutlineBorder = decoration.border != null && decoration.border.isOutline; final double floatingY = isOutlineBorder ? -labelHeight * 0.25 : contentPadding.top; final double scale = lerpDouble(1.0, 0.75, t); - final double dx = textDirection == TextDirection.rtl - ? labelOffset.dx + label.size.width * (1.0 - scale) // origin is on the right - : labelOffset.dx; // origin on the left + double dx; + switch (textDirection) { + case TextDirection.rtl: + dx = labelOffset.dx + label.size.width * (1.0 - scale); // origin is on the right + break; + case TextDirection.ltr: + dx = labelOffset.dx; // origin on the left + break; + } final double dy = lerpDouble(0.0, floatingY - labelOffset.dy, t); _labelTransform = new Matrix4.identity() ..translate(dx, labelOffset.dy + dy) @@ -1237,10 +1281,17 @@ class _RenderDecorationElement extends RenderObjectElement { class _Decorator extends RenderObjectWidget { const _Decorator({ Key key, - this.decoration, - }) : super(key: key); + @required this.decoration, + @required this.textDirection, + @required this.textBaseline, + }) : assert(decoration != null), + assert(textDirection != null), + assert(textBaseline != null), + super(key: key); final _Decoration decoration; + final TextDirection textDirection; + final TextBaseline textBaseline; @override _RenderDecorationElement createElement() => new _RenderDecorationElement(this); @@ -1249,7 +1300,8 @@ class _Decorator extends RenderObjectWidget { _RenderDecoration createRenderObject(BuildContext context) { return new _RenderDecoration( decoration: decoration, - textDirection: Directionality.of(context), + textDirection: textDirection, + textBaseline: textBaseline, ); } @@ -1257,7 +1309,8 @@ class _Decorator extends RenderObjectWidget { void updateRenderObject(BuildContext context, _RenderDecoration renderObject) { renderObject ..decoration = decoration - ..textDirection = Directionality.of(context); + ..textDirection = textDirection + ..textBaseline = textBaseline; } } @@ -1310,6 +1363,9 @@ class InputDecorator extends StatefulWidget { /// /// If null, `baseStyle` defaults to the `subhead` style from the /// current [Theme], see [ThemeData.textTheme]. + /// + /// The [TextStyle.textBaseline] of the [baseStyle] is used to determine + /// the baseline used for text alignment. final TextStyle baseStyle; /// How the text in the decoration should be aligned horizontally. @@ -1539,6 +1595,7 @@ class _InputDecoratorState extends State with TickerProviderStat Widget build(BuildContext context) { final ThemeData themeData = Theme.of(context); final TextStyle inlineStyle = _getInlineStyle(themeData); + final TextBaseline textBaseline = inlineStyle.textBaseline; final TextStyle hintStyle = inlineStyle.merge(decoration.hintStyle); final Widget hint = decoration.hintText == null ? null : new AnimatedOpacity( @@ -1713,6 +1770,8 @@ class _InputDecoratorState extends State with TickerProviderStat counter: counter, container: container, ), + textDirection: textDirection, + textBaseline: textBaseline, ); } } diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index f87d0ed985..82ef80c291 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -420,16 +420,19 @@ class ListTile extends StatelessWidget { ); } + final TextStyle titleStyle = _titleTextStyle(theme, tileTheme); final Widget titleText = new AnimatedDefaultTextStyle( - style: _titleTextStyle(theme, tileTheme), + style: titleStyle, duration: kThemeChangeDuration, child: title ?? const SizedBox() ); Widget subtitleText; + TextStyle subtitleStyle; if (subtitle != null) { + subtitleStyle = _subtitleTextStyle(theme, tileTheme); subtitleText = new AnimatedDefaultTextStyle( - style: _subtitleTextStyle(theme, tileTheme), + style: subtitleStyle, duration: kThemeChangeDuration, child: subtitle, ); @@ -466,6 +469,9 @@ class ListTile extends StatelessWidget { trailing: trailingIcon, isDense: _isDenseLayout(tileTheme), isThreeLine: isThreeLine, + textDirection: textDirection, + titleBaselineType: titleStyle.textBaseline, + subtitleBaselineType: subtitleStyle?.textBaseline, ), ), ), @@ -473,45 +479,6 @@ class ListTile extends StatelessWidget { } } -class _ListTile extends RenderObjectWidget { - const _ListTile({ - Key key, - this.leading, - this.title, - this.subtitle, - this.trailing, - this.isThreeLine, - this.isDense, - }) : super(key: key); - - final Widget leading; - final Widget title; - final Widget subtitle; - final Widget trailing; - final bool isThreeLine; - final bool isDense; - - @override - _RenderListTileElement createElement() => new _RenderListTileElement(this); - - @override - _RenderListTile createRenderObject(BuildContext context) { - return new _RenderListTile( - isThreeLine: isThreeLine, - isDense: isDense, - textDirection: Directionality.of(context), - ); - } - - @override - void updateRenderObject(BuildContext context, _RenderListTile renderObject) { - renderObject - ..isThreeLine = isThreeLine - ..isDense = isDense - ..textDirection = Directionality.of(context); - } -} - // Identifies the children of a _ListTileElement. enum _ListTileSlot { leading, @@ -520,348 +487,61 @@ enum _ListTileSlot { trailing, } -class _RenderListTile extends RenderBox { - _RenderListTile({ - bool isDense, - bool isThreeLine, - TextDirection textDirection, - }) : _isDense = isDense, - _isThreeLine = isThreeLine, - _textDirection = textDirection; +class _ListTile extends RenderObjectWidget { + const _ListTile({ + Key key, + this.leading, + this.title, + this.subtitle, + this.trailing, + @required this.isThreeLine, + @required this.isDense, + @required this.textDirection, + @required this.titleBaselineType, + this.subtitleBaselineType, + }) : assert(isThreeLine != null), + assert(isDense != null), + assert(textDirection != null), + assert(titleBaselineType != null), + super(key: key); - static const double _minLeadingWidth = 40.0; - // The horizontal gap between the titles and the leading/trailing widgets - static const double _horizontalTitleGap = 16.0; - // The minimum padding on the top and bottom of the title and subtitle widgets. - static const double _minVerticalPadding = 4.0; - - final Map<_ListTileSlot, RenderBox> slotToChild = <_ListTileSlot, RenderBox>{}; - final Map childToSlot = {}; - - RenderBox _updateChild(RenderBox oldChild, RenderBox newChild, _ListTileSlot slot) { - if (oldChild != null) { - dropChild(oldChild); - childToSlot.remove(oldChild); - slotToChild.remove(slot); - } - if (newChild != null) { - childToSlot[newChild] = slot; - slotToChild[slot] = newChild; - adoptChild(newChild); - } - return newChild; - } - - RenderBox _leading; - RenderBox get leading => _leading; - set leading(RenderBox value) { - _leading = _updateChild(_leading, value, _ListTileSlot.leading); - } - - RenderBox _title; - RenderBox get title => _title; - set title(RenderBox value) { - _title = _updateChild(_title, value, _ListTileSlot.title); - } - - RenderBox _subtitle; - RenderBox get subtitle => _subtitle; - set subtitle(RenderBox value) { - _subtitle = _updateChild(_subtitle, value, _ListTileSlot.subtitle); - } - - RenderBox _trailing; - RenderBox get trailing => _trailing; - set trailing(RenderBox value) { - _trailing = _updateChild(_trailing, value, _ListTileSlot.trailing); - } - - // The returned list is ordered for hit testing. - Iterable get _children sync *{ - if (leading != null) - yield leading; - if (title != null) - yield title; - if (subtitle != null) - yield subtitle; - if (trailing != null) - yield trailing; - } - - bool get isDense => _isDense; - bool _isDense; - set isDense(bool value) { - if (_isDense == value) - return; - _isDense = value; - markNeedsLayout(); - } - - bool get isThreeLine => _isThreeLine; - bool _isThreeLine; - set isThreeLine(bool value) { - if (_isThreeLine == value) - return; - _isThreeLine = value; - markNeedsLayout(); - } - - TextDirection get textDirection => _textDirection; - TextDirection _textDirection; - set textDirection(TextDirection value) { - if (_textDirection == value) - return; - _textDirection = value; - markNeedsLayout(); - } + final Widget leading; + final Widget title; + final Widget subtitle; + final Widget trailing; + final bool isThreeLine; + final bool isDense; + final TextDirection textDirection; + final TextBaseline titleBaselineType; + final TextBaseline subtitleBaselineType; @override - void attach(PipelineOwner owner) { - super.attach(owner); - for (RenderBox child in _children) - child.attach(owner); - } + _ListTileElement createElement() => new _ListTileElement(this); @override - void detach() { - super.detach(); - for (RenderBox child in _children) - child.detach(); - } - - @override - void redepthChildren() { - _children.forEach(redepthChild); - } - - @override - void visitChildren(RenderObjectVisitor visitor) { - _children.forEach(visitor); - } - - @override - List debugDescribeChildren() { - final List value = []; - void add(RenderBox child, String name) { - if (child != null) - value.add(child.toDiagnosticsNode(name: name)); - } - add(leading, 'leading'); - add(title, 'title'); - add(subtitle, 'subtitle'); - add(trailing, 'trailing'); - return value; - } - - @override - bool get sizedByParent => false; - - static double _minWidth(RenderBox box, double height) { - return box == null ? 0.0 : box.getMinIntrinsicWidth(height); - } - - static double _maxWidth(RenderBox box, double height) { - return box == null ? 0.0 : box.getMaxIntrinsicWidth(height); - } - - @override - double computeMinIntrinsicWidth(double height) { - final double leadingWidth = leading != null - ? math.max(leading.getMinIntrinsicWidth(height), _minLeadingWidth) + _horizontalTitleGap - : 0.0; - return leadingWidth - + math.max(_minWidth(title, height), _minWidth(subtitle, height)) - + _maxWidth(trailing, height); - } - - @override - double computeMaxIntrinsicWidth(double height) { - final double leadingWidth = leading != null - ? math.max(leading.getMaxIntrinsicWidth(height), _minLeadingWidth) + _horizontalTitleGap - : 0.0; - return leadingWidth - + math.max(_maxWidth(title, height), _maxWidth(subtitle, height)) - + _maxWidth(trailing, height); - } - - double get _defaultTileHeight { - final bool hasSubtitle = subtitle != null; - final bool isTwoLine = !isThreeLine && hasSubtitle; - final bool isOneLine = !isThreeLine && !hasSubtitle; - - if (isOneLine) - return isDense ? 48.0 : 56.0; - if (isTwoLine) - return isDense ? 64.0 : 72.0; - return isDense ? 76.0 : 88.0; - } - - @override - double computeMinIntrinsicHeight(double width) { - return math.max( - _defaultTileHeight, - title.getMinIntrinsicHeight(width) + (subtitle?.getMinIntrinsicHeight(width) ?? 0.0) + _RenderListTile createRenderObject(BuildContext context) { + return new _RenderListTile( + isThreeLine: isThreeLine, + isDense: isDense, + textDirection: textDirection, + titleBaselineType: titleBaselineType, + subtitleBaselineType: subtitleBaselineType, ); } @override - double computeMaxIntrinsicHeight(double width) { - return computeMinIntrinsicHeight(width); - } - - @override - double computeDistanceToActualBaseline(TextBaseline baseline) { - assert(title != null); - final BoxParentData parentData = title.parentData; - return parentData.offset.dy + title.getDistanceToActualBaseline(TextBaseline.alphabetic); - } - - static double _boxBaseline(RenderBox box) { - return box.getDistanceToBaseline(TextBaseline.alphabetic); - } - - static Size _layoutBox(RenderBox box, BoxConstraints constraints) { - if (box == null) - return Size.zero; - box.layout(constraints, parentUsesSize: true); - return box.size; - } - - static void _positionBox(RenderBox box, Offset offset) { - final BoxParentData parentData = box.parentData; - parentData.offset = offset; - } - - // All of the dimensions below were taken from the Material Design spec: - // https://material.io/design/components/lists.html#specs - @override - void performLayout() { - final bool hasLeading = leading != null; - final bool hasSubtitle = subtitle != null; - final bool hasTrailing = trailing != null; - final bool isTwoLine = !isThreeLine && hasSubtitle; - final bool isOneLine = !isThreeLine && !hasSubtitle; - final BoxConstraints looseConstraints = constraints.loosen(); - - final double tileWidth = looseConstraints.maxWidth; - final Size leadingSize = _layoutBox(leading, looseConstraints); - final Size trailingSize = _layoutBox(trailing, looseConstraints); - - final double titleStart = hasLeading - ? math.max(_minLeadingWidth, leadingSize.width) + _horizontalTitleGap - : 0.0; - final BoxConstraints textConstraints = looseConstraints.tighten( - width: tileWidth - titleStart - (hasTrailing ? trailingSize.width + _horizontalTitleGap : 0.0), - ); - final Size titleSize = _layoutBox(title, textConstraints); - final Size subtitleSize = _layoutBox(subtitle, textConstraints); - - double titleBaseline; - double subtitleBaseline; - if (isTwoLine) { - titleBaseline = isDense ? 28.0 : 32.0; - subtitleBaseline = isDense ? 48.0 : 52.0; - } else if (isThreeLine) { - titleBaseline = isDense ? 22.0 : 28.0; - subtitleBaseline = isDense ? 42.0 : 48.0; - } else { - assert(isOneLine); - } - - double tileHeight; - double titleY; - double subtitleY; - if (!hasSubtitle) { - tileHeight = math.max(_defaultTileHeight, titleSize.height + 2.0 * _minVerticalPadding); - titleY = (tileHeight - titleSize.height) / 2.0; - } else { - titleY = titleBaseline - _boxBaseline(title); - subtitleY = subtitleBaseline - _boxBaseline(subtitle); - tileHeight = _defaultTileHeight; - - // If the title and subtitle overlap, move the title upwards by half - // the overlap and the subtitle down by the same amount, and adjust - // tileHeight so that both titles fit. - final double titleOverlap = titleY + titleSize.height - subtitleY; - if (titleOverlap > 0.0) { - titleY -= titleOverlap / 2.0; - subtitleY += titleOverlap / 2.0; - } - - // If the title or subtitle overflow tileHeight then punt: title - // and subtitle are arranged in a column, tileHeight = column height plus - // _minVerticalPadding on top and bottom. - if (titleY < _minVerticalPadding || - (subtitleY + subtitleSize.height + _minVerticalPadding) > tileHeight) { - tileHeight = titleSize.height + subtitleSize.height + 2.0 * _minVerticalPadding; - titleY = _minVerticalPadding; - subtitleY = titleSize.height + _minVerticalPadding; - } - } - - final double leadingY = (tileHeight - leadingSize.height) / 2.0; - final double trailingY = (tileHeight - trailingSize.height) / 2.0; - - switch (textDirection) { - case TextDirection.rtl: { - if (hasLeading) - _positionBox(leading, new Offset(tileWidth - leadingSize.width, leadingY)); - final double titleX = hasTrailing ? trailingSize.width + _horizontalTitleGap : 0.0; - _positionBox(title, new Offset(titleX, titleY)); - if (hasSubtitle) - _positionBox(subtitle, new Offset(titleX, subtitleY)); - if (hasTrailing) - _positionBox(trailing, new Offset(0.0, trailingY)); - break; - } - case TextDirection.ltr: { - if (hasLeading) - _positionBox(leading, new Offset(0.0, leadingY)); - _positionBox(title, new Offset(titleStart, titleY)); - if (hasSubtitle) - _positionBox(subtitle, new Offset(titleStart, subtitleY)); - if (hasTrailing) - _positionBox(trailing, new Offset(tileWidth - trailingSize.width, trailingY)); - break; - } - } - - size = constraints.constrain(new Size(tileWidth, tileHeight)); - assert(size.width == constraints.constrainWidth(tileWidth)); - assert(size.height == constraints.constrainHeight(tileHeight)); - } - - @override - void paint(PaintingContext context, Offset offset) { - void doPaint(RenderBox child) { - if (child != null) { - final BoxParentData parentData = child.parentData; - context.paintChild(child, parentData.offset + offset); - } - } - doPaint(leading); - doPaint(title); - doPaint(subtitle); - doPaint(trailing); - } - - @override - bool hitTestSelf(Offset position) => true; - - @override - bool hitTestChildren(HitTestResult result, { @required Offset position }) { - assert(position != null); - for (RenderBox child in _children) { - final BoxParentData parentData = child.parentData; - if (child.hitTest(result, position: position - parentData.offset)) - return true; - } - return false; + void updateRenderObject(BuildContext context, _RenderListTile renderObject) { + renderObject + ..isThreeLine = isThreeLine + ..isDense = isDense + ..textDirection = textDirection + ..titleBaselineType = titleBaselineType + ..subtitleBaselineType = subtitleBaselineType; } } -class _RenderListTileElement extends RenderObjectElement { - _RenderListTileElement(_ListTile widget) : super(widget); +class _ListTileElement extends RenderObjectElement { + _ListTileElement(_ListTile widget) : super(widget); final Map<_ListTileSlot, Element> slotToChild = <_ListTileSlot, Element>{}; final Map childToSlot = {}; @@ -972,3 +652,374 @@ class _RenderListTileElement extends RenderObjectElement { assert(false, 'not reachable'); } } + +class _RenderListTile extends RenderBox { + _RenderListTile({ + @required bool isDense, + @required bool isThreeLine, + @required TextDirection textDirection, + @required TextBaseline titleBaselineType, + TextBaseline subtitleBaselineType, + }) : assert(isDense != null), + assert(isThreeLine != null), + assert(textDirection != null), + assert(titleBaselineType != null), + _isDense = isDense, + _isThreeLine = isThreeLine, + _textDirection = textDirection, + _titleBaselineType = titleBaselineType, + _subtitleBaselineType = subtitleBaselineType; + + static const double _minLeadingWidth = 40.0; + // The horizontal gap between the titles and the leading/trailing widgets + static const double _horizontalTitleGap = 16.0; + // The minimum padding on the top and bottom of the title and subtitle widgets. + static const double _minVerticalPadding = 4.0; + + final Map<_ListTileSlot, RenderBox> slotToChild = <_ListTileSlot, RenderBox>{}; + final Map childToSlot = {}; + + RenderBox _updateChild(RenderBox oldChild, RenderBox newChild, _ListTileSlot slot) { + if (oldChild != null) { + dropChild(oldChild); + childToSlot.remove(oldChild); + slotToChild.remove(slot); + } + if (newChild != null) { + childToSlot[newChild] = slot; + slotToChild[slot] = newChild; + adoptChild(newChild); + } + return newChild; + } + + RenderBox _leading; + RenderBox get leading => _leading; + set leading(RenderBox value) { + _leading = _updateChild(_leading, value, _ListTileSlot.leading); + } + + RenderBox _title; + RenderBox get title => _title; + set title(RenderBox value) { + _title = _updateChild(_title, value, _ListTileSlot.title); + } + + RenderBox _subtitle; + RenderBox get subtitle => _subtitle; + set subtitle(RenderBox value) { + _subtitle = _updateChild(_subtitle, value, _ListTileSlot.subtitle); + } + + RenderBox _trailing; + RenderBox get trailing => _trailing; + set trailing(RenderBox value) { + _trailing = _updateChild(_trailing, value, _ListTileSlot.trailing); + } + + // The returned list is ordered for hit testing. + Iterable get _children sync *{ + if (leading != null) + yield leading; + if (title != null) + yield title; + if (subtitle != null) + yield subtitle; + if (trailing != null) + yield trailing; + } + + bool get isDense => _isDense; + bool _isDense; + set isDense(bool value) { + assert(value != null); + if (_isDense == value) + return; + _isDense = value; + markNeedsLayout(); + } + + bool get isThreeLine => _isThreeLine; + bool _isThreeLine; + set isThreeLine(bool value) { + assert(value != null); + if (_isThreeLine == value) + return; + _isThreeLine = value; + markNeedsLayout(); + } + + TextDirection get textDirection => _textDirection; + TextDirection _textDirection; + set textDirection(TextDirection value) { + assert(value != null); + if (_textDirection == value) + return; + _textDirection = value; + markNeedsLayout(); + } + + TextBaseline get titleBaselineType => _titleBaselineType; + TextBaseline _titleBaselineType; + set titleBaselineType(TextBaseline value) { + assert(value != null); + if (_titleBaselineType == value) + return; + _titleBaselineType = value; + markNeedsLayout(); + } + + TextBaseline get subtitleBaselineType => _subtitleBaselineType; + TextBaseline _subtitleBaselineType; + set subtitleBaselineType(TextBaseline value) { + if (_subtitleBaselineType == value) + return; + _subtitleBaselineType = value; + markNeedsLayout(); + } + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + for (RenderBox child in _children) + child.attach(owner); + } + + @override + void detach() { + super.detach(); + for (RenderBox child in _children) + child.detach(); + } + + @override + void redepthChildren() { + _children.forEach(redepthChild); + } + + @override + void visitChildren(RenderObjectVisitor visitor) { + _children.forEach(visitor); + } + + @override + List debugDescribeChildren() { + final List value = []; + void add(RenderBox child, String name) { + if (child != null) + value.add(child.toDiagnosticsNode(name: name)); + } + add(leading, 'leading'); + add(title, 'title'); + add(subtitle, 'subtitle'); + add(trailing, 'trailing'); + return value; + } + + @override + bool get sizedByParent => false; + + static double _minWidth(RenderBox box, double height) { + return box == null ? 0.0 : box.getMinIntrinsicWidth(height); + } + + static double _maxWidth(RenderBox box, double height) { + return box == null ? 0.0 : box.getMaxIntrinsicWidth(height); + } + + @override + double computeMinIntrinsicWidth(double height) { + final double leadingWidth = leading != null + ? math.max(leading.getMinIntrinsicWidth(height), _minLeadingWidth) + _horizontalTitleGap + : 0.0; + return leadingWidth + + math.max(_minWidth(title, height), _minWidth(subtitle, height)) + + _maxWidth(trailing, height); + } + + @override + double computeMaxIntrinsicWidth(double height) { + final double leadingWidth = leading != null + ? math.max(leading.getMaxIntrinsicWidth(height), _minLeadingWidth) + _horizontalTitleGap + : 0.0; + return leadingWidth + + math.max(_maxWidth(title, height), _maxWidth(subtitle, height)) + + _maxWidth(trailing, height); + } + + double get _defaultTileHeight { + final bool hasSubtitle = subtitle != null; + final bool isTwoLine = !isThreeLine && hasSubtitle; + final bool isOneLine = !isThreeLine && !hasSubtitle; + + if (isOneLine) + return isDense ? 48.0 : 56.0; + if (isTwoLine) + return isDense ? 64.0 : 72.0; + return isDense ? 76.0 : 88.0; + } + + @override + double computeMinIntrinsicHeight(double width) { + return math.max( + _defaultTileHeight, + title.getMinIntrinsicHeight(width) + (subtitle?.getMinIntrinsicHeight(width) ?? 0.0) + ); + } + + @override + double computeMaxIntrinsicHeight(double width) { + return computeMinIntrinsicHeight(width); + } + + @override + double computeDistanceToActualBaseline(TextBaseline baseline) { + assert(title != null); + final BoxParentData parentData = title.parentData; + return parentData.offset.dy + title.getDistanceToActualBaseline(baseline); + } + + static double _boxBaseline(RenderBox box, TextBaseline baseline) { + return box.getDistanceToBaseline(baseline); + } + + static Size _layoutBox(RenderBox box, BoxConstraints constraints) { + if (box == null) + return Size.zero; + box.layout(constraints, parentUsesSize: true); + return box.size; + } + + static void _positionBox(RenderBox box, Offset offset) { + final BoxParentData parentData = box.parentData; + parentData.offset = offset; + } + + // All of the dimensions below were taken from the Material Design spec: + // https://material.io/design/components/lists.html#specs + @override + void performLayout() { + final bool hasLeading = leading != null; + final bool hasSubtitle = subtitle != null; + final bool hasTrailing = trailing != null; + final bool isTwoLine = !isThreeLine && hasSubtitle; + final bool isOneLine = !isThreeLine && !hasSubtitle; + final BoxConstraints looseConstraints = constraints.loosen(); + + final double tileWidth = looseConstraints.maxWidth; + final Size leadingSize = _layoutBox(leading, looseConstraints); + final Size trailingSize = _layoutBox(trailing, looseConstraints); + + final double titleStart = hasLeading + ? math.max(_minLeadingWidth, leadingSize.width) + _horizontalTitleGap + : 0.0; + final BoxConstraints textConstraints = looseConstraints.tighten( + width: tileWidth - titleStart - (hasTrailing ? trailingSize.width + _horizontalTitleGap : 0.0), + ); + final Size titleSize = _layoutBox(title, textConstraints); + final Size subtitleSize = _layoutBox(subtitle, textConstraints); + + double titleBaseline; + double subtitleBaseline; + if (isTwoLine) { + titleBaseline = isDense ? 28.0 : 32.0; + subtitleBaseline = isDense ? 48.0 : 52.0; + } else if (isThreeLine) { + titleBaseline = isDense ? 22.0 : 28.0; + subtitleBaseline = isDense ? 42.0 : 48.0; + } else { + assert(isOneLine); + } + + double tileHeight; + double titleY; + double subtitleY; + if (!hasSubtitle) { + tileHeight = math.max(_defaultTileHeight, titleSize.height + 2.0 * _minVerticalPadding); + titleY = (tileHeight - titleSize.height) / 2.0; + } else { + assert(subtitleBaselineType != null); + titleY = titleBaseline - _boxBaseline(title, titleBaselineType); + subtitleY = subtitleBaseline - _boxBaseline(subtitle, subtitleBaselineType); + tileHeight = _defaultTileHeight; + + // If the title and subtitle overlap, move the title upwards by half + // the overlap and the subtitle down by the same amount, and adjust + // tileHeight so that both titles fit. + final double titleOverlap = titleY + titleSize.height - subtitleY; + if (titleOverlap > 0.0) { + titleY -= titleOverlap / 2.0; + subtitleY += titleOverlap / 2.0; + } + + // If the title or subtitle overflow tileHeight then punt: title + // and subtitle are arranged in a column, tileHeight = column height plus + // _minVerticalPadding on top and bottom. + if (titleY < _minVerticalPadding || + (subtitleY + subtitleSize.height + _minVerticalPadding) > tileHeight) { + tileHeight = titleSize.height + subtitleSize.height + 2.0 * _minVerticalPadding; + titleY = _minVerticalPadding; + subtitleY = titleSize.height + _minVerticalPadding; + } + } + + final double leadingY = (tileHeight - leadingSize.height) / 2.0; + final double trailingY = (tileHeight - trailingSize.height) / 2.0; + + switch (textDirection) { + case TextDirection.rtl: { + if (hasLeading) + _positionBox(leading, new Offset(tileWidth - leadingSize.width, leadingY)); + final double titleX = hasTrailing ? trailingSize.width + _horizontalTitleGap : 0.0; + _positionBox(title, new Offset(titleX, titleY)); + if (hasSubtitle) + _positionBox(subtitle, new Offset(titleX, subtitleY)); + if (hasTrailing) + _positionBox(trailing, new Offset(0.0, trailingY)); + break; + } + case TextDirection.ltr: { + if (hasLeading) + _positionBox(leading, new Offset(0.0, leadingY)); + _positionBox(title, new Offset(titleStart, titleY)); + if (hasSubtitle) + _positionBox(subtitle, new Offset(titleStart, subtitleY)); + if (hasTrailing) + _positionBox(trailing, new Offset(tileWidth - trailingSize.width, trailingY)); + break; + } + } + + size = constraints.constrain(new Size(tileWidth, tileHeight)); + assert(size.width == constraints.constrainWidth(tileWidth)); + assert(size.height == constraints.constrainHeight(tileHeight)); + } + + @override + void paint(PaintingContext context, Offset offset) { + void doPaint(RenderBox child) { + if (child != null) { + final BoxParentData parentData = child.parentData; + context.paintChild(child, parentData.offset + offset); + } + } + doPaint(leading); + doPaint(title); + doPaint(subtitle); + doPaint(trailing); + } + + @override + bool hitTestSelf(Offset position) => true; + + @override + bool hitTestChildren(HitTestResult result, { @required Offset position }) { + assert(position != null); + for (RenderBox child in _children) { + final BoxParentData parentData = child.parentData; + if (child.hitTest(result, position: position - parentData.offset)) + return true; + } + return false; + } +} diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index 81abc556b4..0d1e773dc9 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -248,7 +248,7 @@ class PopupMenuItemState> extends State { duration: kThemeChangeDuration, child: new Baseline( baseline: widget.height - _kBaselineOffsetFromBottom, - baselineType: TextBaseline.alphabetic, + baselineType: style.textBaseline, child: buildChild(), ) ); diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index 322de44141..5c0f7c2c91 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -1599,9 +1599,12 @@ abstract class RenderBox extends RenderObject { /// Only call this function after calling [layout] on this box. You /// are only allowed to call this from the parent of this box during /// that parent's [performLayout] or [paint] functions. + /// + /// When implementing a [RenderBox] subclass, to override the baseline + /// computation, override [computeDistanceToActualBaseline]. double getDistanceToBaseline(TextBaseline baseline, { bool onlyReal: false }) { + assert(!_debugDoingBaseline, 'Please see the documentation for computeDistanceToActualBaseline for the required calling conventions of this method.'); assert(!debugNeedsLayout); - assert(!_debugDoingBaseline); assert(() { final RenderObject parent = this.parent; if (owner.debugDoingLayout) @@ -1628,7 +1631,7 @@ abstract class RenderBox extends RenderObject { @protected @mustCallSuper double getDistanceToActualBaseline(TextBaseline baseline) { - assert(_debugDoingBaseline); + assert(_debugDoingBaseline, 'Please see the documentation for computeDistanceToActualBaseline for the required calling conventions of this method.'); _cachedBaselines ??= {}; _cachedBaselines.putIfAbsent(baseline, () => computeDistanceToActualBaseline(baseline)); return _cachedBaselines[baseline]; @@ -1638,17 +1641,29 @@ abstract class RenderBox extends RenderObject { /// the y-coordinate of the first given baseline in the box's contents, if /// any, or null otherwise. /// - /// Do not call this function directly. Instead, call [getDistanceToBaseline] - /// if you need to know the baseline of a child from an invocation of - /// [performLayout] or [paint] and call [getDistanceToActualBaseline] if you - /// are implementing [computeDistanceToActualBaseline] and need to defer to a - /// child. + /// Do not call this function directly. If you need to know the baseline of a + /// child from an invocation of [performLayout] or [paint], call + /// [getDistanceToBaseline]. /// /// Subclasses should override this method to supply the distances to their - /// baselines. + /// baselines. When implementing this method, there are generally three + /// strategies: + /// + /// * For classes that use the [ContainerRenderObjectMixin] child model, + /// consider mixing in the [RenderBoxContainerDefaultsMixin] class and + /// using + /// [RenderBoxContainerDefaultsMixin.defaultComputeDistanceToFirstActualBaseline]. + /// + /// * For classes that define a particular baseline themselves, return that + /// value directly. + /// + /// * For classes that have a child to which they wish to defer the + /// computation, call [getDistanceToActualBaseline] on the child (not + /// [computeDistanceToActualBaseline], the internal implementation, and not + /// [getDistanceToBaseline], the public entry point for this API). @protected double computeDistanceToActualBaseline(TextBaseline baseline) { - assert(_debugDoingBaseline); + assert(_debugDoingBaseline, 'Please see the documentation for computeDistanceToActualBaseline for the required calling conventions of this method.'); return null; } diff --git a/packages/flutter/lib/src/rendering/shifted_box.dart b/packages/flutter/lib/src/rendering/shifted_box.dart index d6e57770f9..3ffcc40257 100644 --- a/packages/flutter/lib/src/rendering/shifted_box.dart +++ b/packages/flutter/lib/src/rendering/shifted_box.dart @@ -1124,7 +1124,7 @@ class RenderBaseline extends RenderShiftedBox { RenderBaseline({ RenderBox child, @required double baseline, - @required TextBaseline baselineType + @required TextBaseline baselineType, }) : assert(baseline != null), assert(baselineType != null), _baseline = baseline, diff --git a/packages/flutter/lib/src/widgets/table.dart b/packages/flutter/lib/src/widgets/table.dart index 7d3c86dde1..fcc51d341b 100644 --- a/packages/flutter/lib/src/widgets/table.dart +++ b/packages/flutter/lib/src/widgets/table.dart @@ -101,7 +101,7 @@ class Table extends RenderObjectWidget { this.textDirection, this.border, this.defaultVerticalAlignment: TableCellVerticalAlignment.top, - this.textBaseline + this.textBaseline, }) : assert(children != null), assert(defaultColumnWidth != null), assert(defaultVerticalAlignment != null), @@ -213,7 +213,7 @@ class Table extends RenderObjectWidget { rowDecorations: _rowDecorations, configuration: createLocalImageConfiguration(context), defaultVerticalAlignment: defaultVerticalAlignment, - textBaseline: textBaseline + textBaseline: textBaseline, ); } diff --git a/packages/flutter/test/material/chip_test.dart b/packages/flutter/test/material/chip_test.dart index 02b33f7800..b642b0604f 100644 --- a/packages/flutter/test/material/chip_test.dart +++ b/packages/flutter/test/material/chip_test.dart @@ -65,18 +65,11 @@ Widget _wrapForChip({ double textScaleFactor: 1.0, }) { return new MaterialApp( - home: new Localizations( - locale: const Locale('en', 'US'), - delegates: const >[ - DefaultWidgetsLocalizations.delegate, - DefaultMaterialLocalizations.delegate, - ], - child: new Directionality( - textDirection: textDirection, - child: new MediaQuery( - data: new MediaQueryData.fromWindow(window).copyWith(textScaleFactor: textScaleFactor), - child: new Material(child: child), - ), + home: new Directionality( + textDirection: textDirection, + child: new MediaQuery( + data: new MediaQueryData.fromWindow(window).copyWith(textScaleFactor: textScaleFactor), + child: new Material(child: child), ), ), ); diff --git a/packages/flutter/test/widgets/baseline_test.dart b/packages/flutter/test/widgets/baseline_test.dart index 681600a8bc..03875b5dec 100644 --- a/packages/flutter/test/widgets/baseline_test.dart +++ b/packages/flutter/test/widgets/baseline_test.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; void main() { testWidgets('Baseline - control test', (WidgetTester tester) async { @@ -41,4 +41,108 @@ void main() { expect(tester.renderObject(find.byType(Baseline)).size, within(from: const Size(100.0, 200.0), distance: 0.001)); }); + + testWidgets('Chip caches baseline', (WidgetTester tester) async { + int calls = 0; + await tester.pumpWidget( + new MaterialApp( + home: new Material( + child: new Baseline( + baseline: 100.0, + baselineType: TextBaseline.alphabetic, + child: new Chip( + label: new BaselineDetector(() { + calls += 1; + }), + ), + ), + ), + ), + ); + expect(calls, 1); + await tester.pump(); + expect(calls, 1); + tester.renderObject(find.byType(BaselineDetector)).dirty(); + await tester.pump(); + expect(calls, 2); + }); + + testWidgets('ListTile caches baseline', (WidgetTester tester) async { + int calls = 0; + await tester.pumpWidget( + new MaterialApp( + home: new Material( + child: new Baseline( + baseline: 100.0, + baselineType: TextBaseline.alphabetic, + child: new ListTile( + title: new BaselineDetector(() { + calls += 1; + }), + ), + ), + ), + ), + ); + expect(calls, 1); + await tester.pump(); + expect(calls, 1); + tester.renderObject(find.byType(BaselineDetector)).dirty(); + await tester.pump(); + expect(calls, 2); + }); +} + +class BaselineDetector extends LeafRenderObjectWidget { + const BaselineDetector(this.callback); + + final VoidCallback callback; + + @override + RenderBaselineDetector createRenderObject(BuildContext context) => new RenderBaselineDetector(callback); + + @override + void updateRenderObject(BuildContext context, RenderBaselineDetector renderObject) { + renderObject.callback = callback; + } +} + +class RenderBaselineDetector extends RenderBox { + RenderBaselineDetector(this.callback); + + VoidCallback callback; + + @override + bool get sizedByParent => true; + + @override + double computeMinIntrinsicWidth(double height) => 0.0; + + @override + double computeMaxIntrinsicWidth(double height) => 0.0; + + @override + double computeMinIntrinsicHeight(double width) => 0.0; + + @override + double computeMaxIntrinsicHeight(double width) => 0.0; + + @override + double computeDistanceToActualBaseline(TextBaseline baseline) { + if (callback != null) + callback(); + return 0.0; + } + + void dirty() { + markNeedsLayout(); + } + + @override + void performResize() { + size = constraints.smallest; + } + + @override + void paint(PaintingContext context, Offset offset) { } }