Commit 145f3894 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Add RTL support to ListTile (#11838)

Fixes #11373
parent 15bf91fb
......@@ -391,9 +391,9 @@ class ListTile extends StatelessWidget {
children.add(IconTheme.merge(
data: new IconThemeData(color: _iconColor(theme, tileTheme)),
child: new Container(
margin: const EdgeInsets.only(right: 16.0),
margin: const EdgeInsetsDirectional.only(end: 16.0),
width: 40.0,
alignment: FractionalOffset.centerLeft,
alignment: FractionalOffsetDirectional.centerStart,
child: leading,
),
));
......@@ -425,8 +425,8 @@ class ListTile extends StatelessWidget {
if (trailing != null) {
children.add(new Container(
margin: const EdgeInsets.only(left: 16.0),
alignment: FractionalOffset.centerRight,
margin: const EdgeInsetsDirectional.only(start: 16.0),
alignment: FractionalOffsetDirectional.centerEnd,
child: trailing,
));
}
......
......@@ -108,7 +108,7 @@ class RenderPadding extends RenderShiftedBox {
void _applyUpdate() {
final EdgeInsets resolvedPadding = padding.resolve(textDirection);
assert(resolvedPadding.isNonNegative);
if (resolvedPadding != _resolvedPadding) {
if (_resolvedPadding != resolvedPadding) {
_resolvedPadding = resolvedPadding;
markNeedsLayout();
}
......@@ -117,7 +117,7 @@ class RenderPadding extends RenderShiftedBox {
/// The amount to pad the child in each dimension.
///
/// If this is set to an [EdgeInsetsDirectional] object, then [textDirection]
/// must be non-null.
/// must not be null.
EdgeInsetsGeometry get padding => _padding;
EdgeInsetsGeometry _padding;
set padding(EdgeInsetsGeometry value) {
......@@ -209,22 +209,37 @@ class RenderPadding extends RenderShiftedBox {
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding));
description.add(new EnumProperty<TextDirection>('textDirection', textDirection));
description.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
}
}
/// Abstract class for one-child-layout render boxes that use a
/// [FractionalOffset] to align their children.
/// [FractionalOffsetGeometry] to align their children.
abstract class RenderAligningShiftedBox extends RenderShiftedBox {
/// Initializes member variables for subclasses.
///
/// The [alignment] argument must not be null.
RenderAligningShiftedBox({
FractionalOffset alignment: FractionalOffset.center,
RenderBox child
}) : assert(alignment != null && alignment.dx != null && alignment.dy != null),
FractionalOffsetGeometry alignment: FractionalOffset.center,
TextDirection textDirection,
RenderBox child,
}) : assert(alignment != null),
_alignment = alignment,
super(child);
_textDirection = textDirection,
super(child) {
_applyUpdate();
}
// The resolved absolute alignment.
FractionalOffset _resolvedAlignment;
void _applyUpdate() {
final FractionalOffset resolvedAlignment = alignment.resolve(textDirection);
if (_resolvedAlignment != resolvedAlignment) {
_resolvedAlignment = resolvedAlignment;
markNeedsLayout();
}
}
/// How to align the child.
///
......@@ -235,17 +250,30 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox {
/// edge of the parent. Other values interpolate (and extrapolate) linearly.
/// For example, a value of 0.5 means that the center of the child is aligned
/// with the center of the parent.
FractionalOffset get alignment => _alignment;
FractionalOffset _alignment;
///
/// If this is set to an [FractionalOffsetDirectional] object, then
/// [textDirection] must not be null.
FractionalOffsetGeometry get alignment => _alignment;
FractionalOffsetGeometry _alignment;
/// Sets the alignment to a new value, and triggers a layout update.
///
/// The new alignment must not be null or have any null properties.
set alignment(FractionalOffset value) {
assert(value != null && value.dx != null && value.dy != null);
set alignment(FractionalOffsetGeometry value) {
assert(value != null);
if (_alignment == value)
return;
_alignment = value;
markNeedsLayout();
_applyUpdate();
}
/// The text direction with which to resolve [alignment].
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
if (_textDirection == value)
return;
_textDirection = value;
_applyUpdate();
}
/// Apply the current [alignment] to the [child].
......@@ -262,13 +290,14 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox {
assert(child.hasSize);
assert(hasSize);
final BoxParentData childParentData = child.parentData;
childParentData.offset = alignment.alongOffset(size - child.size);
childParentData.offset = _resolvedAlignment.alongOffset(size - child.size);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<FractionalOffset>('alignment', alignment));
description.add(new DiagnosticsProperty<FractionalOffsetGeometry>('alignment', alignment));
description.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
}
}
......@@ -288,12 +317,13 @@ class RenderPositionedBox extends RenderAligningShiftedBox {
RenderBox child,
double widthFactor,
double heightFactor,
FractionalOffset alignment: FractionalOffset.center
FractionalOffsetGeometry alignment: FractionalOffset.center,
TextDirection textDirection,
}) : assert(widthFactor == null || widthFactor >= 0.0),
assert(heightFactor == null || heightFactor >= 0.0),
_widthFactor = widthFactor,
_heightFactor = heightFactor,
super(child: child, alignment: alignment);
super(child: child, alignment: alignment, textDirection: textDirection);
/// If non-null, sets its width to the child's width multipled by this factor.
///
......@@ -432,12 +462,13 @@ class RenderConstrainedOverflowBox extends RenderAligningShiftedBox {
double maxWidth,
double minHeight,
double maxHeight,
FractionalOffset alignment: FractionalOffset.center
FractionalOffsetGeometry alignment: FractionalOffset.center,
TextDirection textDirection,
}) : _minWidth = minWidth,
_maxWidth = maxWidth,
_minHeight = minHeight,
_maxHeight = maxHeight,
super(child: child, alignment: alignment);
super(child: child, alignment: alignment, textDirection: textDirection);
/// The minimum width constraint to give the child. Set this to null (the
/// default) to use the constraint from the parent instead.
......@@ -527,10 +558,11 @@ class RenderSizedOverflowBox extends RenderAligningShiftedBox {
RenderSizedOverflowBox({
RenderBox child,
@required Size requestedSize,
FractionalOffset alignment: FractionalOffset.center
FractionalOffset alignment: FractionalOffset.center,
TextDirection textDirection,
}) : assert(requestedSize != null),
_requestedSize = requestedSize,
super(child: child, alignment: alignment);
super(child: child, alignment: alignment, textDirection: textDirection);
/// The size this render box should attempt to be.
Size get requestedSize => _requestedSize;
......@@ -598,10 +630,11 @@ class RenderFractionallySizedOverflowBox extends RenderAligningShiftedBox {
RenderBox child,
double widthFactor,
double heightFactor,
FractionalOffset alignment: FractionalOffset.center
FractionalOffset alignment: FractionalOffset.center,
TextDirection textDirection,
}) : _widthFactor = widthFactor,
_heightFactor = heightFactor,
super(child: child, alignment: alignment) {
super(child: child, alignment: alignment, textDirection: textDirection) {
assert(_widthFactor == null || _widthFactor >= 0.0);
assert(_heightFactor == null || _heightFactor >= 0.0);
}
......
......@@ -1187,7 +1187,7 @@ class Align extends SingleChildRenderObjectWidget {
/// edge of the parent. Other values interpolate (and extrapolate) linearly.
/// For example, a value of 0.5 means that the center of the child is aligned
/// with the center of the parent.
final FractionalOffset alignment;
final FractionalOffsetGeometry alignment;
/// If non-null, sets its width to the child's width multipled by this factor.
///
......@@ -1200,20 +1200,28 @@ class Align extends SingleChildRenderObjectWidget {
final double heightFactor;
@override
RenderPositionedBox createRenderObject(BuildContext context) => new RenderPositionedBox(alignment: alignment, widthFactor: widthFactor, heightFactor: heightFactor);
RenderPositionedBox createRenderObject(BuildContext context) {
return new RenderPositionedBox(
alignment: alignment,
widthFactor: widthFactor,
heightFactor: heightFactor,
textDirection: Directionality.of(context),
);
}
@override
void updateRenderObject(BuildContext context, RenderPositionedBox renderObject) {
renderObject
..alignment = alignment
..widthFactor = widthFactor
..heightFactor = heightFactor;
..heightFactor = heightFactor
..textDirection = Directionality.of(context);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<FractionalOffset>('alignment', alignment));
description.add(new DiagnosticsProperty<FractionalOffsetGeometry>('alignment', alignment));
description.add(new DoubleProperty('widthFactor', widthFactor, defaultValue: null));
description.add(new DoubleProperty('heightFactor', heightFactor, defaultValue: null));
}
......@@ -1539,7 +1547,7 @@ class FractionallySizedBox extends SingleChildRenderObjectWidget {
this.alignment: FractionalOffset.center,
this.widthFactor,
this.heightFactor,
Widget child
Widget child,
}) : assert(alignment != null),
assert(widthFactor == null || widthFactor >= 0.0),
assert(heightFactor == null || heightFactor >= 0.0),
......@@ -1572,14 +1580,15 @@ class FractionallySizedBox extends SingleChildRenderObjectWidget {
/// edge of the parent. Other values interpolate (and extrapolate) linearly.
/// For example, a value of 0.5 means that the center of the child is aligned
/// with the center of the parent.
final FractionalOffset alignment;
final FractionalOffsetGeometry alignment;
@override
RenderFractionallySizedOverflowBox createRenderObject(BuildContext context) {
return new RenderFractionallySizedOverflowBox(
alignment: alignment,
widthFactor: widthFactor,
heightFactor: heightFactor
heightFactor: heightFactor,
textDirection: Directionality.of(context),
);
}
......@@ -1588,13 +1597,14 @@ class FractionallySizedBox extends SingleChildRenderObjectWidget {
renderObject
..alignment = alignment
..widthFactor = widthFactor
..heightFactor = heightFactor;
..heightFactor = heightFactor
..textDirection = Directionality.of(context);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<FractionalOffset>('alignment', alignment));
description.add(new DiagnosticsProperty<FractionalOffsetGeometry>('alignment', alignment));
description.add(new DoubleProperty('widthFactor', widthFactor, defaultValue: null));
description.add(new DoubleProperty('heightFactor', heightFactor, defaultValue: null));
}
......@@ -1680,7 +1690,7 @@ class OverflowBox extends SingleChildRenderObjectWidget {
this.maxWidth,
this.minHeight,
this.maxHeight,
Widget child
Widget child,
}) : super(key: key, child: child);
/// How to align the child.
......@@ -1692,7 +1702,7 @@ class OverflowBox extends SingleChildRenderObjectWidget {
/// edge of the parent. Other values interpolate (and extrapolate) linearly.
/// For example, a value of 0.5 means that the center of the child is aligned
/// with the center of the parent.
final FractionalOffset alignment;
final FractionalOffsetGeometry alignment;
/// The minimum width constraint to give the child. Set this to null (the
/// default) to use the constraint from the parent instead.
......@@ -1717,7 +1727,8 @@ class OverflowBox extends SingleChildRenderObjectWidget {
minWidth: minWidth,
maxWidth: maxWidth,
minHeight: minHeight,
maxHeight: maxHeight
maxHeight: maxHeight,
textDirection: Directionality.of(context),
);
}
......@@ -1728,13 +1739,14 @@ class OverflowBox extends SingleChildRenderObjectWidget {
..minWidth = minWidth
..maxWidth = maxWidth
..minHeight = minHeight
..maxHeight = maxHeight;
..maxHeight = maxHeight
..textDirection = Directionality.of(context);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<FractionalOffset>('alignment', alignment));
description.add(new DiagnosticsProperty<FractionalOffsetGeometry>('alignment', alignment));
description.add(new DoubleProperty('minWidth', minWidth, defaultValue: null));
description.add(new DoubleProperty('maxWidth', maxWidth, defaultValue: null));
description.add(new DoubleProperty('minHeight', minHeight, defaultValue: null));
......@@ -1752,7 +1764,7 @@ class SizedOverflowBox extends SingleChildRenderObjectWidget {
Key key,
@required this.size,
this.alignment: FractionalOffset.center,
Widget child
Widget child,
}) : assert(size != null),
assert(alignment != null),
super(key: key, child: child);
......@@ -1766,7 +1778,7 @@ class SizedOverflowBox extends SingleChildRenderObjectWidget {
/// edge of the parent. Other values interpolate (and extrapolate) linearly.
/// For example, a value of 0.5 means that the center of the child is aligned
/// with the center of the parent.
final FractionalOffset alignment;
final FractionalOffsetGeometry alignment;
/// The size this widget should attempt to be.
final Size size;
......@@ -1775,7 +1787,8 @@ class SizedOverflowBox extends SingleChildRenderObjectWidget {
RenderSizedOverflowBox createRenderObject(BuildContext context) {
return new RenderSizedOverflowBox(
alignment: alignment,
requestedSize: size
requestedSize: size,
textDirection: Directionality.of(context),
);
}
......@@ -1783,13 +1796,14 @@ class SizedOverflowBox extends SingleChildRenderObjectWidget {
void updateRenderObject(BuildContext context, RenderSizedOverflowBox renderObject) {
renderObject
..alignment = alignment
..requestedSize = size;
..requestedSize = size
..textDirection = Directionality.of(context);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<FractionalOffset>('alignment', alignment));
description.add(new DiagnosticsProperty<FractionalOffsetGeometry>('alignment', alignment));
description.add(new DiagnosticsProperty<Size>('size', size, defaultValue: null));
}
}
......
......@@ -278,7 +278,7 @@ class Container extends StatelessWidget {
/// constraints are unbounded, then the child will be shrink-wrapped instead.
///
/// Ignored if [child] is null.
final FractionalOffset alignment;
final FractionalOffsetGeometry alignment;
/// Empty space to inscribe inside the [decoration]. The [child], if any, is
/// placed inside this padding.
......@@ -362,7 +362,7 @@ class Container extends StatelessWidget {
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<FractionalOffset>('alignment', alignment, showName: false, defaultValue: null));
description.add(new DiagnosticsProperty<FractionalOffsetGeometry>('alignment', alignment, showName: false, defaultValue: null));
description.add(new DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
description.add(new DiagnosticsProperty<Decoration>('bg', decoration, defaultValue: null));
description.add(new DiagnosticsProperty<Decoration>('fg', foregroundDecoration, defaultValue: null));
......
......@@ -42,7 +42,7 @@ class TestTextState extends State<TestText> {
}
void main() {
testWidgets('ListTile geometry', (WidgetTester tester) async {
testWidgets('ListTile geometry (LTR)', (WidgetTester tester) async {
// See https://material.io/guidelines/components/lists.html
bool hasSubtitle;
......@@ -125,6 +125,33 @@ void main() {
testVerticalGeometry(76.0);
});
testWidgets('ListTile geometry (RTL)', (WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.rtl,
child: new Material(
child: new Center(
child: new ListTile(
leading: const Text('leading'),
title: const Text('title'),
trailing: const Text('trailing'),
),
),
),
));
double left(String text) => tester.getTopLeft(find.text(text)).dx;
double right(String text) => tester.getTopRight(find.text(text)).dx;
void testHorizontalGeometry() {
expect(right('leading'), 800.0 - 16.0);
expect(right('title'), 800.0 - 72.0);
expect(left('leading') - right('title'), 16.0);
expect(left('trailing'), 16.0);
}
testHorizontalGeometry();
});
testWidgets('ListTile.divideTiles', (WidgetTester tester) async {
final List<String> titles = <String>[ 'first', 'second', 'third' ];
......
......@@ -11,18 +11,66 @@ void main() {
await tester.pumpWidget(
new Align(
child: new Container(),
alignment: const FractionalOffset(0.75, 0.75)
)
alignment: const FractionalOffset(0.75, 0.75),
),
);
await tester.pumpWidget(
new Align(
child: new Container(),
alignment: const FractionalOffset(0.5, 0.5)
)
alignment: const FractionalOffset(0.5, 0.5),
),
);
});
testWidgets('Align control test (LTR)', (WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new Align(
child: new Container(width: 100.0, height: 80.0),
alignment: FractionalOffsetDirectional.topStart,
),
));
expect(tester.getTopLeft(find.byType(Container)).dx, 0.0);
expect(tester.getBottomRight(find.byType(Container)).dx, 100.0);
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new Align(
child: new Container(width: 100.0, height: 80.0),
alignment: FractionalOffset.topLeft,
),
));
expect(tester.getTopLeft(find.byType(Container)).dx, 0.0);
expect(tester.getBottomRight(find.byType(Container)).dx, 100.0);
});
testWidgets('Align control test (RTL)', (WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.rtl,
child: new Align(
child: new Container(width: 100.0, height: 80.0),
alignment: FractionalOffsetDirectional.topStart,
),
));
expect(tester.getTopLeft(find.byType(Container)).dx, 700.0);
expect(tester.getBottomRight(find.byType(Container)).dx, 800.0);
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new Align(
child: new Container(width: 100.0, height: 80.0),
alignment: FractionalOffset.topLeft,
),
));
expect(tester.getTopLeft(find.byType(Container)).dx, 0.0);
expect(tester.getBottomRight(find.byType(Container)).dx, 100.0);
});
testWidgets('Shrink wraps in finite space', (WidgetTester tester) async {
final GlobalKey alignKey = new GlobalKey();
await tester.pumpWidget(
......@@ -33,9 +81,9 @@ void main() {
width: 10.0,
height: 10.0
),
alignment: const FractionalOffset(0.50, 0.50)
)
)
alignment: const FractionalOffset(0.50, 0.50),
),
),
);
final Size size = alignKey.currentContext.size;
......
......@@ -60,7 +60,6 @@ void main() {
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
' │ size: Size(63.0, 88.0)\n'
' │ padding: EdgeInsets.all(5.0)\n'
' │ textDirection: null\n'
' │\n'
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
' │ creator: ConstrainedBox ← Padding ← Container ← Align ← [root]\n'
......@@ -100,7 +99,6 @@ void main() {
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
' │ size: Size(53.0, 78.0)\n'
' │ padding: EdgeInsets.all(7.0)\n'
' │ textDirection: null\n'
' │\n'
' └─child: RenderPositionedBox#00000\n'
' │ creator: Align ← Padding ← DecoratedBox ← DecoratedBox ←\n'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment