Commit 09eba82a authored by Hans Muller's avatar Hans Muller Committed by GitHub

Add indicatorWeight, indicatorPadding to TabBar (#10600)

parent 4bde698f
...@@ -28,5 +28,8 @@ const int kRadialReactionAlpha = 0x33; ...@@ -28,5 +28,8 @@ const int kRadialReactionAlpha = 0x33;
/// The duration of the horizontal scroll animation that occurs when a tab is tapped. /// The duration of the horizontal scroll animation that occurs when a tab is tapped.
const Duration kTabScrollDuration = const Duration(milliseconds: 300); 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. /// The padding added around material list items.
const EdgeInsets kMaterialListPadding = const EdgeInsets.symmetric(vertical: 8.0); const EdgeInsets kMaterialListPadding = const EdgeInsets.symmetric(vertical: 8.0);
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// 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';
...@@ -20,10 +21,8 @@ import 'theme.dart'; ...@@ -20,10 +21,8 @@ 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 _kTabIndicatorHeight = 2.0;
const double _kMinTabWidth = 72.0; const double _kMinTabWidth = 72.0;
const double _kMaxTabWidth = 264.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 /// 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.
...@@ -82,7 +81,7 @@ class Tab extends StatelessWidget { ...@@ -82,7 +81,7 @@ class Tab extends StatelessWidget {
} }
return new Container( return new Container(
padding: _kTabLabelPadding, padding: kTabLabelPadding,
height: height, height: height,
constraints: const BoxConstraints(minWidth: _kMinTabWidth), constraints: const BoxConstraints(minWidth: _kMinTabWidth),
child: new Center(child: label), child: new Center(child: label),
...@@ -238,30 +237,39 @@ double _indexChangeProgress(TabController controller) { ...@@ -238,30 +237,39 @@ double _indexChangeProgress(TabController controller) {
} }
class _IndicatorPainter extends CustomPainter { class _IndicatorPainter extends CustomPainter {
_IndicatorPainter(this.controller) : super(repaint: controller.animation); _IndicatorPainter({
this.controller,
this.indicatorWeight,
this.indicatorPadding,
List<double> initialTabOffsets,
}) : _tabOffsets = initialTabOffsets, super(repaint: controller.animation);
TabController controller; final TabController controller;
List<double> tabOffsets; final double indicatorWeight;
Color color; final EdgeInsets indicatorPadding;
Rect currentRect; List<double> _tabOffsets;
Color _color;
Rect _currentRect;
// tabOffsets[index] is the offset of the left edge of the tab at index, and // _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. // _tabOffsets[_tabOffsets.length] is the right edge of the last tab.
int get maxTabIndex => tabOffsets.length - 2; int get maxTabIndex => _tabOffsets.length - 2;
Rect indicatorRect(Size tabBarSize, int tabIndex) { Rect indicatorRect(Size tabBarSize, int tabIndex) {
assert(tabOffsets != null && tabIndex >= 0 && tabIndex <= maxTabIndex); assert(_tabOffsets != null && tabIndex >= 0 && tabIndex <= maxTabIndex);
final double tabLeft = tabOffsets[tabIndex]; double tabLeft = _tabOffsets[tabIndex];
final double tabRight = tabOffsets[tabIndex + 1]; double tabRight = _tabOffsets[tabIndex + 1];
final double tabTop = tabBarSize.height - _kTabIndicatorHeight; tabLeft = math.min(tabLeft + indicatorPadding.left, tabRight);
return new Rect.fromLTWH(tabLeft, tabTop, tabRight - tabLeft, _kTabIndicatorHeight); tabRight = math.max(tabRight - indicatorPadding.right, tabLeft);
final double tabTop = tabBarSize.height - indicatorWeight;
return new Rect.fromLTWH(tabLeft, tabTop, tabRight - tabLeft, indicatorWeight);
} }
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
if (controller.indexIsChanging) { if (controller.indexIsChanging) {
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 {
final int currentIndex = controller.index; final int currentIndex = controller.index;
final Rect left = currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null; final Rect left = currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null;
...@@ -271,21 +279,21 @@ class _IndicatorPainter extends CustomPainter { ...@@ -271,21 +279,21 @@ class _IndicatorPainter extends CustomPainter {
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 = left ?? middle;
else if (value == index + 1.0) else if (value == index + 1.0)
currentRect = right ?? middle; _currentRect = right ?? 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 = left == null ? middle : Rect.lerp(middle, left, index - value);
else else
currentRect = right == null ? middle : Rect.lerp(middle, right, value - index); _currentRect = right == null ? middle : Rect.lerp(middle, right, 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 _tabOffsetsNotEqual(List<double> a, List<double> b) {
assert(a != null && b != null && a.length == b.length); assert(a != null && b != null && a.length == b.length);
for(int i = 0; i < a.length; i++) { for(int i = 0; i < a.length; i++) {
if (a[i] != b[i]) if (a[i] != b[i])
...@@ -297,9 +305,9 @@ class _IndicatorPainter extends CustomPainter { ...@@ -297,9 +305,9 @@ class _IndicatorPainter extends CustomPainter {
@override @override
bool shouldRepaint(_IndicatorPainter old) { bool shouldRepaint(_IndicatorPainter old) {
return controller != old.controller || return controller != old.controller ||
tabOffsets?.length != old.tabOffsets?.length || _tabOffsets?.length != old._tabOffsets?.length ||
tabOffsetsNotEqual(tabOffsets, old.tabOffsets) || _tabOffsetsNotEqual(_tabOffsets, old._tabOffsets) ||
currentRect != old.currentRect; _currentRect != old._currentRect;
} }
} }
...@@ -400,18 +408,26 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -400,18 +408,26 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// ///
/// If a [TabController] is not provided, then there must be a /// If a [TabController] is not provided, then there must be a
/// [DefaultTabController] ancestor. /// [DefaultTabController] ancestor.
///
/// The [indicatorWeight] parameter defaults to 2, and cannot be null.
///
/// The [indicatorPadding] parameter defaults to [EdgeInsets.zero], and cannot be null.
TabBar({ TabBar({
Key key, Key key,
@required this.tabs, @required this.tabs,
this.controller, this.controller,
this.isScrollable: false, this.isScrollable: false,
this.indicatorColor, this.indicatorColor,
this.indicatorWeight: 2.0,
this.indicatorPadding: EdgeInsets.zero,
this.labelColor, this.labelColor,
this.labelStyle, this.labelStyle,
this.unselectedLabelColor, this.unselectedLabelColor,
this.unselectedLabelStyle, this.unselectedLabelStyle,
}) : assert(tabs != null && tabs.length > 1), }) : assert(tabs != null && tabs.length > 1),
assert(isScrollable != null), assert(isScrollable != null),
assert(indicatorWeight != null && indicatorWeight > 0.0),
assert(indicatorPadding != null),
super(key: key); super(key: key);
/// Typically a list of [Tab] widgets. /// Typically a list of [Tab] widgets.
...@@ -434,6 +450,20 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -434,6 +450,20 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// 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.
final Color indicatorColor; 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. /// 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%
...@@ -472,10 +502,10 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -472,10 +502,10 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
if (item is Tab) { if (item is Tab) {
final Tab tab = item; final Tab tab = item;
if (tab.text != null && tab.icon != null) 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 @override
...@@ -515,8 +545,13 @@ class _TabBarState extends State<TabBar> { ...@@ -515,8 +545,13 @@ 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; final List<double> offsets = _indicatorPainter?._tabOffsets;
_indicatorPainter = new _IndicatorPainter(_controller)..tabOffsets = offsets; _indicatorPainter = new _IndicatorPainter(
controller: _controller,
indicatorWeight: widget.indicatorWeight,
indicatorPadding: widget.indicatorPadding,
initialTabOffsets: offsets,
);
} }
} }
...@@ -543,14 +578,14 @@ class _TabBarState extends State<TabBar> { ...@@ -543,14 +578,14 @@ class _TabBarState extends State<TabBar> {
super.dispose(); super.dispose();
} }
// tabOffsets[index] is the offset of the left edge of the tab at index, and // _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. // _tabOffsets[_tabOffsets.length] is the right edge of the last tab.
int get maxTabIndex => _indicatorPainter.tabOffsets.length - 2; 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; final List<double> tabOffsets = _indicatorPainter._tabOffsets;
assert(tabOffsets != null && index >= 0 && index <= maxTabIndex); assert(tabOffsets != null && index >= 0 && index <= maxTabIndex);
final double tabCenter = (tabOffsets[index] + tabOffsets[index + 1]) / 2.0; final double tabCenter = (tabOffsets[index] + tabOffsets[index + 1]) / 2.0;
return (tabCenter - viewportWidth / 2.0).clamp(minExtent, maxExtent); return (tabCenter - viewportWidth / 2.0).clamp(minExtent, maxExtent);
...@@ -610,7 +645,7 @@ class _TabBarState extends State<TabBar> { ...@@ -610,7 +645,7 @@ 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) {
_indicatorPainter?.tabOffsets = tabOffsets; _indicatorPainter?._tabOffsets = tabOffsets;
} }
void _handleTap(int index) { void _handleTap(int index) {
...@@ -638,8 +673,8 @@ class _TabBarState extends State<TabBar> { ...@@ -638,8 +673,8 @@ class _TabBarState extends State<TabBar> {
// 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; _indicatorPainter._color = widget.indicatorColor ?? Theme.of(context).indicatorColor;
if (_indicatorPainter.color == Material.of(context).color) { if (_indicatorPainter._color == Material.of(context).color) {
// ThemeData tries to avoid this by having indicatorColor avoid being the // ThemeData tries to avoid this by having indicatorColor avoid being the
// primaryColor. However, it's possible that the tab bar is on a // primaryColor. However, it's possible that the tab bar is on a
// Material that isn't the primaryColor. In that case, if the indicator // Material that isn't the primaryColor. In that case, if the indicator
...@@ -647,7 +682,7 @@ class _TabBarState extends State<TabBar> { ...@@ -647,7 +682,7 @@ class _TabBarState extends State<TabBar> {
// automatic transitions of the theme will likely look ugly as the // 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 // indicator color suddenly snaps to white at one end, but it's not clear
// how to avoid that any further. // how to avoid that any further.
_indicatorPainter.color = Colors.white; _indicatorPainter._color = Colors.white;
} }
if (_controller.index != _currentIndex) { if (_controller.index != _currentIndex) {
...@@ -697,7 +732,7 @@ class _TabBarState extends State<TabBar> { ...@@ -697,7 +732,7 @@ class _TabBarState extends State<TabBar> {
Widget tabBar = new CustomPaint( Widget tabBar = new CustomPaint(
painter: _indicatorPainter, painter: _indicatorPainter,
child: new Padding( child: new Padding(
padding: const EdgeInsets.only(bottom: _kTabIndicatorHeight), padding: new EdgeInsets.only(bottom: widget.indicatorWeight),
child: new _TabStyle( child: new _TabStyle(
animation: kAlwaysDismissedAnimation, animation: kAlwaysDismissedAnimation,
selected: false, selected: false,
......
...@@ -6,6 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -6,6 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../rendering/mock_canvas.dart';
import '../rendering/recording_canvas.dart'; import '../rendering/recording_canvas.dart';
class StateMarker extends StatefulWidget { class StateMarker extends StatefulWidget {
...@@ -835,4 +836,68 @@ void main() { ...@@ -835,4 +836,68 @@ void main() {
expect(find.text('TAB #19'), findsOneWidget); expect(find.text('TAB #19'), findsOneWidget);
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 {
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(
new Material(
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)
));
});
} }
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