Commit 9cdfb95b authored by Adam Barth's avatar Adam Barth Committed by Ian Hickson

Add RTL support to AppBar (#11834)

Fixes #11381
parent 7ae4de70
......@@ -387,7 +387,7 @@ class _AppBarState extends State<AppBar> {
}
final Widget toolbar = new Padding(
padding: const EdgeInsets.only(right: 4.0),
padding: const EdgeInsetsDirectional.only(end: 4.0),
child: new NavigationToolbar(
leading: leading,
middle: title,
......
......@@ -4,6 +4,7 @@
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart';
......@@ -59,9 +60,12 @@ class NavigationToolbar extends StatelessWidget {
if (trailing != null)
children.add(new LayoutId(id: _ToolbarSlot.trailing, child: trailing));
final TextDirection textDirection = Directionality.of(context);
assert(textDirection != null);
return new CustomMultiChildLayout(
delegate: new _ToolbarLayout(
centerMiddle: centerMiddle,
textDirection: textDirection,
),
children: children,
);
......@@ -76,17 +80,20 @@ enum _ToolbarSlot {
const double _kMiddleMargin = 16.0;
// TODO(xster): support RTL.
class _ToolbarLayout extends MultiChildLayoutDelegate {
_ToolbarLayout({ this.centerMiddle });
_ToolbarLayout({
this.centerMiddle,
@required this.textDirection,
}) : assert(textDirection != null);
// If false the middle widget should be left justified within the space
// If false the middle widget should be start-justified within the space
// between the leading and trailing widgets.
// If true the middle widget is centered within the toolbar (not within the horizontal
// space between the leading and trailing widgets).
// TODO(xster): document RTL once supported.
final bool centerMiddle;
final TextDirection textDirection;
@override
void performLayout(Size size) {
double leadingWidth = 0.0;
......@@ -100,16 +107,33 @@ class _ToolbarLayout extends MultiChildLayoutDelegate {
maxHeight: size.height,
);
leadingWidth = layoutChild(_ToolbarSlot.leading, constraints).width;
positionChild(_ToolbarSlot.leading, Offset.zero);
double leadingX;
switch (textDirection) {
case TextDirection.rtl:
leadingX = size.width - leadingWidth;
break;
case TextDirection.ltr:
leadingX = 0.0;
break;
}
positionChild(_ToolbarSlot.leading, new Offset(leadingX, 0.0));
}
if (hasChild(_ToolbarSlot.trailing)) {
final BoxConstraints constraints = new BoxConstraints.loose(size);
final Size trailingSize = layoutChild(_ToolbarSlot.trailing, constraints);
final double trailingLeft = size.width - trailingSize.width;
final double trailingTop = (size.height - trailingSize.height) / 2.0;
double trailingX;
switch (textDirection) {
case TextDirection.rtl:
trailingX = 0.0;
break;
case TextDirection.ltr:
trailingX = size.width - trailingSize.width;
break;
}
final double trailingY = (size.height - trailingSize.height) / 2.0;
trailingWidth = trailingSize.width;
positionChild(_ToolbarSlot.trailing, new Offset(trailingLeft, trailingTop));
positionChild(_ToolbarSlot.trailing, new Offset(trailingX, trailingY));
}
if (hasChild(_ToolbarSlot.middle)) {
......@@ -117,17 +141,27 @@ class _ToolbarLayout extends MultiChildLayoutDelegate {
final BoxConstraints constraints = new BoxConstraints.loose(size).copyWith(maxWidth: maxWidth);
final Size middleSize = layoutChild(_ToolbarSlot.middle, constraints);
final double middleLeftMargin = leadingWidth + _kMiddleMargin;
double middleX = middleLeftMargin;
final double middleStartMargin = leadingWidth + _kMiddleMargin;
double middleStart = middleStartMargin;
final double middleY = (size.height - middleSize.height) / 2.0;
// If the centered middle will not fit between the leading and trailing
// widgets, then align its left or right edge with the adjacent boundary.
if (centerMiddle) {
middleX = (size.width - middleSize.width) / 2.0;
if (middleX + middleSize.width > size.width - trailingWidth)
middleX = size.width - trailingWidth - middleSize.width;
else if (middleX < middleLeftMargin)
middleX = middleLeftMargin;
middleStart = (size.width - middleSize.width) / 2.0;
if (middleStart + middleSize.width > size.width - trailingWidth)
middleStart = size.width - trailingWidth - middleSize.width;
else if (middleStart < middleStartMargin)
middleStart = middleStartMargin;
}
double middleX;
switch (textDirection) {
case TextDirection.rtl:
middleX = size.width - middleSize.width - middleStart;
break;
case TextDirection.ltr:
middleX = middleStart;
break;
}
positionChild(_ToolbarSlot.middle, new Offset(middleX, middleY));
......@@ -135,5 +169,8 @@ class _ToolbarLayout extends MultiChildLayoutDelegate {
}
@override
bool shouldRelayout(_ToolbarLayout oldDelegate) => centerMiddle != oldDelegate.centerMiddle;
bool shouldRelayout(_ToolbarLayout oldDelegate) {
return oldDelegate.centerMiddle != centerMiddle
|| oldDelegate.textDirection != textDirection;
}
}
......@@ -157,7 +157,7 @@ void main() {
expect(center.dx, lessThan(400 + size.width / 2.0));
});
testWidgets('AppBar centerTitle:false title left edge is 16.0 ', (WidgetTester tester) async {
testWidgets('AppBar centerTitle:false title start edge is 16.0 (LTR)', (WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
home: new Scaffold(
......@@ -172,8 +172,26 @@ void main() {
expect(tester.getTopLeft(find.text('X')).dx, 16.0);
});
testWidgets('AppBar centerTitle:false title start edge is 16.0 (RTL)', (WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
home: new Directionality(
textDirection: TextDirection.rtl,
child: new Scaffold(
appBar: new AppBar(
centerTitle: false,
title: const Text('X'),
),
),
),
),
);
expect(tester.getTopRight(find.text('X')).dx, 800.0 - 16.0);
});
testWidgets(
'AppBar centerTitle:false leading button title left edge is 72.0 ',
'AppBar centerTitle:false leading button title left edge is 72.0 (LTR)',
(WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
......@@ -191,6 +209,28 @@ void main() {
expect(tester.getTopLeft(find.text('X')).dx, 72.0);
});
testWidgets(
'AppBar centerTitle:false leading button title left edge is 72.0 (RTL)',
(WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
home: new Directionality(
textDirection: TextDirection.rtl,
child: new Scaffold(
appBar: new AppBar(
centerTitle: false,
title: const Text('X'),
),
// A drawer causes a leading hamburger.
drawer: const Drawer(),
),
),
),
);
expect(tester.getTopRight(find.text('X')).dx, 800.0 - 72.0);
});
testWidgets('AppBar centerTitle:false title overflow OK ', (WidgetTester tester) async {
// The app bar's title should be constrained to fit within the available space
// between the leading and actions widgets.
......@@ -249,10 +289,10 @@ void main() {
expect(tester.getSize(title).width, equals(800.0 - 4.0 - 56.0 - 16.0 - 16.0 - 200.0));
});
testWidgets('AppBar centerTitle:true title overflow OK ', (WidgetTester tester) async {
testWidgets('AppBar centerTitle:true title overflow OK (LTR)', (WidgetTester tester) async {
// The app bar's title should be constrained to fit within the available space
// between the leading and actions widgets. When it's also centered it may
// also be left or right justified if it doesn't fit in the overall center.
// also be start or end justified if it doesn't fit in the overall center.
final Key titleKey = new UniqueKey();
double titleWidth = 700.0;
......@@ -276,8 +316,8 @@ void main() {
}
// Centering a title with width 700 within the 800 pixel wide test widget
// would mean that its left edge would have to be 50. The material spec says
// that the left edge of the title must be atleast 72.
// would mean that its start edge would have to be 50. The material spec says
// that the start edge of the title must be atleast 72.
await tester.pumpWidget(buildApp());
final Finder title = find.byKey(titleKey);
......@@ -285,9 +325,9 @@ void main() {
expect(tester.getSize(title).width, equals(700.0));
// Centering a title with width 620 within the 800 pixel wide test widget
// would mean that its left edge would have to be 90. We reserve 72
// on the left and the padded actions occupy 96 + 4 on the right. That
// leaves 628, so the title is right justified but its width isn't changed.
// would mean that its start edge would have to be 90. We reserve 72
// on the start and the padded actions occupy 96 + 4 on the end. That
// leaves 628, so the title is end justified but its width isn't changed.
await tester.pumpWidget(buildApp());
leading = null;
......@@ -301,6 +341,61 @@ void main() {
expect(tester.getSize(title).width, equals(620.0));
});
testWidgets('AppBar centerTitle:true title overflow OK (RTL)', (WidgetTester tester) async {
// The app bar's title should be constrained to fit within the available space
// between the leading and actions widgets. When it's also centered it may
// also be start or end justified if it doesn't fit in the overall center.
final Key titleKey = new UniqueKey();
double titleWidth = 700.0;
Widget leading = new Container();
List<Widget> actions;
Widget buildApp() {
return new MaterialApp(
home: new Directionality(
textDirection: TextDirection.rtl,
child: new Scaffold(
appBar: new AppBar(
leading: leading,
centerTitle: true,
title: new Container(
key: titleKey,
constraints: new BoxConstraints.loose(new Size(titleWidth, 1000.0)),
),
actions: actions,
),
),
),
);
}
// Centering a title with width 700 within the 800 pixel wide test widget
// would mean that its start edge would have to be 50. The material spec says
// that the start edge of the title must be atleast 72.
await tester.pumpWidget(buildApp());
final Finder title = find.byKey(titleKey);
expect(tester.getTopRight(title).dx, 800.0 - 72.0);
expect(tester.getSize(title).width, equals(700.0));
// Centering a title with width 620 within the 800 pixel wide test widget
// would mean that its start edge would have to be 90. We reserve 72
// on the start and the padded actions occupy 96 + 4 on the end. That
// leaves 628, so the title is end justified but its width isn't changed.
await tester.pumpWidget(buildApp());
leading = null;
titleWidth = 620.0;
actions = <Widget>[
const SizedBox(width: 48.0),
const SizedBox(width: 48.0)
];
await tester.pumpWidget(buildApp());
expect(tester.getTopRight(title).dx, 620 + 48 + 48 + 4);
expect(tester.getSize(title).width, equals(620.0));
});
testWidgets('AppBar with no Scaffold', (WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
......
......@@ -21,10 +21,10 @@ class _CustomPhysics extends ClampingScrollPhysics {
}
Widget buildTest({ ScrollController controller, String title:'TTTTTTTT' }) {
return new MediaQuery(
data: const MediaQueryData(),
child: new Directionality(
textDirection: TextDirection.ltr,
return new Directionality(
textDirection: TextDirection.ltr,
child: new MediaQuery(
data: const MediaQueryData(),
child: new Scaffold(
body: new DefaultTabController(
length: 4,
......@@ -307,20 +307,24 @@ void main() {
});
testWidgets('NestedScrollViews with custom physics', (WidgetTester tester) async {
await tester.pumpWidget(new MediaQuery(
data: const MediaQueryData(),
child: new NestedScrollView(
physics: const _CustomPhysics(),
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
const SliverAppBar(
floating: true,
title: const Text('AA'),
),
];
},
body: new Container(),
)));
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new MediaQuery(
data: const MediaQueryData(),
child: new NestedScrollView(
physics: const _CustomPhysics(),
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
const SliverAppBar(
floating: true,
title: const Text('AA'),
),
];
},
body: new Container(),
),
),
));
expect(find.text('AA'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 500));
final Offset point1 = tester.getCenter(find.text('AA'));
......
......@@ -23,8 +23,9 @@ void main() {
child: new Text('Item $i'),
);
});
await tester.pumpWidget(
new MediaQuery(
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new MediaQuery(
data: const MediaQueryData(),
child: new CustomScrollView(
controller: scrollController,
......@@ -39,6 +40,7 @@ void main() {
),
],
),
),
));
// AppBar is child of node with semantic scroll actions.
......@@ -293,8 +295,9 @@ void main() {
);
});
final ScrollController controller = new ScrollController(initialScrollOffset: 280.0);
await tester.pumpWidget(
new MediaQuery(
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new MediaQuery(
data: const MediaQueryData(),
child: new CustomScrollView(
slivers: <Widget>[
......@@ -309,7 +312,7 @@ void main() {
controller: controller,
),
),
);
));
// 'Item 0' is covered by app bar.
expect(semantics, isNot(includesNodeWith(label: 'Item 0')));
......@@ -375,8 +378,9 @@ void main() {
),
);
});
await tester.pumpWidget(
new MediaQuery(
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new MediaQuery(
data: const MediaQueryData(),
child: new CustomScrollView(
controller: controller,
......@@ -388,7 +392,7 @@ void main() {
]..addAll(slivers),
),
),
);
));
// 'Item 0' is covered by app bar.
expect(semantics, isNot(includesNodeWith(label: 'Item 0')));
......@@ -452,8 +456,9 @@ void main() {
);
});
final ScrollController controller = new ScrollController(initialScrollOffset: 280.0);
await tester.pumpWidget(
new MediaQuery(
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new MediaQuery(
data: const MediaQueryData(),
child: new CustomScrollView(
reverse: true, // This is the important setting for this test.
......@@ -469,7 +474,7 @@ void main() {
controller: controller,
),
),
);
));
// 'Item 0' is covered by app bar.
expect(semantics, isNot(includesNodeWith(label: 'Item 0')));
......@@ -536,8 +541,9 @@ void main() {
),
);
});
await tester.pumpWidget(
new MediaQuery(
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new MediaQuery(
data: const MediaQueryData(),
child: new CustomScrollView(
reverse: true, // This is the important setting for this test.
......@@ -550,7 +556,7 @@ void main() {
]..addAll(slivers),
),
),
);
));
// 'Item 0' is covered by app bar.
expect(semantics, isNot(includesNodeWith(label: 'Item 0')));
......@@ -622,8 +628,9 @@ void main() {
child: new Text('Backward Item $i'),
);
});
await tester.pumpWidget(
new MediaQuery(
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new MediaQuery(
data: const MediaQueryData(),
child: new Scrollable(
controller: controller,
......@@ -658,7 +665,7 @@ void main() {
},
),
),
);
));
// 'Forward Item 0' is covered by app bar.
expect(semantics, isNot(includesNodeWith(label: 'Forward Item 0')));
......
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