Unverified Commit 2bda59a1 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

TabBar RTL (#13164)

parent 112df6ef
...@@ -11,18 +11,26 @@ enum TabsDemoStyle { ...@@ -11,18 +11,26 @@ enum TabsDemoStyle {
} }
class _Page { class _Page {
_Page({ this.icon, this.text }); const _Page({ this.icon, this.text });
final IconData icon; final IconData icon;
final String text; final String text;
} }
final List<_Page> _allPages = <_Page>[ const List<_Page> _allPages = const <_Page>[
new _Page(icon: Icons.event, text: 'EVENT'), const _Page(icon: Icons.grade, text: 'TRIUMPH'),
new _Page(icon: Icons.home, text: 'HOME'), const _Page(icon: Icons.playlist_add, text: 'NOTE'),
new _Page(icon: Icons.android, text: 'ANDROID'), const _Page(icon: Icons.check_circle, text: 'SUCCESS'),
new _Page(icon: Icons.alarm, text: 'ALARM'), const _Page(icon: Icons.question_answer, text: 'OVERSTATE'),
new _Page(icon: Icons.face, text: 'FACE'), const _Page(icon: Icons.sentiment_very_satisfied, text: 'SATISFACTION'),
new _Page(icon: Icons.language, text: 'LANGUAGE'), 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 { class ScrollableTabsDemo extends StatefulWidget {
......
...@@ -155,18 +155,20 @@ class _TabStyle extends AnimatedWidget { ...@@ -155,18 +155,20 @@ class _TabStyle extends AnimatedWidget {
} }
} }
typedef void _LayoutCallback(List<double> xOffsets, TextDirection textDirection, double width);
class _TabLabelBarRenderer extends RenderFlex { class _TabLabelBarRenderer extends RenderFlex {
_TabLabelBarRenderer({ _TabLabelBarRenderer({
List<RenderBox> children, List<RenderBox> children,
Axis direction, @required Axis direction,
MainAxisSize mainAxisSize, @required MainAxisSize mainAxisSize,
MainAxisAlignment mainAxisAlignment, @required MainAxisAlignment mainAxisAlignment,
CrossAxisAlignment crossAxisAlignment, @required CrossAxisAlignment crossAxisAlignment,
TextDirection textDirection, @required TextDirection textDirection,
VerticalDirection verticalDirection, @required VerticalDirection verticalDirection,
TextBaseline textBaseline,
@required this.onPerformLayout, @required this.onPerformLayout,
}) : assert(onPerformLayout != null), }) : assert(onPerformLayout != null),
assert(textDirection != null),
super( super(
children: children, children: children,
direction: direction, direction: direction,
...@@ -175,14 +177,17 @@ class _TabLabelBarRenderer extends RenderFlex { ...@@ -175,14 +177,17 @@ class _TabLabelBarRenderer extends RenderFlex {
crossAxisAlignment: crossAxisAlignment, crossAxisAlignment: crossAxisAlignment,
textDirection: textDirection, textDirection: textDirection,
verticalDirection: verticalDirection, verticalDirection: verticalDirection,
textBaseline: textBaseline,
); );
ValueChanged<List<double>> onPerformLayout; _LayoutCallback onPerformLayout;
@override @override
void performLayout() { void performLayout() {
super.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; RenderBox child = firstChild;
final List<double> xOffsets = <double>[]; final List<double> xOffsets = <double>[];
while (child != null) { while (child != null) {
...@@ -191,8 +196,16 @@ class _TabLabelBarRenderer extends RenderFlex { ...@@ -191,8 +196,16 @@ class _TabLabelBarRenderer extends RenderFlex {
assert(child.parentData == childParentData); assert(child.parentData == childParentData);
child = childParentData.nextSibling; child = childParentData.nextSibling;
} }
xOffsets.add(size.width); // So xOffsets[lastTabIndex + 1] is valid. assert(textDirection != null);
onPerformLayout(xOffsets); 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 { ...@@ -202,10 +215,6 @@ class _TabLabelBarRenderer extends RenderFlex {
class _TabLabelBar extends Flex { class _TabLabelBar extends Flex {
_TabLabelBar({ _TabLabelBar({
Key key, Key key,
MainAxisAlignment mainAxisAlignment,
CrossAxisAlignment crossAxisAlignment,
TextDirection textDirection,
VerticalDirection verticalDirection: VerticalDirection.down,
List<Widget> children: const <Widget>[], List<Widget> children: const <Widget>[],
this.onPerformLayout, this.onPerformLayout,
}) : super( }) : super(
...@@ -215,11 +224,10 @@ class _TabLabelBar extends Flex { ...@@ -215,11 +224,10 @@ class _TabLabelBar extends Flex {
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
textDirection: textDirection, verticalDirection: VerticalDirection.down,
verticalDirection: verticalDirection,
); );
final ValueChanged<List<double>> onPerformLayout; final _LayoutCallback onPerformLayout;
@override @override
RenderFlex createRenderObject(BuildContext context) { RenderFlex createRenderObject(BuildContext context) {
...@@ -230,7 +238,6 @@ class _TabLabelBar extends Flex { ...@@ -230,7 +238,6 @@ class _TabLabelBar extends Flex {
crossAxisAlignment: crossAxisAlignment, crossAxisAlignment: crossAxisAlignment,
textDirection: getEffectiveTextDirection(context), textDirection: getEffectiveTextDirection(context),
verticalDirection: verticalDirection, verticalDirection: verticalDirection,
textBaseline: textBaseline,
onPerformLayout: onPerformLayout, onPerformLayout: onPerformLayout,
); );
} }
...@@ -258,29 +265,66 @@ double _indexChangeProgress(TabController controller) { ...@@ -258,29 +265,66 @@ double _indexChangeProgress(TabController controller) {
class _IndicatorPainter extends CustomPainter { class _IndicatorPainter extends CustomPainter {
_IndicatorPainter({ _IndicatorPainter({
this.controller, @required this.controller,
this.indicatorWeight, @required this.indicatorWeight,
this.indicatorPadding, @required this.indicatorPadding,
List<double> initialTabOffsets, _IndicatorPainter old,
}) : _tabOffsets = initialTabOffsets, super(repaint: controller.animation); }) : 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 TabController controller;
final double indicatorWeight; final double indicatorWeight;
final EdgeInsets indicatorPadding; final EdgeInsetsGeometry indicatorPadding;
List<double> _tabOffsets;
List<double> _currentTabOffsets;
TextDirection _currentTextDirection;
EdgeInsets _resolvedIndicatorPadding;
Color _color; Color _color;
Rect _currentRect; Rect _currentRect;
// _tabOffsets[index] is the offset of the left edge of the tab at index, and void saveTabOffsets(List<double> tabOffsets, TextDirection textDirection) {
// _tabOffsets[_tabOffsets.length] is the right edge of the last tab. _currentTabOffsets = tabOffsets;
int get maxTabIndex => _tabOffsets.length - 2; _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) { Rect indicatorRect(Size tabBarSize, int tabIndex) {
assert(_tabOffsets != null && tabIndex >= 0 && tabIndex <= maxTabIndex); assert(_currentTabOffsets != null);
double tabLeft = _tabOffsets[tabIndex]; assert(_currentTextDirection != null);
double tabRight = _tabOffsets[tabIndex + 1]; assert(_currentTabOffsets.isNotEmpty);
tabLeft = math.min(tabLeft + indicatorPadding.left, tabRight); assert(tabIndex >= 0);
tabRight = math.max(tabRight - indicatorPadding.right, tabLeft); 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; final double tabTop = tabBarSize.height - indicatorWeight;
return new Rect.fromLTWH(tabLeft, tabTop, tabRight - tabLeft, indicatorWeight); return new Rect.fromLTWH(tabLeft, tabTop, tabRight - tabLeft, indicatorWeight);
} }
...@@ -288,46 +332,49 @@ class _IndicatorPainter extends CustomPainter { ...@@ -288,46 +332,49 @@ class _IndicatorPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
if (controller.indexIsChanging) { if (controller.indexIsChanging) {
// The user tapped on a tab, the tab controller's animation is running.
final Rect targetRect = indicatorRect(size, controller.index); final Rect targetRect = indicatorRect(size, controller.index);
_currentRect = Rect.lerp(targetRect, _currentRect ?? targetRect, _indexChangeProgress(controller)); _currentRect = Rect.lerp(targetRect, _currentRect ?? targetRect, _indexChangeProgress(controller));
} else { } else {
// The user is dragging the TabBarView's PageView left or right.
final int currentIndex = controller.index; 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 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 index = controller.index.toDouble();
final double value = controller.animation.value; final double value = controller.animation.value;
if (value == index - 1.0) if (value == index - 1.0)
_currentRect = left ?? middle; _currentRect = previous ?? middle;
else if (value == index + 1.0) else if (value == index + 1.0)
_currentRect = right ?? middle; _currentRect = next ?? middle;
else if (value == index) else if (value == index)
_currentRect = middle; _currentRect = middle;
else if (value < index) 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 else
_currentRect = right == null ? middle : Rect.lerp(middle, right, value - index); _currentRect = next == null ? middle : Rect.lerp(middle, next, value - index);
} }
assert(_currentRect != null); assert(_currentRect != null);
canvas.drawRect(_currentRect, new Paint()..color = _color); canvas.drawRect(_currentRect, new Paint()..color = _color);
} }
static bool _tabOffsetsNotEqual(List<double> a, List<double> b) { static bool _tabOffsetsEqual(List<double> a, List<double> b) {
assert(a != null && b != null && a.length == b.length); if (a?.length != b?.length)
return false;
for (int i = 0; i < a.length; i += 1) { for (int i = 0; i < a.length; i += 1) {
if (a[i] != b[i]) if (a[i] != b[i])
return true; return false;
} }
return false; return true;
} }
@override @override
bool shouldRepaint(_IndicatorPainter old) { bool shouldRepaint(_IndicatorPainter old) {
return controller != old.controller || return controller != old.controller
_tabOffsets?.length != old._tabOffsets?.length || || indicatorWeight != old.indicatorWeight
_tabOffsetsNotEqual(_tabOffsets, old._tabOffsets) || || indicatorPadding != old.indicatorPadding
_currentRect != old._currentRect; || (!_tabOffsetsEqual(_currentTabOffsets, old._currentTabOffsets))
|| _currentTextDirection != old._currentTextDirection;
} }
} }
...@@ -488,7 +535,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -488,7 +535,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// [indicatorPadding] are ignored. /// [indicatorPadding] are ignored.
/// ///
/// The default value of [indicatorPadding] is [EdgeInsets.zero]. /// The default value of [indicatorPadding] is [EdgeInsets.zero].
final EdgeInsets indicatorPadding; final EdgeInsetsGeometry indicatorPadding;
/// The color of selected tab labels. /// The color of selected tab labels.
/// ///
...@@ -540,10 +587,10 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -540,10 +587,10 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
class _TabBarState extends State<TabBar> { class _TabBarState extends State<TabBar> {
ScrollController _scrollController; ScrollController _scrollController;
TabController _controller; TabController _controller;
_IndicatorPainter _indicatorPainter; _IndicatorPainter _indicatorPainter;
int _currentIndex; int _currentIndex;
double _tabStripWidth;
void _updateTabController() { void _updateTabController() {
final TabController newController = widget.controller ?? DefaultTabController.of(context); final TabController newController = widget.controller ?? DefaultTabController.of(context);
...@@ -571,12 +618,11 @@ class _TabBarState extends State<TabBar> { ...@@ -571,12 +618,11 @@ class _TabBarState extends State<TabBar> {
_controller.animation.addListener(_handleTabControllerAnimationTick); _controller.animation.addListener(_handleTabControllerAnimationTick);
_controller.addListener(_handleTabControllerTick); _controller.addListener(_handleTabControllerTick);
_currentIndex = _controller.index; _currentIndex = _controller.index;
final List<double> offsets = _indicatorPainter?._tabOffsets;
_indicatorPainter = new _IndicatorPainter( _indicatorPainter = new _IndicatorPainter(
controller: _controller, controller: _controller,
indicatorWeight: widget.indicatorWeight, indicatorWeight: widget.indicatorWeight,
indicatorPadding: widget.indicatorPadding, indicatorPadding: widget.indicatorPadding,
initialTabOffsets: offsets, old: _indicatorPainter,
); );
} }
} }
...@@ -604,16 +650,19 @@ class _TabBarState extends State<TabBar> { ...@@ -604,16 +650,19 @@ class _TabBarState extends State<TabBar> {
super.dispose(); super.dispose();
} }
// _tabOffsets[index] is the offset of the left edge of the tab at index, and int get maxTabIndex => _indicatorPainter.maxTabIndex;
// _tabOffsets[_tabOffsets.length] is the right edge of the last tab.
int get maxTabIndex => _indicatorPainter._tabOffsets.length - 2;
double _tabScrollOffset(int index, double viewportWidth, double minExtent, double maxExtent) { double _tabScrollOffset(int index, double viewportWidth, double minExtent, double maxExtent) {
if (!widget.isScrollable) if (!widget.isScrollable)
return 0.0; return 0.0;
final List<double> tabOffsets = _indicatorPainter._tabOffsets; double tabCenter = _indicatorPainter.centerOf(index);
assert(tabOffsets != null && index >= 0 && index <= maxTabIndex); switch (Directionality.of(context)) {
final double tabCenter = (tabOffsets[index] + tabOffsets[index + 1]) / 2.0; case TextDirection.rtl:
tabCenter = _tabStripWidth - tabCenter;
break;
case TextDirection.ltr:
break;
}
return (tabCenter - viewportWidth / 2.0).clamp(minExtent, maxExtent); return (tabCenter - viewportWidth / 2.0).clamp(minExtent, maxExtent);
} }
...@@ -632,23 +681,23 @@ class _TabBarState extends State<TabBar> { ...@@ -632,23 +681,23 @@ class _TabBarState extends State<TabBar> {
} }
void _scrollToControllerValue() { void _scrollToControllerValue() {
final double left = _currentIndex > 0 ? _tabCenteredScrollOffset(_currentIndex - 1) : null; final double leadingPosition = _currentIndex > 0 ? _tabCenteredScrollOffset(_currentIndex - 1) : null;
final double middle = _tabCenteredScrollOffset(_currentIndex); final double middlePosition = _tabCenteredScrollOffset(_currentIndex);
final double right = _currentIndex < maxTabIndex ? _tabCenteredScrollOffset(_currentIndex + 1) : null; final double trailingPosition = _currentIndex < maxTabIndex ? _tabCenteredScrollOffset(_currentIndex + 1) : null;
final double index = _controller.index.toDouble(); final double index = _controller.index.toDouble();
final double value = _controller.animation.value; final double value = _controller.animation.value;
double offset; double offset;
if (value == index - 1.0) if (value == index - 1.0)
offset = left ?? middle; offset = leadingPosition ?? middlePosition;
else if (value == index + 1.0) else if (value == index + 1.0)
offset = right ?? middle; offset = trailingPosition ?? middlePosition;
else if (value == index) else if (value == index)
offset = middle; offset = middlePosition;
else if (value < index) else if (value < index)
offset = left == null ? middle : lerpDouble(middle, left, index - value); offset = leadingPosition == null ? middlePosition : lerpDouble(middlePosition, leadingPosition, index - value);
else else
offset = right == null ? middle : lerpDouble(middle, right, value - index); offset = trailingPosition == null ? middlePosition : lerpDouble(middlePosition, trailingPosition, value - index);
_scrollController.jumpTo(offset); _scrollController.jumpTo(offset);
} }
...@@ -663,6 +712,11 @@ class _TabBarState extends State<TabBar> { ...@@ -663,6 +712,11 @@ class _TabBarState extends State<TabBar> {
} }
void _handleTabControllerTick() { void _handleTabControllerTick() {
if (_controller.index != _currentIndex) {
_currentIndex = _controller.index;
if (widget.isScrollable)
_scrollToCurrentIndex();
}
setState(() { setState(() {
// Rebuild the tabs after a (potentially animated) index change // Rebuild the tabs after a (potentially animated) index change
// has completed. // has completed.
...@@ -670,8 +724,9 @@ class _TabBarState extends State<TabBar> { ...@@ -670,8 +724,9 @@ class _TabBarState extends State<TabBar> {
} }
// Called each time layout completes. // Called each time layout completes.
void _saveTabOffsets(List<double> tabOffsets) { void _saveTabOffsets(List<double> tabOffsets, TextDirection textDirection, double width) {
_indicatorPainter?._tabOffsets = tabOffsets; _tabStripWidth = width;
_indicatorPainter?.saveTabOffsets(tabOffsets, textDirection);
} }
void _handleTap(int index) { void _handleTap(int index) {
...@@ -717,12 +772,6 @@ class _TabBarState extends State<TabBar> { ...@@ -717,12 +772,6 @@ class _TabBarState extends State<TabBar> {
_indicatorPainter._color = Colors.white; _indicatorPainter._color = Colors.white;
} }
if (_controller.index != _currentIndex) {
_currentIndex = _controller.index;
if (widget.isScrollable)
_scrollToCurrentIndex();
}
final int previousIndex = _controller.previousIndex; final int previousIndex = _controller.previousIndex;
if (_controller.indexIsChanging) { if (_controller.indexIsChanging) {
...@@ -738,13 +787,13 @@ class _TabBarState extends State<TabBar> { ...@@ -738,13 +787,13 @@ class _TabBarState extends State<TabBar> {
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation); wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation);
if (_currentIndex > 0) { if (_currentIndex > 0) {
final int tabIndex = _currentIndex - 1; final int tabIndex = _currentIndex - 1;
final Animation<double> leftAnimation = new _DragAnimation(_controller, tabIndex); final Animation<double> previousAnimation = new _DragAnimation(_controller, tabIndex);
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, leftAnimation); wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, previousAnimation);
} }
if (_currentIndex < widget.tabs.length - 1) { if (_currentIndex < widget.tabs.length - 1) {
final int tabIndex = _currentIndex + 1; final int tabIndex = _currentIndex + 1;
final Animation<double> rightAnimation = new _DragAnimation(_controller, tabIndex); final Animation<double> nextAnimation = new _DragAnimation(_controller, tabIndex);
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, rightAnimation); wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, nextAnimation);
} }
} }
} }
...@@ -753,7 +802,7 @@ class _TabBarState extends State<TabBar> { ...@@ -753,7 +802,7 @@ class _TabBarState extends State<TabBar> {
// then give all of the tabs equal flexibility so that their widths // then give all of the tabs equal flexibility so that their widths
// reflect the intrinsic width of their labels. // reflect the intrinsic width of their labels.
final int tabCount = widget.tabs.length; 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( wrappedTabs[index] = new InkWell(
onTap: () { _handleTap(index); }, onTap: () { _handleTap(index); },
child: new Padding( child: new Padding(
...@@ -777,17 +826,17 @@ class _TabBarState extends State<TabBar> { ...@@ -777,17 +826,17 @@ class _TabBarState extends State<TabBar> {
Widget tabBar = new CustomPaint( Widget tabBar = new CustomPaint(
painter: _indicatorPainter, painter: _indicatorPainter,
child: new _TabStyle( child: new _TabStyle(
animation: kAlwaysDismissedAnimation, animation: kAlwaysDismissedAnimation,
selected: false, selected: false,
labelColor: widget.labelColor, labelColor: widget.labelColor,
unselectedLabelColor: widget.unselectedLabelColor, unselectedLabelColor: widget.unselectedLabelColor,
labelStyle: widget.labelStyle, labelStyle: widget.labelStyle,
unselectedLabelStyle: widget.unselectedLabelStyle, unselectedLabelStyle: widget.unselectedLabelStyle,
child: new _TabLabelBar( child: new _TabLabelBar(
onPerformLayout: _saveTabOffsets, onPerformLayout: _saveTabOffsets,
children: wrappedTabs, children: wrappedTabs,
),
), ),
),
); );
if (widget.isScrollable) { if (widget.isScrollable) {
...@@ -1085,8 +1134,8 @@ class TabPageSelector extends StatelessWidget { ...@@ -1085,8 +1134,8 @@ class TabPageSelector extends StatelessWidget {
else else
background = selectedColorTween.begin; background = selectedColorTween.begin;
} else { } else {
// The selection's offset reflects how far the TabBarView has // The selection's offset reflects how far the TabBarView has / been dragged
/// been dragged to the left (-1.0 to 0.0) or the right (0.0 to 1.0). // to the previous page (-1.0 to 0.0) or the next page (0.0 to 1.0).
final double offset = tabController.offset; final double offset = tabController.offset;
if (tabController.index == tabIndex) { if (tabController.index == tabIndex) {
background = selectedColorTween.lerp(1.0 - offset.abs()); background = selectedColorTween.lerp(1.0 - offset.abs());
......
...@@ -440,8 +440,10 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin ...@@ -440,8 +440,10 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
} }
void _handleDragStart(DragStartDetails details) { 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(_drag == null);
assert(_hold != null);
_drag = position.drag(details, _disposeDrag); _drag = position.drag(details, _disposeDrag);
assert(_drag != null); assert(_drag != null);
assert(_hold == null); assert(_hold == null);
......
...@@ -14,9 +14,9 @@ import '../rendering/mock_canvas.dart'; ...@@ -14,9 +14,9 @@ import '../rendering/mock_canvas.dart';
import '../rendering/recording_canvas.dart'; import '../rendering/recording_canvas.dart';
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
Widget boilerplate({ Widget child }) { Widget boilerplate({ Widget child, TextDirection textDirection: TextDirection.ltr }) {
return new Directionality( return new Directionality(
textDirection: TextDirection.ltr, textDirection: textDirection,
child: new Material( child: new Material(
child: child, child: child,
), ),
...@@ -918,7 +918,7 @@ void main() { ...@@ -918,7 +918,7 @@ void main() {
expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB #19')).dx, 800.0); 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 Color color = const Color(0xFF00FF00);
const double height = 100.0; const double height = 100.0;
const double weight = 8.0; const double weight = 8.0;
...@@ -982,6 +982,196 @@ void main() { ...@@ -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<Widget> tabs = new List<Widget>.generate(4, (int index) {
return new Container(
key: new ValueKey<int>(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: <Widget>[
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<RenderBox>(find.byType(TabBar));
// Selected tab dimensions
double tabWidth = tester.getSize(find.byKey(const ValueKey<int>(0))).width;
double tabLeft = tester.getTopLeft(find.byKey(const ValueKey<int>(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<int>(3))).width;
tabLeft = tester.getTopLeft(find.byKey(const ValueKey<int>(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<Widget> tabs = <Widget>[
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<RenderBox>(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<Widget> tabs = <Widget>[
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<RenderBox>(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<Widget> tabs = new List<Widget>.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<RenderBox>(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<RenderBox>(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<RenderBox>(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 { testWidgets('correct semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
......
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