Unverified Commit 24efb55b authored by Hans Muller's avatar Hans Muller Committed by GitHub

Generalized TabBar selected tab indicator (#14741)

parent 21c514fc
...@@ -81,6 +81,7 @@ export 'src/material/stepper.dart'; ...@@ -81,6 +81,7 @@ export 'src/material/stepper.dart';
export 'src/material/switch.dart'; export 'src/material/switch.dart';
export 'src/material/switch_list_tile.dart'; export 'src/material/switch_list_tile.dart';
export 'src/material/tab_controller.dart'; export 'src/material/tab_controller.dart';
export 'src/material/tab_indicator.dart';
export 'src/material/tabs.dart'; export 'src/material/tabs.dart';
export 'src/material/text_field.dart'; export 'src/material/text_field.dart';
export 'src/material/text_form_field.dart'; export 'src/material/text_form_field.dart';
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/widgets.dart';
import 'colors.dart';
/// Used with [TabBar.indicator] to draw a horizontal line below the
/// selected tab.
///
/// The selected tab underline is inset from the tab's boundary by [insets].
/// The [borderSide] defines the line's color and weight.
///
/// The [TabBar.indicatorSize] property can be used to define the indicator's
/// bounds in terms of its (centered) widget with [TabIndicatorSize.label],
/// or the entire tab with [TabIndicatorSize.tab].
class UnderlineTabIndicator extends Decoration {
/// Create an underline style selected tab indicator.
///
/// The [borderSide] and [insets] arguments must not be null.
const UnderlineTabIndicator({
this.borderSide: const BorderSide(width: 2.0, color: Colors.white),
this.insets: EdgeInsets.zero,
}) : assert(borderSide != null), assert(insets != null);
/// The color and weight of the horizontal line drawn below the selected tab.
final BorderSide borderSide;
/// Locates the selected tab's underline relative to the tab's boundary.
///
/// The [TabBar.indicatorSize] property can be used to define the
/// tab indicator's bounds in terms of its (centered) tab widget with
/// [TabIndicatorSize.label], or the entire tab with [TabIndicatorSize.tab].
final EdgeInsetsGeometry insets;
@override
Decoration lerpFrom(Decoration a, double t) {
if (a is UnderlineTabIndicator) {
return new UnderlineTabIndicator(
borderSide: BorderSide.lerp(a.borderSide, borderSide, t),
insets: EdgeInsetsGeometry.lerp(a.insets, insets, t),
);
}
return super.lerpFrom(a, t);
}
@override
Decoration lerpTo(Decoration b, double t) {
if (b is UnderlineTabIndicator) {
return new UnderlineTabIndicator(
borderSide: BorderSide.lerp(borderSide, b.borderSide, t),
insets: EdgeInsetsGeometry.lerp(insets, b.insets, t),
);
}
return super.lerpTo(b, t);
}
@override
_UnderlinePainter createBoxPainter([VoidCallback onChanged]) {
return new _UnderlinePainter(this, onChanged);
}
}
class _UnderlinePainter extends BoxPainter {
_UnderlinePainter(this.decoration, VoidCallback onChanged)
: assert(decoration != null), super(onChanged);
final UnderlineTabIndicator decoration;
BorderSide get borderSide => decoration.borderSide;
EdgeInsetsGeometry get insets => decoration.insets;
Rect _indicatorRectFor(Rect rect, TextDirection textDirection) {
assert(rect != null);
assert(textDirection != null);
final Rect indicator = insets.resolve(textDirection).deflateRect(rect);
return new Rect.fromLTWH(
indicator.left,
indicator.bottom - borderSide.width,
indicator.width,
borderSide.width,
);
}
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
assert(configuration != null);
assert(configuration.size != null);
final Rect rect = offset & configuration.size;
final TextDirection textDirection = configuration.textDirection;
final Rect indicator = _indicatorRectFor(rect, textDirection).deflate(borderSide.width / 2.0);
canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, borderSide.toPaint());
}
}
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'dart:ui' show lerpDouble; import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -18,11 +17,31 @@ import 'ink_well.dart'; ...@@ -18,11 +17,31 @@ import 'ink_well.dart';
import 'material.dart'; import 'material.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'tab_controller.dart'; import 'tab_controller.dart';
import 'tab_indicator.dart';
import 'theme.dart'; import 'theme.dart';
const double _kTabHeight = 46.0; const double _kTabHeight = 46.0;
const double _kTextAndIconTabHeight = 72.0; const double _kTextAndIconTabHeight = 72.0;
const double _kMinTabWidth = 72.0;
/// Defines how the bounds of the selected tab indicator are computed.
///
/// See also:
/// * [TabBar], which displays a row of tabs.
/// * [TabBarView], which displays a widget for the currently selected tab.
/// * [TabBar.indicator], which defines the appearance of the selected tab
/// indicator relative to the tab's bounds.
enum TabBarIndicatorSize {
/// The tab indicator's bounds are as wide as the space occupied by the tab
/// in the tab bar: from the right edge of the previous tab to the left edge
/// of the next tab.
tab,
/// The tab's bounds are only as wide as the (centered) tab widget itself.
///
/// This value is used to align the tab's label, typically a [Tab]
/// widget's text or icon, with the selected tab indicator.
label,
}
/// A material design [TabBar] tab. If both [icon] and [text] are /// A material design [TabBar] tab. If both [icon] and [text] are
/// provided, the text is displayed below the icon. /// provided, the text is displayed below the icon.
...@@ -85,18 +104,19 @@ class Tab extends StatelessWidget { ...@@ -85,18 +104,19 @@ class Tab extends StatelessWidget {
children: <Widget>[ children: <Widget>[
new Container( new Container(
child: icon, child: icon,
margin: const EdgeInsets.only(bottom: 10.0) margin: const EdgeInsets.only(bottom: 10.0),
), ),
_buildLabelText() _buildLabelText()
] ]
); );
} }
return new Container( return new SizedBox(
padding: kTabLabelPadding,
height: height, height: height,
constraints: const BoxConstraints(minWidth: _kMinTabWidth), child: new Center(
child: new Center(child: label), child: label,
widthFactor: 1.0,
),
); );
} }
...@@ -266,32 +286,38 @@ double _indexChangeProgress(TabController controller) { ...@@ -266,32 +286,38 @@ double _indexChangeProgress(TabController controller) {
class _IndicatorPainter extends CustomPainter { class _IndicatorPainter extends CustomPainter {
_IndicatorPainter({ _IndicatorPainter({
@required this.controller, @required this.controller,
@required this.indicatorWeight, @required this.indicator,
@required this.indicatorPadding, @required this.indicatorSize,
@required this.tabKeys,
_IndicatorPainter old, _IndicatorPainter old,
}) : assert(controller != null), }) : assert(controller != null),
assert(indicatorWeight != null), assert(indicator != null),
assert(indicatorPadding != null),
super(repaint: controller.animation) { super(repaint: controller.animation) {
if (old != null) if (old != null)
saveTabOffsets(old._currentTabOffsets, old._currentTextDirection); saveTabOffsets(old._currentTabOffsets, old._currentTextDirection);
} }
final TabController controller; final TabController controller;
final double indicatorWeight; final Decoration indicator;
final EdgeInsetsGeometry indicatorPadding; final TabBarIndicatorSize indicatorSize;
final List<GlobalKey> tabKeys;
List<double> _currentTabOffsets; List<double> _currentTabOffsets;
TextDirection _currentTextDirection; TextDirection _currentTextDirection;
EdgeInsets _resolvedIndicatorPadding;
Color _color;
Rect _currentRect; Rect _currentRect;
BoxPainter _painter;
bool _needsPaint = false;
void markNeedsPaint() {
_needsPaint = true;
}
void dispose() {
_painter?.dispose();
}
void saveTabOffsets(List<double> tabOffsets, TextDirection textDirection) { void saveTabOffsets(List<double> tabOffsets, TextDirection textDirection) {
_currentTabOffsets = tabOffsets; _currentTabOffsets = tabOffsets;
_currentTextDirection = textDirection; _currentTextDirection = textDirection;
_resolvedIndicatorPadding = indicatorPadding.resolve(_currentTextDirection);
} }
// _currentTabOffsets[index] is the offset of the start edge of the tab at index, and // _currentTabOffsets[index] is the offset of the start edge of the tab at index, and
...@@ -323,14 +349,22 @@ class _IndicatorPainter extends CustomPainter { ...@@ -323,14 +349,22 @@ class _IndicatorPainter extends CustomPainter {
tabRight = _currentTabOffsets[tabIndex + 1]; tabRight = _currentTabOffsets[tabIndex + 1];
break; break;
} }
tabLeft = math.min(tabLeft + _resolvedIndicatorPadding.left, tabRight);
tabRight = math.max(tabRight - _resolvedIndicatorPadding.right, tabLeft); if (indicatorSize == TabBarIndicatorSize.label) {
final double tabTop = tabBarSize.height - indicatorWeight; final double tabWidth = tabKeys[tabIndex].currentContext.size.width;
return new Rect.fromLTWH(tabLeft, tabTop, tabRight - tabLeft, indicatorWeight); final double delta = ((tabRight - tabLeft) - tabWidth) / 2.0;
tabLeft += delta;
tabRight -= delta;
}
return new Rect.fromLTWH(tabLeft, 0.0, tabRight - tabLeft, tabBarSize.height);
} }
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
_needsPaint = false;
_painter ??= indicator.createBoxPainter(markNeedsPaint);
if (controller.indexIsChanging) { if (controller.indexIsChanging) {
// The user tapped on a tab, the tab controller's animation is running. // 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);
...@@ -355,7 +389,12 @@ class _IndicatorPainter extends CustomPainter { ...@@ -355,7 +389,12 @@ class _IndicatorPainter extends CustomPainter {
_currentRect = next == null ? middle : Rect.lerp(middle, next, 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);
final ImageConfiguration configuration = new ImageConfiguration(
size: _currentRect.size,
textDirection: _currentTextDirection,
);
_painter.paint(canvas, _currentRect.topLeft, configuration);
} }
static bool _tabOffsetsEqual(List<double> a, List<double> b) { static bool _tabOffsetsEqual(List<double> a, List<double> b) {
...@@ -370,9 +409,10 @@ class _IndicatorPainter extends CustomPainter { ...@@ -370,9 +409,10 @@ class _IndicatorPainter extends CustomPainter {
@override @override
bool shouldRepaint(_IndicatorPainter old) { bool shouldRepaint(_IndicatorPainter old) {
return controller != old.controller return _needsPaint
|| indicatorWeight != old.indicatorWeight || controller != old.controller
|| indicatorPadding != old.indicatorPadding || indicator != old.indicator
|| tabKeys.length != old.tabKeys.length
|| (!_tabOffsetsEqual(_currentTabOffsets, old._currentTabOffsets)) || (!_tabOffsetsEqual(_currentTabOffsets, old._currentTabOffsets))
|| _currentTextDirection != old._currentTextDirection; || _currentTextDirection != old._currentTextDirection;
} }
...@@ -480,6 +520,9 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -480,6 +520,9 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// The [indicatorWeight] parameter defaults to 2, and must not be null. /// The [indicatorWeight] parameter defaults to 2, and must not be null.
/// ///
/// The [indicatorPadding] parameter defaults to [EdgeInsets.zero], and must not be null. /// The [indicatorPadding] parameter defaults to [EdgeInsets.zero], and must not be null.
///
/// If [indicator] is not null, then [indicatorWeight], [indicatorPadding], and
/// [indicatorColor] are ignored.
const TabBar({ const TabBar({
Key key, Key key,
@required this.tabs, @required this.tabs,
...@@ -488,14 +531,16 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -488,14 +531,16 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.indicatorColor, this.indicatorColor,
this.indicatorWeight: 2.0, this.indicatorWeight: 2.0,
this.indicatorPadding: EdgeInsets.zero, this.indicatorPadding: EdgeInsets.zero,
this.indicator,
this.indicatorSize,
this.labelColor, this.labelColor,
this.labelStyle, this.labelStyle,
this.unselectedLabelColor, this.unselectedLabelColor,
this.unselectedLabelStyle, this.unselectedLabelStyle,
}) : assert(tabs != null), }) : assert(tabs != null),
assert(isScrollable != null), assert(isScrollable != null),
assert(indicatorWeight != null && indicatorWeight > 0.0), assert(indicator != null || (indicatorWeight != null && indicatorWeight > 0.0)),
assert(indicatorPadding != null), assert(indicator != null || (indicatorPadding != null)),
super(key: key); super(key: key);
/// Typically a list of two or more [Tab] widgets. /// Typically a list of two or more [Tab] widgets.
...@@ -518,12 +563,16 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -518,12 +563,16 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// The color of the line that appears below the selected tab. If this parameter /// The color of the line that appears below the selected tab. If this parameter
/// is null then the value of the Theme's indicatorColor property is used. /// is null then the value of the Theme's indicatorColor property is used.
///
/// If [indicator] is specified, this property is ignored.
final Color indicatorColor; final Color indicatorColor;
/// The thickness of the line that appears below the selected tab. The value /// The thickness of the line that appears below the selected tab. The value
/// of this parameter must be greater than zero. /// of this parameter must be greater than zero.
/// ///
/// The default value of [indicatorWeight] is 2.0. /// The default value of [indicatorWeight] is 2.0.
///
/// If [indicator] is specified, this property is ignored.
final double indicatorWeight; final double indicatorWeight;
/// The horizontal padding for the line that appears below the selected tab. /// The horizontal padding for the line that appears below the selected tab.
...@@ -535,8 +584,37 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -535,8 +584,37 @@ 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].
///
/// If [indicator] is specified, this property is ignored.
final EdgeInsetsGeometry indicatorPadding; final EdgeInsetsGeometry indicatorPadding;
/// Defines the appearance of the selected tab indicator.
///
/// If [indicator] is specified, the [indicatorColor], [indicatorWeight],
/// and [indicatorPadding] properties are ignored.
///
/// The default, underline-style, selected tab indicator can be defined with
/// [UnderlineTabIndicator].
///
/// The indicator's size is based on the tab's bounds. If [indicatorSize]
/// is [TabBarIndicatorSize.tab] the tab's bounds are as wide as the space
/// occupied by the tab in the tab bar. If [indicatorSize] is
/// [TabBarIndicatorSize.label] then the tab's bounds are only as wide as
/// the tab widget itself.
final Decoration indicator;
/// Defines how the selected tab indicator's size is computed.
///
/// The size of the selected tab indicator is defined relative to the
/// tab's overall bounds if [indicatorSize] is [TabBarIndicatorSize.tab]
/// (the default) or relative to the bounds of the tab's widget if
/// [indicatorSize] is [TabBarIndicatorSize.label].
///
/// The selected tab's location appearance can be refined further with
/// the [indicatorColor], [indicatorWeight], [indicatorPadding], and
/// [indicator] properties.
final TabBarIndicatorSize indicatorSize;
/// The color of selected tab labels. /// The color of selected tab labels.
/// ///
/// Unselected tab labels are rendered with the same color rendered at 70% /// Unselected tab labels are rendered with the same color rendered at 70%
...@@ -591,6 +669,39 @@ class _TabBarState extends State<TabBar> { ...@@ -591,6 +669,39 @@ class _TabBarState extends State<TabBar> {
_IndicatorPainter _indicatorPainter; _IndicatorPainter _indicatorPainter;
int _currentIndex; int _currentIndex;
double _tabStripWidth; double _tabStripWidth;
List<GlobalKey> _tabKeys;
@override
void initState() {
super.initState();
// If indicatorSize is TabIndicatorSize.label, _tabKeys[i] is used to find
// the width of tab widget i. See _IndicatorPainter.indicatorRect().
_tabKeys = widget.tabs.map((Widget tab) => new GlobalKey()).toList();
}
Decoration get _indicator {
if (widget.indicator != null)
return widget.indicator;
Color color = widget.indicatorColor ?? Theme.of(context).indicatorColor;
// ThemeData tries to avoid this by having indicatorColor avoid being the
// primaryColor. However, it's possible that the tab bar is on a
// Material that isn't the primaryColor. In that case, if the indicator
// color ends up matching the material's color, then this overrides it.
// When that happens, automatic transitions of the theme will likely look
// ugly as the indicator color suddenly snaps to white at one end, but it's
// not clear how to avoid that any further.
if (color == Material.of(context).color)
color = Colors.white;
return new UnderlineTabIndicator(
insets: widget.indicatorPadding,
borderSide: new BorderSide(
width: widget.indicatorWeight,
color: color,
),
);
}
void _updateTabController() { void _updateTabController() {
final TabController newController = widget.controller ?? DefaultTabController.of(context); final TabController newController = widget.controller ?? DefaultTabController.of(context);
...@@ -618,19 +729,24 @@ class _TabBarState extends State<TabBar> { ...@@ -618,19 +729,24 @@ 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;
_indicatorPainter = new _IndicatorPainter(
controller: _controller,
indicatorWeight: widget.indicatorWeight,
indicatorPadding: widget.indicatorPadding,
old: _indicatorPainter,
);
} }
} }
void _initIndicatorPainter() {
_indicatorPainter = _controller == null ? null : new _IndicatorPainter(
controller: _controller,
indicator: _indicator,
indicatorSize: widget.indicatorSize,
tabKeys: _tabKeys,
old: _indicatorPainter,
);
}
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_updateTabController(); _updateTabController();
_initIndicatorPainter();
} }
@override @override
...@@ -638,10 +754,24 @@ class _TabBarState extends State<TabBar> { ...@@ -638,10 +754,24 @@ class _TabBarState extends State<TabBar> {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) if (widget.controller != oldWidget.controller)
_updateTabController(); _updateTabController();
if (widget.indicatorColor != oldWidget.indicatorColor ||
widget.indicatorWeight != oldWidget.indicatorWeight ||
widget.indicatorSize != oldWidget.indicatorSize ||
widget.indicator != oldWidget.indicator)
_initIndicatorPainter();
if (widget.tabs.length > oldWidget.tabs.length) {
final int delta = widget.tabs.length - oldWidget.tabs.length;
_tabKeys.addAll(new List<GlobalKey>.generate(delta, (int n) => new GlobalKey()));
} else if (widget.tabs.length < oldWidget.tabs.length) {
_tabKeys.removeRange(widget.tabs.length, oldWidget.tabs.length);
}
} }
@override @override
void dispose() { void dispose() {
_indicatorPainter.dispose();
if (_controller != null) { if (_controller != null) {
_controller.animation.removeListener(_handleTabControllerAnimationTick); _controller.animation.removeListener(_handleTabControllerAnimationTick);
_controller.removeListener(_handleTabControllerTick); _controller.removeListener(_handleTabControllerTick);
...@@ -755,24 +885,25 @@ class _TabBarState extends State<TabBar> { ...@@ -755,24 +885,25 @@ class _TabBarState extends State<TabBar> {
); );
} }
final List<Widget> wrappedTabs = new List<Widget>.from(widget.tabs, growable: false); final List<Widget> wrappedTabs = new List<Widget>(widget.tabs.length);
for (int i = 0; i < widget.tabs.length; i += 1) {
wrappedTabs[i] = new Center(
heightFactor: 1.0,
child: new Padding(
padding: kTabLabelPadding,
child: new KeyedSubtree(
key: _tabKeys[i],
child: widget.tabs[i],
),
),
);
}
// If the controller was provided by DefaultTabController and we're part // If the controller was provided by DefaultTabController and we're part
// of a Hero (typically the AppBar), then we will not be able to find the // of a Hero (typically the AppBar), then we will not be able to find the
// controller during a Hero transition. See https://github.com/flutter/flutter/issues/213. // controller during a Hero transition. See https://github.com/flutter/flutter/issues/213.
if (_controller != null) { if (_controller != null) {
_indicatorPainter._color = widget.indicatorColor ?? Theme.of(context).indicatorColor;
if (_indicatorPainter._color == Material.of(context).color) {
// ThemeData tries to avoid this by having indicatorColor avoid being the
// primaryColor. However, it's possible that the tab bar is on a
// Material that isn't the primaryColor. In that case, if the indicator
// color ends up clashing, then this overrides it. When that happens,
// automatic transitions of the theme will likely look ugly as the
// indicator color suddenly snaps to white at one end, but it's not clear
// how to avoid that any further.
_indicatorPainter._color = Colors.white;
}
final int previousIndex = _controller.previousIndex; final int previousIndex = _controller.previousIndex;
if (_controller.indexIsChanging) { if (_controller.indexIsChanging) {
...@@ -799,9 +930,9 @@ class _TabBarState extends State<TabBar> { ...@@ -799,9 +930,9 @@ class _TabBarState extends State<TabBar> {
} }
} }
// Add the tap handler to each tab. If the tab bar is scrollable // Add the tap handler to each tab. If the tab bar is not scrollable
// then give all of the tabs equal flexibility so that their widths // then give all of the tabs equal flexibility so that they each occupy
// reflect the intrinsic width of their labels. // the same share of the tab bar's overall width.
final int tabCount = widget.tabs.length; final int tabCount = widget.tabs.length;
for (int index = 0; index < tabCount; index += 1) { for (int index = 0; index < tabCount; index += 1) {
wrappedTabs[index] = new InkWell( wrappedTabs[index] = new InkWell(
......
...@@ -188,7 +188,7 @@ abstract class Decoration extends Diagnosticable { ...@@ -188,7 +188,7 @@ abstract class Decoration extends Diagnosticable {
abstract class BoxPainter { abstract class BoxPainter {
/// Abstract const constructor. This constructor enables subclasses to provide /// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions. /// const constructors so that they can be used in const expressions.
BoxPainter([this.onChanged]); const BoxPainter([this.onChanged]);
/// Paints the [Decoration] for which this object was created on the /// Paints the [Decoration] for which this object was created on the
/// given canvas using the given configuration. /// given canvas using the given configuration.
......
...@@ -140,9 +140,11 @@ class TabIndicatorRecordingCanvas extends TestRecordingCanvas { ...@@ -140,9 +140,11 @@ class TabIndicatorRecordingCanvas extends TestRecordingCanvas {
Rect indicatorRect; Rect indicatorRect;
@override @override
void drawRect(Rect rect, Paint paint) { void drawLine(Offset p1, Offset p2, Paint paint) {
// Assuming that the indicatorWeight is 2.0, the default.
const double indicatorWeight = 2.0;
if (paint.color == indicatorColor) if (paint.color == indicatorColor)
indicatorRect = rect; indicatorRect = new Rect.fromPoints(p1, p2).inflate(indicatorWeight / 2.0);
} }
} }
...@@ -926,21 +928,22 @@ void main() { ...@@ -926,21 +928,22 @@ void main() {
// The initialIndex tab should be visible and right justified // The initialIndex tab should be visible and right justified
expect(find.text('TAB #19'), findsOneWidget); expect(find.text('TAB #19'), findsOneWidget);
expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB #19')).dx, 800.0);
// Tabs have a minimum width of 72.0 and 'TAB #19' is wider than
// that. Tabs are padded horizontally with kTabLabelPadding.
final double tabRight = 800.0 - kTabLabelPadding.right;
expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB #19')).dx, tabRight);
}); });
testWidgets('TabBar with indicatorWeight, indicatorPadding (LTR)', (WidgetTester tester) async { testWidgets('TabBar with indicatorWeight, indicatorPadding (LTR)', (WidgetTester tester) async {
const Color color = const Color(0xFF00FF00); const Color indicatorColor = const Color(0xFF00FF00);
const double height = 100.0; const double indicatorWeight = 8.0;
const double weight = 8.0;
const double padLeft = 8.0; const double padLeft = 8.0;
const double padRight = 4.0; const double padRight = 4.0;
final List<Widget> tabs = new List<Widget>.generate(4, (int index) { final List<Widget> tabs = new List<Widget>.generate(4, (int index) {
return new Container( return new Tab(text: 'Tab $index');
key: new ValueKey<int>(index),
height: height,
);
}); });
final TabController controller = new TabController( final TabController controller = new TabController(
...@@ -950,61 +953,56 @@ void main() { ...@@ -950,61 +953,56 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: new Column( child: new Container(
children: <Widget>[ alignment: Alignment.topLeft,
new TabBar( child: new TabBar(
indicatorWeight: 8.0, indicatorWeight: indicatorWeight,
indicatorColor: color, indicatorColor: indicatorColor,
indicatorPadding: const EdgeInsets.only(left: padLeft, right: padRight), indicatorPadding: const EdgeInsets.only(left: padLeft, right: padRight),
controller: controller, controller: controller,
tabs: tabs, tabs: tabs,
), ),
new Flexible(child: new Container()),
],
), ),
), ),
); );
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(tabBarBox.size.height, 54.0); // 54 = _kTabHeight(46) + indicatorWeight(8.0)
// Selected tab dimensions final double indicatorY = 54.0 - indicatorWeight / 2.0;
double tabWidth = tester.getSize(find.byKey(const ValueKey<int>(0))).width; double indicatorLeft = padLeft + indicatorWeight / 2.0;
double tabLeft = tester.getTopLeft(find.byKey(const ValueKey<int>(0))).dx; double indicatorRight = 200.0 - (padRight + indicatorWeight / 2.0);
double tabRight = tabLeft + tabWidth;
expect(tabBarBox, paints..rect( expect(tabBarBox, paints..line(
style: PaintingStyle.fill, color: indicatorColor,
color: color, strokeWidth: indicatorWeight,
rect: new Rect.fromLTRB(tabLeft + padLeft, height, tabRight - padRight, height + weight) p1: new Offset(indicatorLeft, indicatorY),
p2: new Offset(indicatorRight, indicatorY),
)); ));
// Select tab 3 // Select tab 3
controller.index = 3; controller.index = 3;
await tester.pumpAndSettle(); await tester.pumpAndSettle();
tabWidth = tester.getSize(find.byKey(const ValueKey<int>(3))).width; indicatorLeft = 600.0 + padLeft + indicatorWeight / 2.0;
tabLeft = tester.getTopLeft(find.byKey(const ValueKey<int>(3))).dx; indicatorRight = 800.0 - (padRight + indicatorWeight / 2.0);
tabRight = tabLeft + tabWidth;
expect(tabBarBox, paints..rect( expect(tabBarBox, paints..line(
style: PaintingStyle.fill, color: indicatorColor,
color: color, strokeWidth: indicatorWeight,
rect: new Rect.fromLTRB(tabLeft + padLeft, height, tabRight - padRight, height + weight) p1: new Offset(indicatorLeft, indicatorY),
p2: new Offset(indicatorRight, indicatorY),
)); ));
}); });
testWidgets('TabBar with indicatorWeight, indicatorPadding (RTL)', (WidgetTester tester) async { testWidgets('TabBar with indicatorWeight, indicatorPadding (RTL)', (WidgetTester tester) async {
const Color color = const Color(0xFF00FF00); const Color indicatorColor = const Color(0xFF00FF00);
const double height = 100.0; const double indicatorWeight = 8.0;
const double weight = 8.0;
const double padLeft = 8.0; const double padLeft = 8.0;
const double padRight = 4.0; const double padRight = 4.0;
final List<Widget> tabs = new List<Widget>.generate(4, (int index) { final List<Widget> tabs = new List<Widget>.generate(4, (int index) {
return new Container( return new Tab(text: 'Tab $index');
key: new ValueKey<int>(index),
height: height,
);
}); });
final TabController controller = new TabController( final TabController controller = new TabController(
...@@ -1015,46 +1013,113 @@ void main() { ...@@ -1015,46 +1013,113 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: new Column( child: new Container(
children: <Widget>[ alignment: Alignment.topLeft,
new TabBar( child: new TabBar(
indicatorWeight: 8.0, indicatorWeight: indicatorWeight,
indicatorColor: color, indicatorColor: indicatorColor,
indicatorPadding: const EdgeInsets.only(left: padLeft, right: padRight), indicatorPadding: const EdgeInsets.only(left: padLeft, right: padRight),
controller: controller, controller: controller,
tabs: tabs, tabs: tabs,
), ),
new Flexible(child: new Container()),
],
), ),
), ),
); );
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(tabBarBox.size.height, 54.0); // 54 = _kTabHeight(46) + indicatorWeight(8.0)
// Selected tab dimensions expect(tabBarBox.size.width, 800.0);
double tabWidth = tester.getSize(find.byKey(const ValueKey<int>(0))).width;
double tabLeft = tester.getTopLeft(find.byKey(const ValueKey<int>(0))).dx; final double indicatorY = 54.0 - indicatorWeight / 2.0;
double tabRight = tabLeft + tabWidth; double indicatorLeft = 600.0 + padLeft + indicatorWeight / 2.0;
double indicatorRight = 800.0 - padRight - indicatorWeight / 2.0;
expect(tabBarBox, paints..rect(
style: PaintingStyle.fill, expect(tabBarBox, paints..line(
color: color, color: indicatorColor,
rect: new Rect.fromLTRB(tabLeft + padLeft, height, tabRight - padRight, height + weight) strokeWidth: indicatorWeight,
p1: new Offset(indicatorLeft, indicatorY),
p2: new Offset(indicatorRight, indicatorY),
)); ));
// Select tab 3 // Select tab 3
controller.index = 3; controller.index = 3;
await tester.pumpAndSettle(); await tester.pumpAndSettle();
tabWidth = tester.getSize(find.byKey(const ValueKey<int>(3))).width; indicatorLeft = padLeft + indicatorWeight / 2.0;
tabLeft = tester.getTopLeft(find.byKey(const ValueKey<int>(3))).dx; indicatorRight = 200.0 - padRight - indicatorWeight / 2.0;
tabRight = tabLeft + tabWidth;
expect(tabBarBox, paints..line(
color: indicatorColor,
strokeWidth: indicatorWeight,
p1: new Offset(indicatorLeft, indicatorY),
p2: new Offset(indicatorRight, indicatorY),
));
});
testWidgets('TabBar changes indicator attributes', (WidgetTester tester) async {
final List<Widget> tabs = new List<Widget>.generate(4, (int index) {
return new Tab(text: 'Tab $index');
});
final TabController controller = new TabController(
vsync: const TestVSync(),
length: tabs.length,
);
Color indicatorColor = const Color(0xFF00FF00);
double indicatorWeight = 8.0;
double padLeft = 8.0;
double padRight = 4.0;
Widget buildFrame() {
return boilerplate(
child: new Container(
alignment: Alignment.topLeft,
child: new TabBar(
indicatorWeight: indicatorWeight,
indicatorColor: indicatorColor,
indicatorPadding: new EdgeInsets.only(left: padLeft, right: padRight),
controller: controller,
tabs: tabs,
),
),
);
}
await tester.pumpWidget(buildFrame());
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(tabBarBox.size.height, 54.0); // 54 = _kTabHeight(46) + indicatorWeight(8.0)
expect(tabBarBox, paints..rect( double indicatorY = 54.0 - indicatorWeight / 2.0;
style: PaintingStyle.fill, double indicatorLeft = padLeft + indicatorWeight / 2.0;
color: color, double indicatorRight = 200.0 - (padRight + indicatorWeight / 2.0);
rect: new Rect.fromLTRB(tabLeft + padLeft, height, tabRight - padRight, height + weight)
expect(tabBarBox, paints..line(
color: indicatorColor,
strokeWidth: indicatorWeight,
p1: new Offset(indicatorLeft, indicatorY),
p2: new Offset(indicatorRight, indicatorY),
));
indicatorColor = const Color(0xFF0000FF);
indicatorWeight = 4.0;
padLeft = 4.0;
padRight = 8.0;
await tester.pumpWidget(buildFrame());
expect(tabBarBox.size.height, 50.0); // 54 = _kTabHeight(46) + indicatorWeight(4.0)
indicatorY = 50.0 - indicatorWeight / 2.0;
indicatorLeft = padLeft + indicatorWeight / 2.0;
indicatorRight = 200.0 - (padRight + indicatorWeight / 2.0);
expect(tabBarBox, paints..line(
color: indicatorColor,
strokeWidth: indicatorWeight,
p1: new Offset(indicatorLeft, indicatorY),
p2: new Offset(indicatorRight, indicatorY),
)); ));
}); });
...@@ -1065,6 +1130,8 @@ void main() { ...@@ -1065,6 +1130,8 @@ void main() {
new SizedBox(key: new UniqueKey(), width: 150.0, height: 50.0), new SizedBox(key: new UniqueKey(), width: 150.0, height: 50.0),
]; ];
const double indicatorWeight = 2.0; // the default
final TabController controller = new TabController( final TabController controller = new TabController(
vsync: const TestVSync(), vsync: const TestVSync(),
length: tabs.length, length: tabs.length,
...@@ -1072,27 +1139,56 @@ void main() { ...@@ -1072,27 +1139,56 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: new Center( child: new Container(
child: new SizedBox( alignment: Alignment.topLeft,
width: 800.0, child: new TabBar(
child: new TabBar( indicatorPadding: const EdgeInsetsDirectional.only(start: 100.0),
indicatorPadding: const EdgeInsetsDirectional.only(start: 100.0), isScrollable: true,
isScrollable: true, controller: controller,
controller: controller, tabs: tabs,
tabs: tabs,
),
), ),
), ),
), ),
); );
expect(tester.getRect(find.byKey(tabs[0].key)), new Rect.fromLTRB(0.0, 284.0, 130.0, 314.0)); final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(tester.getRect(find.byKey(tabs[1].key)), new Rect.fromLTRB(130.0, 279.0, 270.0, 319.0)); const double tabBarHeight = 50.0 + indicatorWeight; // 50 = max tab height
expect(tester.getRect(find.byKey(tabs[2].key)), new Rect.fromLTRB(270.0, 274.0, 420.0, 324.0)); expect(tabBarBox.size.height, tabBarHeight);
expect(tester.firstRenderObject<RenderBox>(find.byType(TabBar)), paints..rect( // Tab0 width = 130, height = 30
style: PaintingStyle.fill, double tabLeft = kTabLabelPadding.left;
rect: new Rect.fromLTRB(100.0, 50.0, 130.0, 52.0), double tabRight = tabLeft + 130.0;
double tabTop = (tabBarHeight - indicatorWeight - 30.0) / 2.0;
double tabBottom = tabTop + 30.0;
Rect tabRect = new Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom);
expect(tester.getRect(find.byKey(tabs[0].key)), tabRect);
// Tab1 width = 140, height = 40
tabLeft = tabRight + kTabLabelPadding.right + kTabLabelPadding.left;
tabRight = tabLeft + 140.0;
tabTop = (tabBarHeight - indicatorWeight - 40.0) / 2.0;
tabBottom = tabTop + 40.0;
tabRect = new Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom);
expect(tester.getRect(find.byKey(tabs[1].key)), tabRect);
// Tab2 width = 150, height = 50
tabLeft = tabRight + kTabLabelPadding.right + kTabLabelPadding.left;
tabRight = tabLeft + 150.0;
tabTop = (tabBarHeight - indicatorWeight - 50.0) / 2.0;
tabBottom = tabTop + 50.0;
tabRect = new Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom);
expect(tester.getRect(find.byKey(tabs[2].key)), tabRect);
// Tab 0 selected, indicator padding resolves to left: 100.0
final double indicatorLeft = 100.0 + indicatorWeight / 2.0;
final double indicatorRight = 130.0 + kTabLabelPadding.horizontal - indicatorWeight / 2.0;
final double indicatorY = tabBottom + indicatorWeight / 2.0;
expect(tabBarBox, paints..line(
strokeWidth: indicatorWeight,
p1: new Offset(indicatorLeft, indicatorY),
p2: new Offset(indicatorRight, indicatorY),
)); ));
}); });
...@@ -1103,6 +1199,8 @@ void main() { ...@@ -1103,6 +1199,8 @@ void main() {
new SizedBox(key: new UniqueKey(), width: 150.0, height: 50.0), new SizedBox(key: new UniqueKey(), width: 150.0, height: 50.0),
]; ];
const double indicatorWeight = 2.0; // the default
final TabController controller = new TabController( final TabController controller = new TabController(
vsync: const TestVSync(), vsync: const TestVSync(),
length: tabs.length, length: tabs.length,
...@@ -1111,36 +1209,62 @@ void main() { ...@@ -1111,36 +1209,62 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: new Center( child: new Container(
child: new SizedBox( alignment: Alignment.topLeft,
width: 800.0, child: new TabBar(
child: new TabBar( indicatorPadding: const EdgeInsetsDirectional.only(start: 100.0),
indicatorPadding: const EdgeInsetsDirectional.only(start: 100.0), isScrollable: true,
isScrollable: true, controller: controller,
controller: controller, tabs: tabs,
tabs: tabs,
),
), ),
), ),
), ),
); );
expect(tester.getRect(find.byKey(tabs[0].key)), new Rect.fromLTRB(670.0, 284.0, 800.0, 314.0)); final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(tester.getRect(find.byKey(tabs[1].key)), new Rect.fromLTRB(530.0, 279.0, 670.0, 319.0)); const double tabBarHeight = 50.0 + indicatorWeight; // 50 = max tab height
expect(tester.getRect(find.byKey(tabs[2].key)), new Rect.fromLTRB(380.0, 274.0, 530.0, 324.0)); expect(tabBarBox.size.height, tabBarHeight);
final RenderBox tabBar = tester.renderObject<RenderBox>(find.byType(CustomPaint).at(1)); // Tab2 width = 150, height = 50
double tabLeft = kTabLabelPadding.left;
expect(tabBar.size, const Size(420.0, 52.0)); double tabRight = tabLeft + 150.0;
expect(tabBar, paints..rect( double tabTop = (tabBarHeight - indicatorWeight - 50.0) / 2.0;
style: PaintingStyle.fill, double tabBottom = tabTop + 50.0;
rect: new Rect.fromLTRB(tabBar.size.width - 130.0, 50.0, tabBar.size.width - 100.0, 52.0), Rect tabRect = new Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom);
expect(tester.getRect(find.byKey(tabs[2].key)), tabRect);
// Tab1 width = 140, height = 40
tabLeft = tabRight + kTabLabelPadding.right + kTabLabelPadding.left;
tabRight = tabLeft + 140.0;
tabTop = (tabBarHeight - indicatorWeight - 40.0) / 2.0;
tabBottom = tabTop + 40.0;
tabRect = new Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom);
expect(tester.getRect(find.byKey(tabs[1].key)), tabRect);
// Tab0 width = 130, height = 30
tabLeft = tabRight + kTabLabelPadding.right + kTabLabelPadding.left;
tabRight = tabLeft + 130.0;
tabTop = (tabBarHeight - indicatorWeight - 30.0) / 2.0;
tabBottom = tabTop + 30.0;
tabRect = new Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom);
expect(tester.getRect(find.byKey(tabs[0].key)), tabRect);
// Tab 0 selected, indicator padding resolves to right: 100.0
final double indicatorLeft = tabLeft - kTabLabelPadding.left + indicatorWeight / 2.0;
final double indicatorRight = tabRight + kTabLabelPadding.left - indicatorWeight / 2.0 - 100.0;
final double indicatorY = 50.0 + indicatorWeight / 2.0;
expect(tabBarBox, paints..line(
strokeWidth: indicatorWeight,
p1: new Offset(indicatorLeft, indicatorY),
p2: new Offset(indicatorRight, indicatorY),
)); ));
}); });
testWidgets('Overflowing RTL tab bar', (WidgetTester tester) async { testWidgets('Overflowing RTL tab bar', (WidgetTester tester) async {
final List<Widget> tabs = new List<Widget>.filled(100, final List<Widget> tabs = new List<Widget>.filled(100,
new SizedBox(key: new UniqueKey(), width: 30.0, height: 20.0), // For convenience padded width of each tab will equal 100:
// 76 + kTabLabelPadding.horizontal(24)
new SizedBox(key: new UniqueKey(), width: 76.0, height: 40.0),
); );
final TabController controller = new TabController( final TabController controller = new TabController(
...@@ -1148,10 +1272,13 @@ void main() { ...@@ -1148,10 +1272,13 @@ void main() {
length: tabs.length, length: tabs.length,
); );
const double indicatorWeight = 2.0; // the default
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: new Center( child: new Container(
alignment: Alignment.topLeft,
child: new TabBar( child: new TabBar(
isScrollable: true, isScrollable: true,
controller: controller, controller: controller,
...@@ -1161,25 +1288,40 @@ void main() { ...@@ -1161,25 +1288,40 @@ void main() {
), ),
); );
expect(tester.firstRenderObject<RenderBox>(find.byType(TabBar)), paints..rect( final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
style: PaintingStyle.fill, const double tabBarHeight = 40.0 + indicatorWeight; // 40 = tab height
rect: new Rect.fromLTRB(2970.0, 20.0, 3000.0, 22.0), expect(tabBarBox.size.height, tabBarHeight);
// Tab 0 out of 100 selected
double indicatorLeft = 99.0 * 100.0 + indicatorWeight / 2.0;
double indicatorRight = 100.0 * 100.0 - indicatorWeight / 2.0;
final double indicatorY = 40.0 + indicatorWeight / 2.0;
expect(tabBarBox, paints..line(
strokeWidth: indicatorWeight,
p1: new Offset(indicatorLeft, indicatorY),
p2: new Offset(indicatorRight, indicatorY),
)); ));
controller.animateTo(tabs.length - 1, duration: const Duration(seconds: 1), curve: Curves.linear); controller.animateTo(tabs.length - 1, duration: const Duration(seconds: 1), curve: Curves.linear);
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
expect(tester.firstRenderObject<RenderBox>(find.byType(TabBar)), paints..rect( // The x coordinates of p1 and p2 were derived empirically, not analytically.
style: PaintingStyle.fill, expect(tabBarBox, paints..line(
rect: new Rect.fromLTRB(742.5, 20.0, 772.5, 22.0), // (these values were derived empirically, not analytically) strokeWidth: indicatorWeight,
p1: new Offset(2476.0, indicatorY),
p2: new Offset(2574.0, indicatorY),
)); ));
await tester.pump(const Duration(milliseconds: 501)); await tester.pump(const Duration(milliseconds: 501));
expect(tester.firstRenderObject<RenderBox>(find.byType(TabBar)), paints..rect( // Tab 99 out of 100 selected, appears on the far left because RTL
style: PaintingStyle.fill, indicatorLeft = indicatorWeight / 2.0;
rect: new Rect.fromLTRB(0.0, 20.0, 30.0, 22.0), indicatorRight = 100.0 - indicatorWeight / 2.0;
expect(tabBarBox, paints..line(
strokeWidth: indicatorWeight,
p1: new Offset(indicatorLeft, indicatorY),
p2: new Offset(indicatorRight, indicatorY),
)); ));
}); });
...@@ -1372,22 +1514,23 @@ void main() { ...@@ -1372,22 +1514,23 @@ void main() {
expect(tester.getSize(find.byType(TabBar)), const Size(800.0, 48.0)); expect(tester.getSize(find.byType(TabBar)), const Size(800.0, 48.0));
expect(tester.getSize(find.byType(TabBarView)), const Size(800.0, 600.0 - 48.0)); expect(tester.getSize(find.byType(TabBarView)), const Size(800.0, 600.0 - 48.0));
// The one tab spans the app's width // The one tab should be center vis the app's width (800).
expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, 0); final double tabLeft = tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx;
expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, 800); final double tabRight = tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx;
expect(tabLeft + (tabRight - tabLeft) / 2.0, 400.0);
// A fling in the TabBar or TabBarView, shouldn't move the tab. // A fling in the TabBar or TabBarView, shouldn't move the tab.
await tester.fling(find.byType(TabBar), const Offset(-100.0, 0.0), 5000.0); await tester.fling(find.byType(TabBar), const Offset(-100.0, 0.0), 5000.0);
await tester.pump(const Duration(milliseconds: 50)); await tester.pump(const Duration(milliseconds: 50));
expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, 0); expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, tabLeft);
expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, 800); expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, tabRight);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 5000.0); await tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 5000.0);
await tester.pump(const Duration(milliseconds: 50)); await tester.pump(const Duration(milliseconds: 50));
expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, 0); expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, tabLeft);
expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, 800); expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, tabRight);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.index, 0); expect(controller.index, 0);
......
...@@ -270,8 +270,8 @@ abstract class PaintPattern { ...@@ -270,8 +270,8 @@ abstract class PaintPattern {
/// Indicates that a line is expected next. /// Indicates that a line is expected next.
/// ///
/// The next line is examined. Any arguments that are passed to this method /// The next line is examined. Any arguments that are passed to this method
/// are compared to the actual [Canvas.drawLine] call's `paint` argument, and /// are compared to the actual [Canvas.drawLine] call's `p1`, `p2`, and
/// any mismatches result in failure. /// `paint` arguments, and any mismatches result in failure.
/// ///
/// If no call to [Canvas.drawLine] was made, then this results in failure. /// If no call to [Canvas.drawLine] was made, then this results in failure.
/// ///
...@@ -283,7 +283,7 @@ abstract class PaintPattern { ...@@ -283,7 +283,7 @@ abstract class PaintPattern {
/// painting has completed, not at the time of the call. If the same [Paint] /// painting has completed, not at the time of the call. If the same [Paint]
/// object is reused multiple times, then this may not match the actual /// object is reused multiple times, then this may not match the actual
/// arguments as they were seen by the method. /// arguments as they were seen by the method.
void line({ Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }); void line({ Offset p1, Offset p2, Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style });
/// Indicates that an arc is expected next. /// Indicates that an arc is expected next.
/// ///
...@@ -690,8 +690,8 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher imp ...@@ -690,8 +690,8 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher imp
} }
@override @override
void line({ Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }) { void line({ Offset p1, Offset p2, Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }) {
_predicates.add(new _LinePaintPredicate(color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style)); _predicates.add(new _LinePaintPredicate(p1: p1, p2: p2, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
} }
@override @override
...@@ -1073,11 +1073,38 @@ class _PathPaintPredicate extends _DrawCommandPaintPredicate { ...@@ -1073,11 +1073,38 @@ class _PathPaintPredicate extends _DrawCommandPaintPredicate {
} }
} }
// TODO(ianh): add arguments to test the points, length, angle, that kind of thing // TODO(ianh): add arguments to test the length, angle, that kind of thing
class _LinePaintPredicate extends _DrawCommandPaintPredicate { class _LinePaintPredicate extends _DrawCommandPaintPredicate {
_LinePaintPredicate({ Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }) : super( _LinePaintPredicate({ this.p1, this.p2, Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }) : super(
#drawLine, 'a line', 3, 2, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style #drawLine, 'a line', 3, 2, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style
); );
final Offset p1;
final Offset p2;
@override
void verifyArguments(List<dynamic> arguments) {
super.verifyArguments(arguments); // Checks the 3rd argument, a Paint
if (arguments.length != 3)
throw 'It called $methodName with ${arguments.length} arguments; expected 3.';
final Offset p1Argument = arguments[0];
final Offset p2Argument = arguments[1];
if (p1 != null && p1Argument != p1) {
throw 'It called $methodName with p1 endpoint, $p1Argument, which was not exactly the expected endpoint ($p1).';
}
if (p2 != null && p2Argument != p2) {
throw 'It called $methodName with p2 endpoint, $p2Argument, which was not exactly the expected endpoint ($p2).';
}
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (p1 != null)
description.add('end point p1: $p1');
if (p2 != null)
description.add('end point p2: $p2');
}
} }
class _ArcPaintPredicate extends _DrawCommandPaintPredicate { class _ArcPaintPredicate extends _DrawCommandPaintPredicate {
......
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