Clean up baseline logic in material library. (#17922)

This commit is contained in:
Ian Hickson 2018-05-30 22:53:03 -07:00 committed by GitHub
parent 587a1b7708
commit 9f1e76e967
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 637 additions and 415 deletions

View File

@ -1855,7 +1855,7 @@ class _RenderChip extends RenderBox {
@override @override
double computeDistanceToActualBaseline(TextBaseline baseline) { double computeDistanceToActualBaseline(TextBaseline baseline) {
// The baseline of this widget is the baseline of the label. // 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) { Size _layoutLabel(double iconSizes, Size size) {

View File

@ -537,10 +537,15 @@ class _RenderDecorationLayout {
// The workhorse: layout and paint a _Decorator widget's _Decoration. // The workhorse: layout and paint a _Decorator widget's _Decoration.
class _RenderDecoration extends RenderBox { class _RenderDecoration extends RenderBox {
_RenderDecoration({ _RenderDecoration({
_Decoration decoration, @required _Decoration decoration,
TextDirection textDirection, @required TextDirection textDirection,
}) : _decoration = decoration, @required TextBaseline textBaseline,
_textDirection = textDirection; }) : assert(decoration != null),
assert(textDirection != null),
assert(textBaseline != null),
_decoration = decoration,
_textDirection = textDirection,
_textBaseline = textBaseline;
final Map<_DecorationSlot, RenderBox> slotToChild = <_DecorationSlot, RenderBox>{}; final Map<_DecorationSlot, RenderBox> slotToChild = <_DecorationSlot, RenderBox>{};
final Map<RenderBox, _DecorationSlot> childToSlot = <RenderBox, _DecorationSlot>{}; final Map<RenderBox, _DecorationSlot> childToSlot = <RenderBox, _DecorationSlot>{};
@ -654,6 +659,7 @@ class _RenderDecoration extends RenderBox {
_Decoration get decoration => _decoration; _Decoration get decoration => _decoration;
_Decoration _decoration; _Decoration _decoration;
set decoration(_Decoration value) { set decoration(_Decoration value) {
assert(value != null);
if (_decoration == value) if (_decoration == value)
return; return;
_decoration = value; _decoration = value;
@ -663,12 +669,23 @@ class _RenderDecoration extends RenderBox {
TextDirection get textDirection => _textDirection; TextDirection get textDirection => _textDirection;
TextDirection _textDirection; TextDirection _textDirection;
set textDirection(TextDirection value) { set textDirection(TextDirection value) {
assert(value != null);
if (_textDirection == value) if (_textDirection == value)
return; return;
_textDirection = value; _textDirection = value;
markNeedsLayout(); markNeedsLayout();
} }
TextBaseline get textBaseline => _textBaseline;
TextBaseline _textBaseline;
set textBaseline(TextBaseline value) {
assert(value != null);
if (_textBaseline == value)
return;
_textBaseline = value;
markNeedsLayout();
}
@override @override
void attach(PipelineOwner owner) { void attach(PipelineOwner owner) {
super.attach(owner); super.attach(owner);
@ -748,7 +765,7 @@ class _RenderDecoration extends RenderBox {
if (box == null) if (box == null)
return; return;
box.layout(boxConstraints, parentUsesSize: true); box.layout(boxConstraints, parentUsesSize: true);
final double baseline = box.getDistanceToBaseline(TextBaseline.alphabetic); final double baseline = box.getDistanceToBaseline(textBaseline);
assert(baseline != null && baseline >= 0.0); assert(baseline != null && baseline >= 0.0);
boxToBaseline[box] = baseline; boxToBaseline[box] = baseline;
aboveBaseline = math.max(baseline, aboveBaseline); aboveBaseline = math.max(baseline, aboveBaseline);
@ -913,7 +930,15 @@ class _RenderDecoration extends RenderBox {
width: overallWidth - _boxSize(icon).width, width: overallWidth - _boxSize(icon).width,
); );
container.layout(containerConstraints, parentUsesSize: true); 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); _boxParentData(container).offset = new Offset(x, 0.0);
} }
@ -938,7 +963,15 @@ class _RenderDecoration extends RenderBox {
: layout.outlineBaseline; : layout.outlineBaseline;
if (icon != null) { 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); centerLayout(icon, x);
} }
@ -1004,9 +1037,14 @@ class _RenderDecoration extends RenderBox {
} }
if (label != null) { if (label != null) {
decoration.borderGap.start = textDirection == TextDirection.rtl switch (textDirection) {
? _boxParentData(label).offset.dx + label.size.width case TextDirection.rtl:
: _boxParentData(label).offset.dx; 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; decoration.borderGap.extent = label.size.width * 0.75;
} else { } else {
decoration.borderGap.start = null; decoration.borderGap.start = null;
@ -1039,9 +1077,15 @@ class _RenderDecoration extends RenderBox {
final bool isOutlineBorder = decoration.border != null && decoration.border.isOutline; final bool isOutlineBorder = decoration.border != null && decoration.border.isOutline;
final double floatingY = isOutlineBorder ? -labelHeight * 0.25 : contentPadding.top; final double floatingY = isOutlineBorder ? -labelHeight * 0.25 : contentPadding.top;
final double scale = lerpDouble(1.0, 0.75, t); final double scale = lerpDouble(1.0, 0.75, t);
final double dx = textDirection == TextDirection.rtl double dx;
? labelOffset.dx + label.size.width * (1.0 - scale) // origin is on the right switch (textDirection) {
: labelOffset.dx; // origin on the left 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); final double dy = lerpDouble(0.0, floatingY - labelOffset.dy, t);
_labelTransform = new Matrix4.identity() _labelTransform = new Matrix4.identity()
..translate(dx, labelOffset.dy + dy) ..translate(dx, labelOffset.dy + dy)
@ -1237,10 +1281,17 @@ class _RenderDecorationElement extends RenderObjectElement {
class _Decorator extends RenderObjectWidget { class _Decorator extends RenderObjectWidget {
const _Decorator({ const _Decorator({
Key key, Key key,
this.decoration, @required this.decoration,
}) : super(key: key); @required this.textDirection,
@required this.textBaseline,
}) : assert(decoration != null),
assert(textDirection != null),
assert(textBaseline != null),
super(key: key);
final _Decoration decoration; final _Decoration decoration;
final TextDirection textDirection;
final TextBaseline textBaseline;
@override @override
_RenderDecorationElement createElement() => new _RenderDecorationElement(this); _RenderDecorationElement createElement() => new _RenderDecorationElement(this);
@ -1249,7 +1300,8 @@ class _Decorator extends RenderObjectWidget {
_RenderDecoration createRenderObject(BuildContext context) { _RenderDecoration createRenderObject(BuildContext context) {
return new _RenderDecoration( return new _RenderDecoration(
decoration: decoration, decoration: decoration,
textDirection: Directionality.of(context), textDirection: textDirection,
textBaseline: textBaseline,
); );
} }
@ -1257,7 +1309,8 @@ class _Decorator extends RenderObjectWidget {
void updateRenderObject(BuildContext context, _RenderDecoration renderObject) { void updateRenderObject(BuildContext context, _RenderDecoration renderObject) {
renderObject renderObject
..decoration = decoration ..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 /// If null, `baseStyle` defaults to the `subhead` style from the
/// current [Theme], see [ThemeData.textTheme]. /// current [Theme], see [ThemeData.textTheme].
///
/// The [TextStyle.textBaseline] of the [baseStyle] is used to determine
/// the baseline used for text alignment.
final TextStyle baseStyle; final TextStyle baseStyle;
/// How the text in the decoration should be aligned horizontally. /// How the text in the decoration should be aligned horizontally.
@ -1539,6 +1595,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context); final ThemeData themeData = Theme.of(context);
final TextStyle inlineStyle = _getInlineStyle(themeData); final TextStyle inlineStyle = _getInlineStyle(themeData);
final TextBaseline textBaseline = inlineStyle.textBaseline;
final TextStyle hintStyle = inlineStyle.merge(decoration.hintStyle); final TextStyle hintStyle = inlineStyle.merge(decoration.hintStyle);
final Widget hint = decoration.hintText == null ? null : new AnimatedOpacity( final Widget hint = decoration.hintText == null ? null : new AnimatedOpacity(
@ -1713,6 +1770,8 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
counter: counter, counter: counter,
container: container, container: container,
), ),
textDirection: textDirection,
textBaseline: textBaseline,
); );
} }
} }

View File

@ -420,16 +420,19 @@ class ListTile extends StatelessWidget {
); );
} }
final TextStyle titleStyle = _titleTextStyle(theme, tileTheme);
final Widget titleText = new AnimatedDefaultTextStyle( final Widget titleText = new AnimatedDefaultTextStyle(
style: _titleTextStyle(theme, tileTheme), style: titleStyle,
duration: kThemeChangeDuration, duration: kThemeChangeDuration,
child: title ?? const SizedBox() child: title ?? const SizedBox()
); );
Widget subtitleText; Widget subtitleText;
TextStyle subtitleStyle;
if (subtitle != null) { if (subtitle != null) {
subtitleStyle = _subtitleTextStyle(theme, tileTheme);
subtitleText = new AnimatedDefaultTextStyle( subtitleText = new AnimatedDefaultTextStyle(
style: _subtitleTextStyle(theme, tileTheme), style: subtitleStyle,
duration: kThemeChangeDuration, duration: kThemeChangeDuration,
child: subtitle, child: subtitle,
); );
@ -466,6 +469,9 @@ class ListTile extends StatelessWidget {
trailing: trailingIcon, trailing: trailingIcon,
isDense: _isDenseLayout(tileTheme), isDense: _isDenseLayout(tileTheme),
isThreeLine: isThreeLine, 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. // Identifies the children of a _ListTileElement.
enum _ListTileSlot { enum _ListTileSlot {
leading, leading,
@ -520,348 +487,61 @@ enum _ListTileSlot {
trailing, trailing,
} }
class _RenderListTile extends RenderBox { class _ListTile extends RenderObjectWidget {
_RenderListTile({ const _ListTile({
bool isDense, Key key,
bool isThreeLine, this.leading,
TextDirection textDirection, this.title,
}) : _isDense = isDense, this.subtitle,
_isThreeLine = isThreeLine, this.trailing,
_textDirection = textDirection; @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; final Widget leading;
// The horizontal gap between the titles and the leading/trailing widgets final Widget title;
static const double _horizontalTitleGap = 16.0; final Widget subtitle;
// The minimum padding on the top and bottom of the title and subtitle widgets. final Widget trailing;
static const double _minVerticalPadding = 4.0; final bool isThreeLine;
final bool isDense;
final Map<_ListTileSlot, RenderBox> slotToChild = <_ListTileSlot, RenderBox>{}; final TextDirection textDirection;
final Map<RenderBox, _ListTileSlot> childToSlot = <RenderBox, _ListTileSlot>{}; final TextBaseline titleBaselineType;
final TextBaseline subtitleBaselineType;
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<RenderBox> 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();
}
@override @override
void attach(PipelineOwner owner) { _ListTileElement createElement() => new _ListTileElement(this);
super.attach(owner);
for (RenderBox child in _children)
child.attach(owner);
}
@override @override
void detach() { _RenderListTile createRenderObject(BuildContext context) {
super.detach(); return new _RenderListTile(
for (RenderBox child in _children) isThreeLine: isThreeLine,
child.detach(); isDense: isDense,
} textDirection: textDirection,
titleBaselineType: titleBaselineType,
@override subtitleBaselineType: subtitleBaselineType,
void redepthChildren() {
_children.forEach(redepthChild);
}
@override
void visitChildren(RenderObjectVisitor visitor) {
_children.forEach(visitor);
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
final List<DiagnosticsNode> value = <DiagnosticsNode>[];
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 @override
double computeMaxIntrinsicHeight(double width) { void updateRenderObject(BuildContext context, _RenderListTile renderObject) {
return computeMinIntrinsicHeight(width); renderObject
} ..isThreeLine = isThreeLine
..isDense = isDense
@override ..textDirection = textDirection
double computeDistanceToActualBaseline(TextBaseline baseline) { ..titleBaselineType = titleBaselineType
assert(title != null); ..subtitleBaselineType = subtitleBaselineType;
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; class _ListTileElement extends RenderObjectElement {
final double trailingY = (tileHeight - trailingSize.height) / 2.0; _ListTileElement(_ListTile widget) : super(widget);
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;
}
}
class _RenderListTileElement extends RenderObjectElement {
_RenderListTileElement(_ListTile widget) : super(widget);
final Map<_ListTileSlot, Element> slotToChild = <_ListTileSlot, Element>{}; final Map<_ListTileSlot, Element> slotToChild = <_ListTileSlot, Element>{};
final Map<Element, _ListTileSlot> childToSlot = <Element, _ListTileSlot>{}; final Map<Element, _ListTileSlot> childToSlot = <Element, _ListTileSlot>{};
@ -972,3 +652,374 @@ class _RenderListTileElement extends RenderObjectElement {
assert(false, 'not reachable'); 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<RenderBox, _ListTileSlot> childToSlot = <RenderBox, _ListTileSlot>{};
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<RenderBox> 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<DiagnosticsNode> debugDescribeChildren() {
final List<DiagnosticsNode> value = <DiagnosticsNode>[];
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;
}
}

View File

@ -248,7 +248,7 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
duration: kThemeChangeDuration, duration: kThemeChangeDuration,
child: new Baseline( child: new Baseline(
baseline: widget.height - _kBaselineOffsetFromBottom, baseline: widget.height - _kBaselineOffsetFromBottom,
baselineType: TextBaseline.alphabetic, baselineType: style.textBaseline,
child: buildChild(), child: buildChild(),
) )
); );

View File

@ -1599,9 +1599,12 @@ abstract class RenderBox extends RenderObject {
/// Only call this function after calling [layout] on this box. You /// Only call this function after calling [layout] on this box. You
/// are only allowed to call this from the parent of this box during /// are only allowed to call this from the parent of this box during
/// that parent's [performLayout] or [paint] functions. /// 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 }) { 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(!debugNeedsLayout);
assert(!_debugDoingBaseline);
assert(() { assert(() {
final RenderObject parent = this.parent; final RenderObject parent = this.parent;
if (owner.debugDoingLayout) if (owner.debugDoingLayout)
@ -1628,7 +1631,7 @@ abstract class RenderBox extends RenderObject {
@protected @protected
@mustCallSuper @mustCallSuper
double getDistanceToActualBaseline(TextBaseline baseline) { double getDistanceToActualBaseline(TextBaseline baseline) {
assert(_debugDoingBaseline); assert(_debugDoingBaseline, 'Please see the documentation for computeDistanceToActualBaseline for the required calling conventions of this method.');
_cachedBaselines ??= <TextBaseline, double>{}; _cachedBaselines ??= <TextBaseline, double>{};
_cachedBaselines.putIfAbsent(baseline, () => computeDistanceToActualBaseline(baseline)); _cachedBaselines.putIfAbsent(baseline, () => computeDistanceToActualBaseline(baseline));
return _cachedBaselines[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 /// the y-coordinate of the first given baseline in the box's contents, if
/// any, or null otherwise. /// any, or null otherwise.
/// ///
/// Do not call this function directly. Instead, call [getDistanceToBaseline] /// Do not call this function directly. If you need to know the baseline of a
/// if you need to know the baseline of a child from an invocation of /// child from an invocation of [performLayout] or [paint], call
/// [performLayout] or [paint] and call [getDistanceToActualBaseline] if you /// [getDistanceToBaseline].
/// are implementing [computeDistanceToActualBaseline] and need to defer to a
/// child.
/// ///
/// Subclasses should override this method to supply the distances to their /// 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 @protected
double computeDistanceToActualBaseline(TextBaseline baseline) { double computeDistanceToActualBaseline(TextBaseline baseline) {
assert(_debugDoingBaseline); assert(_debugDoingBaseline, 'Please see the documentation for computeDistanceToActualBaseline for the required calling conventions of this method.');
return null; return null;
} }

View File

@ -1124,7 +1124,7 @@ class RenderBaseline extends RenderShiftedBox {
RenderBaseline({ RenderBaseline({
RenderBox child, RenderBox child,
@required double baseline, @required double baseline,
@required TextBaseline baselineType @required TextBaseline baselineType,
}) : assert(baseline != null), }) : assert(baseline != null),
assert(baselineType != null), assert(baselineType != null),
_baseline = baseline, _baseline = baseline,

View File

@ -101,7 +101,7 @@ class Table extends RenderObjectWidget {
this.textDirection, this.textDirection,
this.border, this.border,
this.defaultVerticalAlignment: TableCellVerticalAlignment.top, this.defaultVerticalAlignment: TableCellVerticalAlignment.top,
this.textBaseline this.textBaseline,
}) : assert(children != null), }) : assert(children != null),
assert(defaultColumnWidth != null), assert(defaultColumnWidth != null),
assert(defaultVerticalAlignment != null), assert(defaultVerticalAlignment != null),
@ -213,7 +213,7 @@ class Table extends RenderObjectWidget {
rowDecorations: _rowDecorations, rowDecorations: _rowDecorations,
configuration: createLocalImageConfiguration(context), configuration: createLocalImageConfiguration(context),
defaultVerticalAlignment: defaultVerticalAlignment, defaultVerticalAlignment: defaultVerticalAlignment,
textBaseline: textBaseline textBaseline: textBaseline,
); );
} }

View File

@ -65,20 +65,13 @@ Widget _wrapForChip({
double textScaleFactor: 1.0, double textScaleFactor: 1.0,
}) { }) {
return new MaterialApp( return new MaterialApp(
home: new Localizations( home: new Directionality(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
],
child: new Directionality(
textDirection: textDirection, textDirection: textDirection,
child: new MediaQuery( child: new MediaQuery(
data: new MediaQueryData.fromWindow(window).copyWith(textScaleFactor: textScaleFactor), data: new MediaQueryData.fromWindow(window).copyWith(textScaleFactor: textScaleFactor),
child: new Material(child: child), child: new Material(child: child),
), ),
), ),
),
); );
} }

View File

@ -3,7 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart';
void main() { void main() {
testWidgets('Baseline - control test', (WidgetTester tester) async { testWidgets('Baseline - control test', (WidgetTester tester) async {
@ -41,4 +41,108 @@ void main() {
expect(tester.renderObject<RenderBox>(find.byType(Baseline)).size, expect(tester.renderObject<RenderBox>(find.byType(Baseline)).size,
within<Size>(from: const Size(100.0, 200.0), distance: 0.001)); within<Size>(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<RenderBaselineDetector>(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<RenderBaselineDetector>(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) { }
} }