From 09eba82a9efaf705178f13ed34b8f2c93b456f44 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Fri, 9 Jun 2017 15:06:03 -0700 Subject: [PATCH] Add indicatorWeight, indicatorPadding to TabBar (#10600) --- .../flutter/lib/src/material/constants.dart | 3 + packages/flutter/lib/src/material/tabs.dart | 117 ++++++++++++------ packages/flutter/test/material/tabs_test.dart | 65 ++++++++++ 3 files changed, 144 insertions(+), 41 deletions(-) diff --git a/packages/flutter/lib/src/material/constants.dart b/packages/flutter/lib/src/material/constants.dart index 2558975639..22288167fb 100644 --- a/packages/flutter/lib/src/material/constants.dart +++ b/packages/flutter/lib/src/material/constants.dart @@ -28,5 +28,8 @@ const int kRadialReactionAlpha = 0x33; /// The duration of the horizontal scroll animation that occurs when a tab is tapped. const Duration kTabScrollDuration = const Duration(milliseconds: 300); +/// The horizontal padding included by [Tab]s. +const EdgeInsets kTabLabelPadding = const EdgeInsets.symmetric(horizontal: 12.0); + /// The padding added around material list items. const EdgeInsets kMaterialListPadding = const EdgeInsets.symmetric(vertical: 8.0); diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 3b850f41a3..a67bf0509c 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:math' as math; import 'dart:ui' show lerpDouble; import 'package:flutter/foundation.dart'; @@ -20,10 +21,8 @@ import 'theme.dart'; const double _kTabHeight = 46.0; const double _kTextAndIconTabHeight = 72.0; -const double _kTabIndicatorHeight = 2.0; const double _kMinTabWidth = 72.0; const double _kMaxTabWidth = 264.0; -const EdgeInsets _kTabLabelPadding = const EdgeInsets.symmetric(horizontal: 12.0); /// A material design [TabBar] tab. If both [icon] and [text] are /// provided, the text is displayed below the icon. @@ -82,7 +81,7 @@ class Tab extends StatelessWidget { } return new Container( - padding: _kTabLabelPadding, + padding: kTabLabelPadding, height: height, constraints: const BoxConstraints(minWidth: _kMinTabWidth), child: new Center(child: label), @@ -238,30 +237,39 @@ double _indexChangeProgress(TabController controller) { } class _IndicatorPainter extends CustomPainter { - _IndicatorPainter(this.controller) : super(repaint: controller.animation); + _IndicatorPainter({ + this.controller, + this.indicatorWeight, + this.indicatorPadding, + List initialTabOffsets, + }) : _tabOffsets = initialTabOffsets, super(repaint: controller.animation); - TabController controller; - List tabOffsets; - Color color; - Rect currentRect; + final TabController controller; + final double indicatorWeight; + final EdgeInsets indicatorPadding; + List _tabOffsets; + 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; + // _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; Rect indicatorRect(Size tabBarSize, int tabIndex) { - assert(tabOffsets != null && tabIndex >= 0 && tabIndex <= maxTabIndex); - final double tabLeft = tabOffsets[tabIndex]; - final double tabRight = tabOffsets[tabIndex + 1]; - final double tabTop = tabBarSize.height - _kTabIndicatorHeight; - return new Rect.fromLTWH(tabLeft, tabTop, tabRight - tabLeft, _kTabIndicatorHeight); + 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); + final double tabTop = tabBarSize.height - indicatorWeight; + return new Rect.fromLTWH(tabLeft, tabTop, tabRight - tabLeft, indicatorWeight); } @override void paint(Canvas canvas, Size size) { if (controller.indexIsChanging) { final Rect targetRect = indicatorRect(size, controller.index); - currentRect = Rect.lerp(targetRect, currentRect ?? targetRect, _indexChangeProgress(controller)); + _currentRect = Rect.lerp(targetRect, _currentRect ?? targetRect, _indexChangeProgress(controller)); } else { final int currentIndex = controller.index; final Rect left = currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null; @@ -271,21 +279,21 @@ class _IndicatorPainter extends CustomPainter { final double index = controller.index.toDouble(); final double value = controller.animation.value; if (value == index - 1.0) - currentRect = left ?? middle; + _currentRect = left ?? middle; else if (value == index + 1.0) - currentRect = right ?? middle; + _currentRect = right ?? middle; else if (value == index) - currentRect = middle; + _currentRect = middle; else if (value < index) - currentRect = left == null ? middle : Rect.lerp(middle, left, index - value); + _currentRect = left == null ? middle : Rect.lerp(middle, left, index - value); else - currentRect = right == null ? middle : Rect.lerp(middle, right, value - index); + _currentRect = right == null ? middle : Rect.lerp(middle, right, value - index); } - assert(currentRect != null); - canvas.drawRect(currentRect, new Paint()..color = color); + assert(_currentRect != null); + canvas.drawRect(_currentRect, new Paint()..color = _color); } - static bool tabOffsetsNotEqual(List a, List b) { + static bool _tabOffsetsNotEqual(List a, List b) { assert(a != null && b != null && a.length == b.length); for(int i = 0; i < a.length; i++) { if (a[i] != b[i]) @@ -297,9 +305,9 @@ class _IndicatorPainter extends CustomPainter { @override bool shouldRepaint(_IndicatorPainter old) { return controller != old.controller || - tabOffsets?.length != old.tabOffsets?.length || - tabOffsetsNotEqual(tabOffsets, old.tabOffsets) || - currentRect != old.currentRect; + _tabOffsets?.length != old._tabOffsets?.length || + _tabOffsetsNotEqual(_tabOffsets, old._tabOffsets) || + _currentRect != old._currentRect; } } @@ -400,18 +408,26 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { /// /// If a [TabController] is not provided, then there must be a /// [DefaultTabController] ancestor. + /// + /// The [indicatorWeight] parameter defaults to 2, and cannot be null. + /// + /// The [indicatorPadding] parameter defaults to [EdgeInsets.zero], and cannot be null. TabBar({ Key key, @required this.tabs, this.controller, this.isScrollable: false, this.indicatorColor, + this.indicatorWeight: 2.0, + this.indicatorPadding: EdgeInsets.zero, this.labelColor, this.labelStyle, this.unselectedLabelColor, this.unselectedLabelStyle, }) : assert(tabs != null && tabs.length > 1), assert(isScrollable != null), + assert(indicatorWeight != null && indicatorWeight > 0.0), + assert(indicatorPadding != null), super(key: key); /// Typically a list of [Tab] widgets. @@ -434,6 +450,20 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { /// is null then the value of the Theme's indicatorColor property is used. final Color indicatorColor; + /// The thickness of the line that appears below the selected tab. The value + /// of this parameter must be greater than zero. + /// + /// The default value of [indicatorWeight] is 2.0. + final double indicatorWeight; + + /// The horizontal padding for the line that appears below the selected tab. + /// For [isScrollable] tab bars, specifying [kDefaultTabLabelPadding] will align + /// the indicator with the tab's text for [Tab] widgets and all but the + /// shortest [Tab.text] values. + /// + /// The default value of [indicatorPadding] is [EdgeInsets.zero]. + final EdgeInsets indicatorPadding; + /// The color of selected tab labels. /// /// Unselected tab labels are rendered with the same color rendered at 70% @@ -472,10 +502,10 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { if (item is Tab) { final Tab tab = item; if (tab.text != null && tab.icon != null) - return const Size.fromHeight(_kTextAndIconTabHeight + _kTabIndicatorHeight); + return new Size.fromHeight(_kTextAndIconTabHeight + indicatorWeight); } } - return const Size.fromHeight(_kTabHeight + _kTabIndicatorHeight); + return new Size.fromHeight(_kTabHeight + indicatorWeight); } @override @@ -515,8 +545,13 @@ class _TabBarState extends State { _controller.animation.addListener(_handleTabControllerAnimationTick); _controller.addListener(_handleTabControllerTick); _currentIndex = _controller.index; - final List offsets = _indicatorPainter?.tabOffsets; - _indicatorPainter = new _IndicatorPainter(_controller)..tabOffsets = offsets; + final List offsets = _indicatorPainter?._tabOffsets; + _indicatorPainter = new _IndicatorPainter( + controller: _controller, + indicatorWeight: widget.indicatorWeight, + indicatorPadding: widget.indicatorPadding, + initialTabOffsets: offsets, + ); } } @@ -543,14 +578,14 @@ 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; + // _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; double _tabScrollOffset(int index, double viewportWidth, double minExtent, double maxExtent) { if (!widget.isScrollable) return 0.0; - final List tabOffsets = _indicatorPainter.tabOffsets; + final List tabOffsets = _indicatorPainter._tabOffsets; assert(tabOffsets != null && index >= 0 && index <= maxTabIndex); final double tabCenter = (tabOffsets[index] + tabOffsets[index + 1]) / 2.0; return (tabCenter - viewportWidth / 2.0).clamp(minExtent, maxExtent); @@ -610,7 +645,7 @@ class _TabBarState extends State { // Called each time layout completes. void _saveTabOffsets(List tabOffsets) { - _indicatorPainter?.tabOffsets = tabOffsets; + _indicatorPainter?._tabOffsets = tabOffsets; } void _handleTap(int index) { @@ -638,8 +673,8 @@ class _TabBarState extends State { // 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. if (_controller != null) { - _indicatorPainter.color = widget.indicatorColor ?? Theme.of(context).indicatorColor; - if (_indicatorPainter.color == Material.of(context).color) { + _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 @@ -647,7 +682,7 @@ class _TabBarState extends State { // 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; + _indicatorPainter._color = Colors.white; } if (_controller.index != _currentIndex) { @@ -697,7 +732,7 @@ class _TabBarState extends State { Widget tabBar = new CustomPaint( painter: _indicatorPainter, child: new Padding( - padding: const EdgeInsets.only(bottom: _kTabIndicatorHeight), + padding: new EdgeInsets.only(bottom: widget.indicatorWeight), child: new _TabStyle( animation: kAlwaysDismissedAnimation, selected: false, diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index 9961768f2c..988e25870c 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -6,6 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import '../rendering/mock_canvas.dart'; import '../rendering/recording_canvas.dart'; class StateMarker extends StatefulWidget { @@ -835,4 +836,68 @@ void main() { expect(find.text('TAB #19'), findsOneWidget); expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB #19')).dx, 800.0); }); + + testWidgets('TabBar with indicatorWeight, indicatorPadding', (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( + new Material( + 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) + )); + }); }