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.
|
||||
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)
|
||||
));
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user