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;
/// 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);
......@@ -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<double> initialTabOffsets,
}) : _tabOffsets = initialTabOffsets, super(repaint: controller.animation);
TabController controller;
List<double> tabOffsets;
Color color;
Rect currentRect;
final TabController controller;
final double indicatorWeight;
final EdgeInsets indicatorPadding;
List<double> _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<double> a, List<double> b) {
static bool _tabOffsetsNotEqual(List<double> a, List<double> 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<TabBar> {
_controller.animation.addListener(_handleTabControllerAnimationTick);
_controller.addListener(_handleTabControllerTick);
_currentIndex = _controller.index;
final List<double> offsets = _indicatorPainter?.tabOffsets;
_indicatorPainter = new _IndicatorPainter(_controller)..tabOffsets = offsets;
final List<double> offsets = _indicatorPainter?._tabOffsets;
_indicatorPainter = new _IndicatorPainter(
controller: _controller,
indicatorWeight: widget.indicatorWeight,
indicatorPadding: widget.indicatorPadding,
initialTabOffsets: offsets,
);
}
}
......@@ -543,14 +578,14 @@ class _TabBarState extends State<TabBar> {
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<double> tabOffsets = _indicatorPainter.tabOffsets;
final List<double> 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<TabBar> {
// Called each time layout completes.
void _saveTabOffsets(List<double> tabOffsets) {
_indicatorPainter?.tabOffsets = tabOffsets;
_indicatorPainter?._tabOffsets = tabOffsets;
}
void _handleTap(int index) {
......@@ -638,8 +673,8 @@ class _TabBarState extends State<TabBar> {
// 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<TabBar> {
// 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<TabBar> {
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,
......
......@@ -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<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