diff --git a/examples/flutter_gallery/lib/demo/material/scrollable_tabs_demo.dart b/examples/flutter_gallery/lib/demo/material/scrollable_tabs_demo.dart index e7c9092e1d..48e1ae78be 100644 --- a/examples/flutter_gallery/lib/demo/material/scrollable_tabs_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/scrollable_tabs_demo.dart @@ -11,18 +11,26 @@ enum TabsDemoStyle { } class _Page { - _Page({ this.icon, this.text }); + const _Page({ this.icon, this.text }); final IconData icon; final String text; } -final List<_Page> _allPages = <_Page>[ - new _Page(icon: Icons.event, text: 'EVENT'), - new _Page(icon: Icons.home, text: 'HOME'), - new _Page(icon: Icons.android, text: 'ANDROID'), - new _Page(icon: Icons.alarm, text: 'ALARM'), - new _Page(icon: Icons.face, text: 'FACE'), - new _Page(icon: Icons.language, text: 'LANGUAGE'), +const List<_Page> _allPages = const <_Page>[ + const _Page(icon: Icons.grade, text: 'TRIUMPH'), + const _Page(icon: Icons.playlist_add, text: 'NOTE'), + const _Page(icon: Icons.check_circle, text: 'SUCCESS'), + const _Page(icon: Icons.question_answer, text: 'OVERSTATE'), + const _Page(icon: Icons.sentiment_very_satisfied, text: 'SATISFACTION'), + const _Page(icon: Icons.camera, text: 'APERTURE'), + const _Page(icon: Icons.assignment_late, text: 'WE MUST'), + const _Page(icon: Icons.assignment_turned_in, text: 'WE CAN'), + const _Page(icon: Icons.group, text: 'ALL'), + const _Page(icon: Icons.block, text: 'EXCEPT'), + const _Page(icon: Icons.sentiment_very_dissatisfied, text: 'CRYING'), + const _Page(icon: Icons.error, text: 'MISTAKE'), + const _Page(icon: Icons.loop, text: 'TRYING'), + const _Page(icon: Icons.cake, text: 'CAKE'), ]; class ScrollableTabsDemo extends StatefulWidget { diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index e8d57e56fd..ebe1e3c174 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -155,18 +155,20 @@ class _TabStyle extends AnimatedWidget { } } +typedef void _LayoutCallback(List xOffsets, TextDirection textDirection, double width); + class _TabLabelBarRenderer extends RenderFlex { _TabLabelBarRenderer({ List children, - Axis direction, - MainAxisSize mainAxisSize, - MainAxisAlignment mainAxisAlignment, - CrossAxisAlignment crossAxisAlignment, - TextDirection textDirection, - VerticalDirection verticalDirection, - TextBaseline textBaseline, + @required Axis direction, + @required MainAxisSize mainAxisSize, + @required MainAxisAlignment mainAxisAlignment, + @required CrossAxisAlignment crossAxisAlignment, + @required TextDirection textDirection, + @required VerticalDirection verticalDirection, @required this.onPerformLayout, }) : assert(onPerformLayout != null), + assert(textDirection != null), super( children: children, direction: direction, @@ -175,14 +177,17 @@ class _TabLabelBarRenderer extends RenderFlex { crossAxisAlignment: crossAxisAlignment, textDirection: textDirection, verticalDirection: verticalDirection, - textBaseline: textBaseline, ); - ValueChanged> onPerformLayout; + _LayoutCallback onPerformLayout; @override void performLayout() { super.performLayout(); + // xOffsets will contain childCount+1 values, giving the offsets of the + // leading edge of the first tab as the first value, of the leading edge of + // the each subsequent tab as each subsequent value, and of the trailing + // edge of the last tab as the last value. RenderBox child = firstChild; final List xOffsets = []; while (child != null) { @@ -191,8 +196,16 @@ class _TabLabelBarRenderer extends RenderFlex { assert(child.parentData == childParentData); child = childParentData.nextSibling; } - xOffsets.add(size.width); // So xOffsets[lastTabIndex + 1] is valid. - onPerformLayout(xOffsets); + assert(textDirection != null); + switch (textDirection) { + case TextDirection.rtl: + xOffsets.insert(0, size.width); + break; + case TextDirection.ltr: + xOffsets.add(size.width); + break; + } + onPerformLayout(xOffsets, textDirection, size.width); } } @@ -202,10 +215,6 @@ class _TabLabelBarRenderer extends RenderFlex { class _TabLabelBar extends Flex { _TabLabelBar({ Key key, - MainAxisAlignment mainAxisAlignment, - CrossAxisAlignment crossAxisAlignment, - TextDirection textDirection, - VerticalDirection verticalDirection: VerticalDirection.down, List children: const [], this.onPerformLayout, }) : super( @@ -215,11 +224,10 @@ class _TabLabelBar extends Flex { mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, - textDirection: textDirection, - verticalDirection: verticalDirection, + verticalDirection: VerticalDirection.down, ); - final ValueChanged> onPerformLayout; + final _LayoutCallback onPerformLayout; @override RenderFlex createRenderObject(BuildContext context) { @@ -230,7 +238,6 @@ class _TabLabelBar extends Flex { crossAxisAlignment: crossAxisAlignment, textDirection: getEffectiveTextDirection(context), verticalDirection: verticalDirection, - textBaseline: textBaseline, onPerformLayout: onPerformLayout, ); } @@ -258,29 +265,66 @@ double _indexChangeProgress(TabController controller) { class _IndicatorPainter extends CustomPainter { _IndicatorPainter({ - this.controller, - this.indicatorWeight, - this.indicatorPadding, - List initialTabOffsets, - }) : _tabOffsets = initialTabOffsets, super(repaint: controller.animation); + @required this.controller, + @required this.indicatorWeight, + @required this.indicatorPadding, + _IndicatorPainter old, + }) : assert(controller != null), + assert(indicatorWeight != null), + assert(indicatorPadding != null), + super(repaint: controller.animation) { + if (old != null) + saveTabOffsets(old._currentTabOffsets, old._currentTextDirection); + } final TabController controller; final double indicatorWeight; - final EdgeInsets indicatorPadding; - List _tabOffsets; + final EdgeInsetsGeometry indicatorPadding; + + List _currentTabOffsets; + TextDirection _currentTextDirection; + EdgeInsets _resolvedIndicatorPadding; + Color _color; Rect _currentRect; - // _tabOffsets[index] is the offset of the left edge of the tab at index, and - // _tabOffsets[_tabOffsets.length] is the right edge of the last tab. - int get maxTabIndex => _tabOffsets.length - 2; + void saveTabOffsets(List tabOffsets, TextDirection textDirection) { + _currentTabOffsets = tabOffsets; + _currentTextDirection = textDirection; + _resolvedIndicatorPadding = indicatorPadding.resolve(_currentTextDirection); + } + + // _currentTabOffsets[index] is the offset of the start edge of the tab at index, and + // _currentTabOffsets[_currentTabOffsets.length] is the end edge of the last tab. + int get maxTabIndex => _currentTabOffsets.length - 2; + + double centerOf(int tabIndex) { + assert(_currentTabOffsets != null); + assert(_currentTabOffsets.isNotEmpty); + assert(tabIndex >= 0); + assert(tabIndex <= maxTabIndex); + return (_currentTabOffsets[tabIndex] + _currentTabOffsets[tabIndex + 1]) / 2.0; + } Rect indicatorRect(Size tabBarSize, int tabIndex) { - assert(_tabOffsets != null && tabIndex >= 0 && tabIndex <= maxTabIndex); - double tabLeft = _tabOffsets[tabIndex]; - double tabRight = _tabOffsets[tabIndex + 1]; - tabLeft = math.min(tabLeft + indicatorPadding.left, tabRight); - tabRight = math.max(tabRight - indicatorPadding.right, tabLeft); + assert(_currentTabOffsets != null); + assert(_currentTextDirection != null); + assert(_currentTabOffsets.isNotEmpty); + assert(tabIndex >= 0); + assert(tabIndex <= maxTabIndex); + double tabLeft, tabRight; + switch (_currentTextDirection) { + case TextDirection.rtl: + tabLeft = _currentTabOffsets[tabIndex + 1]; + tabRight = _currentTabOffsets[tabIndex]; + break; + case TextDirection.ltr: + tabLeft = _currentTabOffsets[tabIndex]; + tabRight = _currentTabOffsets[tabIndex + 1]; + break; + } + tabLeft = math.min(tabLeft + _resolvedIndicatorPadding.left, tabRight); + tabRight = math.max(tabRight - _resolvedIndicatorPadding.right, tabLeft); final double tabTop = tabBarSize.height - indicatorWeight; return new Rect.fromLTWH(tabLeft, tabTop, tabRight - tabLeft, indicatorWeight); } @@ -288,46 +332,49 @@ class _IndicatorPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { if (controller.indexIsChanging) { + // The user tapped on a tab, the tab controller's animation is running. final Rect targetRect = indicatorRect(size, controller.index); _currentRect = Rect.lerp(targetRect, _currentRect ?? targetRect, _indexChangeProgress(controller)); } else { + // The user is dragging the TabBarView's PageView left or right. final int currentIndex = controller.index; - final Rect left = currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null; + final Rect previous = currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null; final Rect middle = indicatorRect(size, currentIndex); - final Rect right = currentIndex < maxTabIndex ? indicatorRect(size, currentIndex + 1) : null; - + final Rect next = currentIndex < maxTabIndex ? indicatorRect(size, currentIndex + 1) : null; final double index = controller.index.toDouble(); final double value = controller.animation.value; if (value == index - 1.0) - _currentRect = left ?? middle; + _currentRect = previous ?? middle; else if (value == index + 1.0) - _currentRect = right ?? middle; + _currentRect = next ?? middle; else if (value == index) _currentRect = middle; else if (value < index) - _currentRect = left == null ? middle : Rect.lerp(middle, left, index - value); + _currentRect = previous == null ? middle : Rect.lerp(middle, previous, index - value); else - _currentRect = right == null ? middle : Rect.lerp(middle, right, value - index); + _currentRect = next == null ? middle : Rect.lerp(middle, next, value - index); } assert(_currentRect != null); canvas.drawRect(_currentRect, new Paint()..color = _color); } - static bool _tabOffsetsNotEqual(List a, List b) { - assert(a != null && b != null && a.length == b.length); + static bool _tabOffsetsEqual(List a, List b) { + if (a?.length != b?.length) + return false; for (int i = 0; i < a.length; i += 1) { if (a[i] != b[i]) - return true; + return false; } - return false; + return true; } @override bool shouldRepaint(_IndicatorPainter old) { - return controller != old.controller || - _tabOffsets?.length != old._tabOffsets?.length || - _tabOffsetsNotEqual(_tabOffsets, old._tabOffsets) || - _currentRect != old._currentRect; + return controller != old.controller + || indicatorWeight != old.indicatorWeight + || indicatorPadding != old.indicatorPadding + || (!_tabOffsetsEqual(_currentTabOffsets, old._currentTabOffsets)) + || _currentTextDirection != old._currentTextDirection; } } @@ -488,7 +535,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { /// [indicatorPadding] are ignored. /// /// The default value of [indicatorPadding] is [EdgeInsets.zero]. - final EdgeInsets indicatorPadding; + final EdgeInsetsGeometry indicatorPadding; /// The color of selected tab labels. /// @@ -540,10 +587,10 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { class _TabBarState extends State { ScrollController _scrollController; - TabController _controller; _IndicatorPainter _indicatorPainter; int _currentIndex; + double _tabStripWidth; void _updateTabController() { final TabController newController = widget.controller ?? DefaultTabController.of(context); @@ -571,12 +618,11 @@ class _TabBarState extends State { _controller.animation.addListener(_handleTabControllerAnimationTick); _controller.addListener(_handleTabControllerTick); _currentIndex = _controller.index; - final List offsets = _indicatorPainter?._tabOffsets; _indicatorPainter = new _IndicatorPainter( controller: _controller, indicatorWeight: widget.indicatorWeight, indicatorPadding: widget.indicatorPadding, - initialTabOffsets: offsets, + old: _indicatorPainter, ); } } @@ -604,16 +650,19 @@ class _TabBarState extends State { super.dispose(); } - // _tabOffsets[index] is the offset of the left edge of the tab at index, and - // _tabOffsets[_tabOffsets.length] is the right edge of the last tab. - int get maxTabIndex => _indicatorPainter._tabOffsets.length - 2; + int get maxTabIndex => _indicatorPainter.maxTabIndex; double _tabScrollOffset(int index, double viewportWidth, double minExtent, double maxExtent) { if (!widget.isScrollable) return 0.0; - final List tabOffsets = _indicatorPainter._tabOffsets; - assert(tabOffsets != null && index >= 0 && index <= maxTabIndex); - final double tabCenter = (tabOffsets[index] + tabOffsets[index + 1]) / 2.0; + double tabCenter = _indicatorPainter.centerOf(index); + switch (Directionality.of(context)) { + case TextDirection.rtl: + tabCenter = _tabStripWidth - tabCenter; + break; + case TextDirection.ltr: + break; + } return (tabCenter - viewportWidth / 2.0).clamp(minExtent, maxExtent); } @@ -632,23 +681,23 @@ class _TabBarState extends State { } void _scrollToControllerValue() { - final double left = _currentIndex > 0 ? _tabCenteredScrollOffset(_currentIndex - 1) : null; - final double middle = _tabCenteredScrollOffset(_currentIndex); - final double right = _currentIndex < maxTabIndex ? _tabCenteredScrollOffset(_currentIndex + 1) : null; + final double leadingPosition = _currentIndex > 0 ? _tabCenteredScrollOffset(_currentIndex - 1) : null; + final double middlePosition = _tabCenteredScrollOffset(_currentIndex); + final double trailingPosition = _currentIndex < maxTabIndex ? _tabCenteredScrollOffset(_currentIndex + 1) : null; final double index = _controller.index.toDouble(); final double value = _controller.animation.value; double offset; if (value == index - 1.0) - offset = left ?? middle; + offset = leadingPosition ?? middlePosition; else if (value == index + 1.0) - offset = right ?? middle; + offset = trailingPosition ?? middlePosition; else if (value == index) - offset = middle; + offset = middlePosition; else if (value < index) - offset = left == null ? middle : lerpDouble(middle, left, index - value); + offset = leadingPosition == null ? middlePosition : lerpDouble(middlePosition, leadingPosition, index - value); else - offset = right == null ? middle : lerpDouble(middle, right, value - index); + offset = trailingPosition == null ? middlePosition : lerpDouble(middlePosition, trailingPosition, value - index); _scrollController.jumpTo(offset); } @@ -663,6 +712,11 @@ class _TabBarState extends State { } void _handleTabControllerTick() { + if (_controller.index != _currentIndex) { + _currentIndex = _controller.index; + if (widget.isScrollable) + _scrollToCurrentIndex(); + } setState(() { // Rebuild the tabs after a (potentially animated) index change // has completed. @@ -670,8 +724,9 @@ class _TabBarState extends State { } // Called each time layout completes. - void _saveTabOffsets(List tabOffsets) { - _indicatorPainter?._tabOffsets = tabOffsets; + void _saveTabOffsets(List tabOffsets, TextDirection textDirection, double width) { + _tabStripWidth = width; + _indicatorPainter?.saveTabOffsets(tabOffsets, textDirection); } void _handleTap(int index) { @@ -717,12 +772,6 @@ class _TabBarState extends State { _indicatorPainter._color = Colors.white; } - if (_controller.index != _currentIndex) { - _currentIndex = _controller.index; - if (widget.isScrollable) - _scrollToCurrentIndex(); - } - final int previousIndex = _controller.previousIndex; if (_controller.indexIsChanging) { @@ -738,13 +787,13 @@ class _TabBarState extends State { wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation); if (_currentIndex > 0) { final int tabIndex = _currentIndex - 1; - final Animation leftAnimation = new _DragAnimation(_controller, tabIndex); - wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, leftAnimation); + final Animation previousAnimation = new _DragAnimation(_controller, tabIndex); + wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, previousAnimation); } if (_currentIndex < widget.tabs.length - 1) { final int tabIndex = _currentIndex + 1; - final Animation rightAnimation = new _DragAnimation(_controller, tabIndex); - wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, rightAnimation); + final Animation nextAnimation = new _DragAnimation(_controller, tabIndex); + wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, nextAnimation); } } } @@ -753,7 +802,7 @@ class _TabBarState extends State { // then give all of the tabs equal flexibility so that their widths // reflect the intrinsic width of their labels. final int tabCount = widget.tabs.length; - for (int index = 0; index < tabCount; index++) { + for (int index = 0; index < tabCount; index += 1) { wrappedTabs[index] = new InkWell( onTap: () { _handleTap(index); }, child: new Padding( @@ -777,17 +826,17 @@ class _TabBarState extends State { Widget tabBar = new CustomPaint( painter: _indicatorPainter, child: new _TabStyle( - animation: kAlwaysDismissedAnimation, - selected: false, - labelColor: widget.labelColor, - unselectedLabelColor: widget.unselectedLabelColor, - labelStyle: widget.labelStyle, - unselectedLabelStyle: widget.unselectedLabelStyle, - child: new _TabLabelBar( - onPerformLayout: _saveTabOffsets, - children: wrappedTabs, - ), + animation: kAlwaysDismissedAnimation, + selected: false, + labelColor: widget.labelColor, + unselectedLabelColor: widget.unselectedLabelColor, + labelStyle: widget.labelStyle, + unselectedLabelStyle: widget.unselectedLabelStyle, + child: new _TabLabelBar( + onPerformLayout: _saveTabOffsets, + children: wrappedTabs, ), + ), ); if (widget.isScrollable) { @@ -1085,8 +1134,8 @@ class TabPageSelector extends StatelessWidget { else background = selectedColorTween.begin; } else { - // The selection's offset reflects how far the TabBarView has - /// been dragged to the left (-1.0 to 0.0) or the right (0.0 to 1.0). + // The selection's offset reflects how far the TabBarView has / been dragged + // to the previous page (-1.0 to 0.0) or the next page (0.0 to 1.0). final double offset = tabController.offset; if (tabController.index == tabIndex) { background = selectedColorTween.lerp(1.0 - offset.abs()); diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index aba03653e1..221b87f561 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -440,8 +440,10 @@ class ScrollableState extends State with TickerProviderStateMixin } void _handleDragStart(DragStartDetails details) { + // It's possible for _hold to become null between _handleDragDown and + // _handleDragStart, for example if some user code calls jumpTo or otherwise + // triggers a new activity to begin. assert(_drag == null); - assert(_hold != null); _drag = position.drag(details, _disposeDrag); assert(_drag != null); assert(_hold == null); diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index c98dfa6d09..cd8a3f1a3e 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -14,9 +14,9 @@ import '../rendering/mock_canvas.dart'; import '../rendering/recording_canvas.dart'; import '../widgets/semantics_tester.dart'; -Widget boilerplate({ Widget child }) { +Widget boilerplate({ Widget child, TextDirection textDirection: TextDirection.ltr }) { return new Directionality( - textDirection: TextDirection.ltr, + textDirection: textDirection, child: new Material( child: child, ), @@ -918,7 +918,7 @@ void main() { expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB #19')).dx, 800.0); }); - testWidgets('TabBar with indicatorWeight, indicatorPadding', (WidgetTester tester) async { + testWidgets('TabBar with indicatorWeight, indicatorPadding (LTR)', (WidgetTester tester) async { const Color color = const Color(0xFF00FF00); const double height = 100.0; const double weight = 8.0; @@ -982,6 +982,196 @@ void main() { )); }); + testWidgets('TabBar with indicatorWeight, indicatorPadding (RTL)', (WidgetTester tester) async { + const Color color = const Color(0xFF00FF00); + const double height = 100.0; + const double weight = 8.0; + const double padLeft = 8.0; + const double padRight = 4.0; + + final List tabs = new List.generate(4, (int index) { + return new Container( + key: new ValueKey(index), + height: height, + ); + }); + + final TabController controller = new TabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + await tester.pumpWidget( + boilerplate( + textDirection: TextDirection.rtl, + child: new Column( + children: [ + new TabBar( + indicatorWeight: 8.0, + indicatorColor: color, + indicatorPadding: const EdgeInsets.only(left: padLeft, right: padRight), + controller: controller, + tabs: tabs, + ), + new Flexible(child: new Container()), + ], + ), + ), + ); + + final RenderBox tabBarBox = tester.firstRenderObject(find.byType(TabBar)); + + // Selected tab dimensions + double tabWidth = tester.getSize(find.byKey(const ValueKey(0))).width; + double tabLeft = tester.getTopLeft(find.byKey(const ValueKey(0))).dx; + double tabRight = tabLeft + tabWidth; + + expect(tabBarBox, paints..rect( + style: PaintingStyle.fill, + color: color, + rect: new Rect.fromLTRB(tabLeft + padLeft, height, tabRight - padRight, height + weight) + )); + + // Select tab 3 + controller.index = 3; + await tester.pumpAndSettle(); + + tabWidth = tester.getSize(find.byKey(const ValueKey(3))).width; + tabLeft = tester.getTopLeft(find.byKey(const ValueKey(3))).dx; + tabRight = tabLeft + tabWidth; + + expect(tabBarBox, paints..rect( + style: PaintingStyle.fill, + color: color, + rect: new Rect.fromLTRB(tabLeft + padLeft, height, tabRight - padRight, height + weight) + )); + }); + + testWidgets('TabBar with directional indicatorPadding (LTR)', (WidgetTester tester) async { + final List tabs = [ + new SizedBox(key: new UniqueKey(), width: 130.0, height: 30.0), + new SizedBox(key: new UniqueKey(), width: 140.0, height: 40.0), + new SizedBox(key: new UniqueKey(), width: 150.0, height: 50.0), + ]; + + final TabController controller = new TabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + await tester.pumpWidget( + boilerplate( + child: new Center( + child: new SizedBox( + width: 800.0, + child: new TabBar( + indicatorPadding: const EdgeInsetsDirectional.only(start: 100.0), + isScrollable: true, + controller: controller, + tabs: tabs, + ), + ), + ), + ), + ); + + expect(tester.getRect(find.byKey(tabs[0].key)), new Rect.fromLTRB(0.0, 284.0, 130.0, 314.0)); + expect(tester.getRect(find.byKey(tabs[1].key)), new Rect.fromLTRB(130.0, 279.0, 270.0, 319.0)); + expect(tester.getRect(find.byKey(tabs[2].key)), new Rect.fromLTRB(270.0, 274.0, 420.0, 324.0)); + + expect(tester.firstRenderObject(find.byType(TabBar)), paints..rect( + style: PaintingStyle.fill, + rect: new Rect.fromLTRB(100.0, 50.0, 130.0, 52.0), + )); + }); + + testWidgets('TabBar with directional indicatorPadding (RTL)', (WidgetTester tester) async { + final List tabs = [ + new SizedBox(key: new UniqueKey(), width: 130.0, height: 30.0), + new SizedBox(key: new UniqueKey(), width: 140.0, height: 40.0), + new SizedBox(key: new UniqueKey(), width: 150.0, height: 50.0), + ]; + + final TabController controller = new TabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + await tester.pumpWidget( + boilerplate( + textDirection: TextDirection.rtl, + child: new Center( + child: new SizedBox( + width: 800.0, + child: new TabBar( + indicatorPadding: const EdgeInsetsDirectional.only(start: 100.0), + isScrollable: true, + controller: controller, + tabs: tabs, + ), + ), + ), + ), + ); + + expect(tester.getRect(find.byKey(tabs[0].key)), new Rect.fromLTRB(670.0, 284.0, 800.0, 314.0)); + expect(tester.getRect(find.byKey(tabs[1].key)), new Rect.fromLTRB(530.0, 279.0, 670.0, 319.0)); + expect(tester.getRect(find.byKey(tabs[2].key)), new Rect.fromLTRB(380.0, 274.0, 530.0, 324.0)); + + final RenderBox tabBar = tester.renderObject(find.byType(CustomPaint).at(1)); + + expect(tabBar.size, const Size(420.0, 52.0)); + expect(tabBar, paints..rect( + style: PaintingStyle.fill, + rect: new Rect.fromLTRB(tabBar.size.width - 130.0, 50.0, tabBar.size.width - 100.0, 52.0), + )); + }); + + testWidgets('Overflowing RTL tab bar', (WidgetTester tester) async { + final List tabs = new List.filled(100, + new SizedBox(key: new UniqueKey(), width: 30.0, height: 20.0), + ); + + final TabController controller = new TabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + await tester.pumpWidget( + boilerplate( + textDirection: TextDirection.rtl, + child: new Center( + child: new TabBar( + isScrollable: true, + controller: controller, + tabs: tabs, + ), + ), + ), + ); + + expect(tester.firstRenderObject(find.byType(TabBar)), paints..rect( + style: PaintingStyle.fill, + rect: new Rect.fromLTRB(2970.0, 20.0, 3000.0, 22.0), + )); + + controller.animateTo(tabs.length - 1, duration: const Duration(seconds: 1), curve: Curves.linear); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); + + expect(tester.firstRenderObject(find.byType(TabBar)), paints..rect( + style: PaintingStyle.fill, + rect: new Rect.fromLTRB(742.5, 20.0, 772.5, 22.0), // (these values were derived empirically, not analytically) + )); + + await tester.pump(const Duration(milliseconds: 501)); + + expect(tester.firstRenderObject(find.byType(TabBar)), paints..rect( + style: PaintingStyle.fill, + rect: new Rect.fromLTRB(0.0, 20.0, 30.0, 22.0), + )); + }); + testWidgets('correct semantics', (WidgetTester tester) async { final SemanticsTester semantics = new SemanticsTester(tester);