Add indicatorWeight, indicatorPadding to TabBar (#10600)
This commit is contained in:
parent
4bde698ffc
commit
09eba82a9e
@ -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 @@
|
|||||||
// 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';
|
|||||||
|
|
||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
@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 {
|
|||||||
///
|
///
|
||||||
/// 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 {
|
|||||||
/// 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 {
|
|||||||
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> {
|
|||||||
_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> {
|
|||||||
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> {
|
|||||||
|
|
||||||
// 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> {
|
|||||||
// 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> {
|
|||||||
// 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> {
|
|||||||
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';
|
|||||||
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() {
|
|||||||
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)
|
||||||
|
));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user