TabBar RTL (#13164)
This commit is contained in:
parent
112df6efb0
commit
2bda59a1fe
@ -11,18 +11,26 @@ enum TabsDemoStyle {
|
||||
}
|
||||
|
||||
class _Page {
|
||||
_Page({ this.icon, this.text });
|
||||
const _Page({ this.icon, this.text });
|
||||
final IconData icon;
|
||||
final String text;
|
||||
}
|
||||
|
||||
final List<_Page> _allPages = <_Page>[
|
||||
new _Page(icon: Icons.event, text: 'EVENT'),
|
||||
new _Page(icon: Icons.home, text: 'HOME'),
|
||||
new _Page(icon: Icons.android, text: 'ANDROID'),
|
||||
new _Page(icon: Icons.alarm, text: 'ALARM'),
|
||||
new _Page(icon: Icons.face, text: 'FACE'),
|
||||
new _Page(icon: Icons.language, text: 'LANGUAGE'),
|
||||
const List<_Page> _allPages = const <_Page>[
|
||||
const _Page(icon: Icons.grade, text: 'TRIUMPH'),
|
||||
const _Page(icon: Icons.playlist_add, text: 'NOTE'),
|
||||
const _Page(icon: Icons.check_circle, text: 'SUCCESS'),
|
||||
const _Page(icon: Icons.question_answer, text: 'OVERSTATE'),
|
||||
const _Page(icon: Icons.sentiment_very_satisfied, text: 'SATISFACTION'),
|
||||
const _Page(icon: Icons.camera, text: 'APERTURE'),
|
||||
const _Page(icon: Icons.assignment_late, text: 'WE MUST'),
|
||||
const _Page(icon: Icons.assignment_turned_in, text: 'WE CAN'),
|
||||
const _Page(icon: Icons.group, text: 'ALL'),
|
||||
const _Page(icon: Icons.block, text: 'EXCEPT'),
|
||||
const _Page(icon: Icons.sentiment_very_dissatisfied, text: 'CRYING'),
|
||||
const _Page(icon: Icons.error, text: 'MISTAKE'),
|
||||
const _Page(icon: Icons.loop, text: 'TRYING'),
|
||||
const _Page(icon: Icons.cake, text: 'CAKE'),
|
||||
];
|
||||
|
||||
class ScrollableTabsDemo extends StatefulWidget {
|
||||
|
@ -155,18 +155,20 @@ class _TabStyle extends AnimatedWidget {
|
||||
}
|
||||
}
|
||||
|
||||
typedef void _LayoutCallback(List<double> xOffsets, TextDirection textDirection, double width);
|
||||
|
||||
class _TabLabelBarRenderer extends RenderFlex {
|
||||
_TabLabelBarRenderer({
|
||||
List<RenderBox> children,
|
||||
Axis direction,
|
||||
MainAxisSize mainAxisSize,
|
||||
MainAxisAlignment mainAxisAlignment,
|
||||
CrossAxisAlignment crossAxisAlignment,
|
||||
TextDirection textDirection,
|
||||
VerticalDirection verticalDirection,
|
||||
TextBaseline textBaseline,
|
||||
@required Axis direction,
|
||||
@required MainAxisSize mainAxisSize,
|
||||
@required MainAxisAlignment mainAxisAlignment,
|
||||
@required CrossAxisAlignment crossAxisAlignment,
|
||||
@required TextDirection textDirection,
|
||||
@required VerticalDirection verticalDirection,
|
||||
@required this.onPerformLayout,
|
||||
}) : assert(onPerformLayout != null),
|
||||
assert(textDirection != null),
|
||||
super(
|
||||
children: children,
|
||||
direction: direction,
|
||||
@ -175,14 +177,17 @@ class _TabLabelBarRenderer extends RenderFlex {
|
||||
crossAxisAlignment: crossAxisAlignment,
|
||||
textDirection: textDirection,
|
||||
verticalDirection: verticalDirection,
|
||||
textBaseline: textBaseline,
|
||||
);
|
||||
|
||||
ValueChanged<List<double>> onPerformLayout;
|
||||
_LayoutCallback onPerformLayout;
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
super.performLayout();
|
||||
// xOffsets will contain childCount+1 values, giving the offsets of the
|
||||
// leading edge of the first tab as the first value, of the leading edge of
|
||||
// the each subsequent tab as each subsequent value, and of the trailing
|
||||
// edge of the last tab as the last value.
|
||||
RenderBox child = firstChild;
|
||||
final List<double> xOffsets = <double>[];
|
||||
while (child != null) {
|
||||
@ -191,8 +196,16 @@ class _TabLabelBarRenderer extends RenderFlex {
|
||||
assert(child.parentData == childParentData);
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
xOffsets.add(size.width); // So xOffsets[lastTabIndex + 1] is valid.
|
||||
onPerformLayout(xOffsets);
|
||||
assert(textDirection != null);
|
||||
switch (textDirection) {
|
||||
case TextDirection.rtl:
|
||||
xOffsets.insert(0, size.width);
|
||||
break;
|
||||
case TextDirection.ltr:
|
||||
xOffsets.add(size.width);
|
||||
break;
|
||||
}
|
||||
onPerformLayout(xOffsets, textDirection, size.width);
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,10 +215,6 @@ class _TabLabelBarRenderer extends RenderFlex {
|
||||
class _TabLabelBar extends Flex {
|
||||
_TabLabelBar({
|
||||
Key key,
|
||||
MainAxisAlignment mainAxisAlignment,
|
||||
CrossAxisAlignment crossAxisAlignment,
|
||||
TextDirection textDirection,
|
||||
VerticalDirection verticalDirection: VerticalDirection.down,
|
||||
List<Widget> children: const <Widget>[],
|
||||
this.onPerformLayout,
|
||||
}) : super(
|
||||
@ -215,11 +224,10 @@ class _TabLabelBar extends Flex {
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
textDirection: textDirection,
|
||||
verticalDirection: verticalDirection,
|
||||
verticalDirection: VerticalDirection.down,
|
||||
);
|
||||
|
||||
final ValueChanged<List<double>> onPerformLayout;
|
||||
final _LayoutCallback onPerformLayout;
|
||||
|
||||
@override
|
||||
RenderFlex createRenderObject(BuildContext context) {
|
||||
@ -230,7 +238,6 @@ class _TabLabelBar extends Flex {
|
||||
crossAxisAlignment: crossAxisAlignment,
|
||||
textDirection: getEffectiveTextDirection(context),
|
||||
verticalDirection: verticalDirection,
|
||||
textBaseline: textBaseline,
|
||||
onPerformLayout: onPerformLayout,
|
||||
);
|
||||
}
|
||||
@ -258,29 +265,66 @@ double _indexChangeProgress(TabController controller) {
|
||||
|
||||
class _IndicatorPainter extends CustomPainter {
|
||||
_IndicatorPainter({
|
||||
this.controller,
|
||||
this.indicatorWeight,
|
||||
this.indicatorPadding,
|
||||
List<double> initialTabOffsets,
|
||||
}) : _tabOffsets = initialTabOffsets, super(repaint: controller.animation);
|
||||
@required this.controller,
|
||||
@required this.indicatorWeight,
|
||||
@required this.indicatorPadding,
|
||||
_IndicatorPainter old,
|
||||
}) : assert(controller != null),
|
||||
assert(indicatorWeight != null),
|
||||
assert(indicatorPadding != null),
|
||||
super(repaint: controller.animation) {
|
||||
if (old != null)
|
||||
saveTabOffsets(old._currentTabOffsets, old._currentTextDirection);
|
||||
}
|
||||
|
||||
final TabController controller;
|
||||
final double indicatorWeight;
|
||||
final EdgeInsets indicatorPadding;
|
||||
List<double> _tabOffsets;
|
||||
final EdgeInsetsGeometry indicatorPadding;
|
||||
|
||||
List<double> _currentTabOffsets;
|
||||
TextDirection _currentTextDirection;
|
||||
EdgeInsets _resolvedIndicatorPadding;
|
||||
|
||||
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;
|
||||
void saveTabOffsets(List<double> tabOffsets, TextDirection textDirection) {
|
||||
_currentTabOffsets = tabOffsets;
|
||||
_currentTextDirection = textDirection;
|
||||
_resolvedIndicatorPadding = indicatorPadding.resolve(_currentTextDirection);
|
||||
}
|
||||
|
||||
// _currentTabOffsets[index] is the offset of the start edge of the tab at index, and
|
||||
// _currentTabOffsets[_currentTabOffsets.length] is the end edge of the last tab.
|
||||
int get maxTabIndex => _currentTabOffsets.length - 2;
|
||||
|
||||
double centerOf(int tabIndex) {
|
||||
assert(_currentTabOffsets != null);
|
||||
assert(_currentTabOffsets.isNotEmpty);
|
||||
assert(tabIndex >= 0);
|
||||
assert(tabIndex <= maxTabIndex);
|
||||
return (_currentTabOffsets[tabIndex] + _currentTabOffsets[tabIndex + 1]) / 2.0;
|
||||
}
|
||||
|
||||
Rect indicatorRect(Size tabBarSize, int tabIndex) {
|
||||
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);
|
||||
assert(_currentTabOffsets != null);
|
||||
assert(_currentTextDirection != null);
|
||||
assert(_currentTabOffsets.isNotEmpty);
|
||||
assert(tabIndex >= 0);
|
||||
assert(tabIndex <= maxTabIndex);
|
||||
double tabLeft, tabRight;
|
||||
switch (_currentTextDirection) {
|
||||
case TextDirection.rtl:
|
||||
tabLeft = _currentTabOffsets[tabIndex + 1];
|
||||
tabRight = _currentTabOffsets[tabIndex];
|
||||
break;
|
||||
case TextDirection.ltr:
|
||||
tabLeft = _currentTabOffsets[tabIndex];
|
||||
tabRight = _currentTabOffsets[tabIndex + 1];
|
||||
break;
|
||||
}
|
||||
tabLeft = math.min(tabLeft + _resolvedIndicatorPadding.left, tabRight);
|
||||
tabRight = math.max(tabRight - _resolvedIndicatorPadding.right, tabLeft);
|
||||
final double tabTop = tabBarSize.height - indicatorWeight;
|
||||
return new Rect.fromLTWH(tabLeft, tabTop, tabRight - tabLeft, indicatorWeight);
|
||||
}
|
||||
@ -288,46 +332,49 @@ class _IndicatorPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (controller.indexIsChanging) {
|
||||
// The user tapped on a tab, the tab controller's animation is running.
|
||||
final Rect targetRect = indicatorRect(size, controller.index);
|
||||
_currentRect = Rect.lerp(targetRect, _currentRect ?? targetRect, _indexChangeProgress(controller));
|
||||
} else {
|
||||
// The user is dragging the TabBarView's PageView left or right.
|
||||
final int currentIndex = controller.index;
|
||||
final Rect left = currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null;
|
||||
final Rect previous = currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null;
|
||||
final Rect middle = indicatorRect(size, currentIndex);
|
||||
final Rect right = currentIndex < maxTabIndex ? indicatorRect(size, currentIndex + 1) : null;
|
||||
|
||||
final Rect next = currentIndex < maxTabIndex ? indicatorRect(size, currentIndex + 1) : null;
|
||||
final double index = controller.index.toDouble();
|
||||
final double value = controller.animation.value;
|
||||
if (value == index - 1.0)
|
||||
_currentRect = left ?? middle;
|
||||
_currentRect = previous ?? middle;
|
||||
else if (value == index + 1.0)
|
||||
_currentRect = right ?? middle;
|
||||
_currentRect = next ?? middle;
|
||||
else if (value == index)
|
||||
_currentRect = middle;
|
||||
else if (value < index)
|
||||
_currentRect = left == null ? middle : Rect.lerp(middle, left, index - value);
|
||||
_currentRect = previous == null ? middle : Rect.lerp(middle, previous, index - value);
|
||||
else
|
||||
_currentRect = right == null ? middle : Rect.lerp(middle, right, value - index);
|
||||
_currentRect = next == null ? middle : Rect.lerp(middle, next, value - index);
|
||||
}
|
||||
assert(_currentRect != null);
|
||||
canvas.drawRect(_currentRect, new Paint()..color = _color);
|
||||
}
|
||||
|
||||
static bool _tabOffsetsNotEqual(List<double> a, List<double> b) {
|
||||
assert(a != null && b != null && a.length == b.length);
|
||||
static bool _tabOffsetsEqual(List<double> a, List<double> b) {
|
||||
if (a?.length != b?.length)
|
||||
return false;
|
||||
for (int i = 0; i < a.length; i += 1) {
|
||||
if (a[i] != b[i])
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_IndicatorPainter old) {
|
||||
return controller != old.controller ||
|
||||
_tabOffsets?.length != old._tabOffsets?.length ||
|
||||
_tabOffsetsNotEqual(_tabOffsets, old._tabOffsets) ||
|
||||
_currentRect != old._currentRect;
|
||||
return controller != old.controller
|
||||
|| indicatorWeight != old.indicatorWeight
|
||||
|| indicatorPadding != old.indicatorPadding
|
||||
|| (!_tabOffsetsEqual(_currentTabOffsets, old._currentTabOffsets))
|
||||
|| _currentTextDirection != old._currentTextDirection;
|
||||
}
|
||||
}
|
||||
|
||||
@ -488,7 +535,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
/// [indicatorPadding] are ignored.
|
||||
///
|
||||
/// The default value of [indicatorPadding] is [EdgeInsets.zero].
|
||||
final EdgeInsets indicatorPadding;
|
||||
final EdgeInsetsGeometry indicatorPadding;
|
||||
|
||||
/// The color of selected tab labels.
|
||||
///
|
||||
@ -540,10 +587,10 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
|
||||
class _TabBarState extends State<TabBar> {
|
||||
ScrollController _scrollController;
|
||||
|
||||
TabController _controller;
|
||||
_IndicatorPainter _indicatorPainter;
|
||||
int _currentIndex;
|
||||
double _tabStripWidth;
|
||||
|
||||
void _updateTabController() {
|
||||
final TabController newController = widget.controller ?? DefaultTabController.of(context);
|
||||
@ -571,12 +618,11 @@ 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: _controller,
|
||||
indicatorWeight: widget.indicatorWeight,
|
||||
indicatorPadding: widget.indicatorPadding,
|
||||
initialTabOffsets: offsets,
|
||||
old: _indicatorPainter,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -604,16 +650,19 @@ 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;
|
||||
int get maxTabIndex => _indicatorPainter.maxTabIndex;
|
||||
|
||||
double _tabScrollOffset(int index, double viewportWidth, double minExtent, double maxExtent) {
|
||||
if (!widget.isScrollable)
|
||||
return 0.0;
|
||||
final List<double> tabOffsets = _indicatorPainter._tabOffsets;
|
||||
assert(tabOffsets != null && index >= 0 && index <= maxTabIndex);
|
||||
final double tabCenter = (tabOffsets[index] + tabOffsets[index + 1]) / 2.0;
|
||||
double tabCenter = _indicatorPainter.centerOf(index);
|
||||
switch (Directionality.of(context)) {
|
||||
case TextDirection.rtl:
|
||||
tabCenter = _tabStripWidth - tabCenter;
|
||||
break;
|
||||
case TextDirection.ltr:
|
||||
break;
|
||||
}
|
||||
return (tabCenter - viewportWidth / 2.0).clamp(minExtent, maxExtent);
|
||||
}
|
||||
|
||||
@ -632,23 +681,23 @@ class _TabBarState extends State<TabBar> {
|
||||
}
|
||||
|
||||
void _scrollToControllerValue() {
|
||||
final double left = _currentIndex > 0 ? _tabCenteredScrollOffset(_currentIndex - 1) : null;
|
||||
final double middle = _tabCenteredScrollOffset(_currentIndex);
|
||||
final double right = _currentIndex < maxTabIndex ? _tabCenteredScrollOffset(_currentIndex + 1) : null;
|
||||
final double leadingPosition = _currentIndex > 0 ? _tabCenteredScrollOffset(_currentIndex - 1) : null;
|
||||
final double middlePosition = _tabCenteredScrollOffset(_currentIndex);
|
||||
final double trailingPosition = _currentIndex < maxTabIndex ? _tabCenteredScrollOffset(_currentIndex + 1) : null;
|
||||
|
||||
final double index = _controller.index.toDouble();
|
||||
final double value = _controller.animation.value;
|
||||
double offset;
|
||||
if (value == index - 1.0)
|
||||
offset = left ?? middle;
|
||||
offset = leadingPosition ?? middlePosition;
|
||||
else if (value == index + 1.0)
|
||||
offset = right ?? middle;
|
||||
offset = trailingPosition ?? middlePosition;
|
||||
else if (value == index)
|
||||
offset = middle;
|
||||
offset = middlePosition;
|
||||
else if (value < index)
|
||||
offset = left == null ? middle : lerpDouble(middle, left, index - value);
|
||||
offset = leadingPosition == null ? middlePosition : lerpDouble(middlePosition, leadingPosition, index - value);
|
||||
else
|
||||
offset = right == null ? middle : lerpDouble(middle, right, value - index);
|
||||
offset = trailingPosition == null ? middlePosition : lerpDouble(middlePosition, trailingPosition, value - index);
|
||||
|
||||
_scrollController.jumpTo(offset);
|
||||
}
|
||||
@ -663,6 +712,11 @@ class _TabBarState extends State<TabBar> {
|
||||
}
|
||||
|
||||
void _handleTabControllerTick() {
|
||||
if (_controller.index != _currentIndex) {
|
||||
_currentIndex = _controller.index;
|
||||
if (widget.isScrollable)
|
||||
_scrollToCurrentIndex();
|
||||
}
|
||||
setState(() {
|
||||
// Rebuild the tabs after a (potentially animated) index change
|
||||
// has completed.
|
||||
@ -670,8 +724,9 @@ class _TabBarState extends State<TabBar> {
|
||||
}
|
||||
|
||||
// Called each time layout completes.
|
||||
void _saveTabOffsets(List<double> tabOffsets) {
|
||||
_indicatorPainter?._tabOffsets = tabOffsets;
|
||||
void _saveTabOffsets(List<double> tabOffsets, TextDirection textDirection, double width) {
|
||||
_tabStripWidth = width;
|
||||
_indicatorPainter?.saveTabOffsets(tabOffsets, textDirection);
|
||||
}
|
||||
|
||||
void _handleTap(int index) {
|
||||
@ -717,12 +772,6 @@ class _TabBarState extends State<TabBar> {
|
||||
_indicatorPainter._color = Colors.white;
|
||||
}
|
||||
|
||||
if (_controller.index != _currentIndex) {
|
||||
_currentIndex = _controller.index;
|
||||
if (widget.isScrollable)
|
||||
_scrollToCurrentIndex();
|
||||
}
|
||||
|
||||
final int previousIndex = _controller.previousIndex;
|
||||
|
||||
if (_controller.indexIsChanging) {
|
||||
@ -738,13 +787,13 @@ class _TabBarState extends State<TabBar> {
|
||||
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation);
|
||||
if (_currentIndex > 0) {
|
||||
final int tabIndex = _currentIndex - 1;
|
||||
final Animation<double> leftAnimation = new _DragAnimation(_controller, tabIndex);
|
||||
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, leftAnimation);
|
||||
final Animation<double> previousAnimation = new _DragAnimation(_controller, tabIndex);
|
||||
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, previousAnimation);
|
||||
}
|
||||
if (_currentIndex < widget.tabs.length - 1) {
|
||||
final int tabIndex = _currentIndex + 1;
|
||||
final Animation<double> rightAnimation = new _DragAnimation(_controller, tabIndex);
|
||||
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, rightAnimation);
|
||||
final Animation<double> nextAnimation = new _DragAnimation(_controller, tabIndex);
|
||||
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, nextAnimation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -753,7 +802,7 @@ class _TabBarState extends State<TabBar> {
|
||||
// then give all of the tabs equal flexibility so that their widths
|
||||
// reflect the intrinsic width of their labels.
|
||||
final int tabCount = widget.tabs.length;
|
||||
for (int index = 0; index < tabCount; index++) {
|
||||
for (int index = 0; index < tabCount; index += 1) {
|
||||
wrappedTabs[index] = new InkWell(
|
||||
onTap: () { _handleTap(index); },
|
||||
child: new Padding(
|
||||
@ -777,17 +826,17 @@ class _TabBarState extends State<TabBar> {
|
||||
Widget tabBar = new CustomPaint(
|
||||
painter: _indicatorPainter,
|
||||
child: new _TabStyle(
|
||||
animation: kAlwaysDismissedAnimation,
|
||||
selected: false,
|
||||
labelColor: widget.labelColor,
|
||||
unselectedLabelColor: widget.unselectedLabelColor,
|
||||
labelStyle: widget.labelStyle,
|
||||
unselectedLabelStyle: widget.unselectedLabelStyle,
|
||||
child: new _TabLabelBar(
|
||||
onPerformLayout: _saveTabOffsets,
|
||||
children: wrappedTabs,
|
||||
),
|
||||
animation: kAlwaysDismissedAnimation,
|
||||
selected: false,
|
||||
labelColor: widget.labelColor,
|
||||
unselectedLabelColor: widget.unselectedLabelColor,
|
||||
labelStyle: widget.labelStyle,
|
||||
unselectedLabelStyle: widget.unselectedLabelStyle,
|
||||
child: new _TabLabelBar(
|
||||
onPerformLayout: _saveTabOffsets,
|
||||
children: wrappedTabs,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.isScrollable) {
|
||||
@ -1085,8 +1134,8 @@ class TabPageSelector extends StatelessWidget {
|
||||
else
|
||||
background = selectedColorTween.begin;
|
||||
} else {
|
||||
// The selection's offset reflects how far the TabBarView has
|
||||
/// been dragged to the left (-1.0 to 0.0) or the right (0.0 to 1.0).
|
||||
// The selection's offset reflects how far the TabBarView has / been dragged
|
||||
// to the previous page (-1.0 to 0.0) or the next page (0.0 to 1.0).
|
||||
final double offset = tabController.offset;
|
||||
if (tabController.index == tabIndex) {
|
||||
background = selectedColorTween.lerp(1.0 - offset.abs());
|
||||
|
@ -440,8 +440,10 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
|
||||
}
|
||||
|
||||
void _handleDragStart(DragStartDetails details) {
|
||||
// It's possible for _hold to become null between _handleDragDown and
|
||||
// _handleDragStart, for example if some user code calls jumpTo or otherwise
|
||||
// triggers a new activity to begin.
|
||||
assert(_drag == null);
|
||||
assert(_hold != null);
|
||||
_drag = position.drag(details, _disposeDrag);
|
||||
assert(_drag != null);
|
||||
assert(_hold == null);
|
||||
|
@ -14,9 +14,9 @@ import '../rendering/mock_canvas.dart';
|
||||
import '../rendering/recording_canvas.dart';
|
||||
import '../widgets/semantics_tester.dart';
|
||||
|
||||
Widget boilerplate({ Widget child }) {
|
||||
Widget boilerplate({ Widget child, TextDirection textDirection: TextDirection.ltr }) {
|
||||
return new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
textDirection: textDirection,
|
||||
child: new Material(
|
||||
child: child,
|
||||
),
|
||||
@ -918,7 +918,7 @@ void main() {
|
||||
expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB #19')).dx, 800.0);
|
||||
});
|
||||
|
||||
testWidgets('TabBar with indicatorWeight, indicatorPadding', (WidgetTester tester) async {
|
||||
testWidgets('TabBar with indicatorWeight, indicatorPadding (LTR)', (WidgetTester tester) async {
|
||||
const Color color = const Color(0xFF00FF00);
|
||||
const double height = 100.0;
|
||||
const double weight = 8.0;
|
||||
@ -982,6 +982,196 @@ void main() {
|
||||
));
|
||||
});
|
||||
|
||||
testWidgets('TabBar with indicatorWeight, indicatorPadding (RTL)', (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(
|
||||
boilerplate(
|
||||
textDirection: TextDirection.rtl,
|
||||
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)
|
||||
));
|
||||
});
|
||||
|
||||
testWidgets('TabBar with directional indicatorPadding (LTR)', (WidgetTester tester) async {
|
||||
final List<Widget> tabs = <Widget>[
|
||||
new SizedBox(key: new UniqueKey(), width: 130.0, height: 30.0),
|
||||
new SizedBox(key: new UniqueKey(), width: 140.0, height: 40.0),
|
||||
new SizedBox(key: new UniqueKey(), width: 150.0, height: 50.0),
|
||||
];
|
||||
|
||||
final TabController controller = new TabController(
|
||||
vsync: const TestVSync(),
|
||||
length: tabs.length,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
boilerplate(
|
||||
child: new Center(
|
||||
child: new SizedBox(
|
||||
width: 800.0,
|
||||
child: new TabBar(
|
||||
indicatorPadding: const EdgeInsetsDirectional.only(start: 100.0),
|
||||
isScrollable: true,
|
||||
controller: controller,
|
||||
tabs: tabs,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.getRect(find.byKey(tabs[0].key)), new Rect.fromLTRB(0.0, 284.0, 130.0, 314.0));
|
||||
expect(tester.getRect(find.byKey(tabs[1].key)), new Rect.fromLTRB(130.0, 279.0, 270.0, 319.0));
|
||||
expect(tester.getRect(find.byKey(tabs[2].key)), new Rect.fromLTRB(270.0, 274.0, 420.0, 324.0));
|
||||
|
||||
expect(tester.firstRenderObject<RenderBox>(find.byType(TabBar)), paints..rect(
|
||||
style: PaintingStyle.fill,
|
||||
rect: new Rect.fromLTRB(100.0, 50.0, 130.0, 52.0),
|
||||
));
|
||||
});
|
||||
|
||||
testWidgets('TabBar with directional indicatorPadding (RTL)', (WidgetTester tester) async {
|
||||
final List<Widget> tabs = <Widget>[
|
||||
new SizedBox(key: new UniqueKey(), width: 130.0, height: 30.0),
|
||||
new SizedBox(key: new UniqueKey(), width: 140.0, height: 40.0),
|
||||
new SizedBox(key: new UniqueKey(), width: 150.0, height: 50.0),
|
||||
];
|
||||
|
||||
final TabController controller = new TabController(
|
||||
vsync: const TestVSync(),
|
||||
length: tabs.length,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
boilerplate(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: new Center(
|
||||
child: new SizedBox(
|
||||
width: 800.0,
|
||||
child: new TabBar(
|
||||
indicatorPadding: const EdgeInsetsDirectional.only(start: 100.0),
|
||||
isScrollable: true,
|
||||
controller: controller,
|
||||
tabs: tabs,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.getRect(find.byKey(tabs[0].key)), new Rect.fromLTRB(670.0, 284.0, 800.0, 314.0));
|
||||
expect(tester.getRect(find.byKey(tabs[1].key)), new Rect.fromLTRB(530.0, 279.0, 670.0, 319.0));
|
||||
expect(tester.getRect(find.byKey(tabs[2].key)), new Rect.fromLTRB(380.0, 274.0, 530.0, 324.0));
|
||||
|
||||
final RenderBox tabBar = tester.renderObject<RenderBox>(find.byType(CustomPaint).at(1));
|
||||
|
||||
expect(tabBar.size, const Size(420.0, 52.0));
|
||||
expect(tabBar, paints..rect(
|
||||
style: PaintingStyle.fill,
|
||||
rect: new Rect.fromLTRB(tabBar.size.width - 130.0, 50.0, tabBar.size.width - 100.0, 52.0),
|
||||
));
|
||||
});
|
||||
|
||||
testWidgets('Overflowing RTL tab bar', (WidgetTester tester) async {
|
||||
final List<Widget> tabs = new List<Widget>.filled(100,
|
||||
new SizedBox(key: new UniqueKey(), width: 30.0, height: 20.0),
|
||||
);
|
||||
|
||||
final TabController controller = new TabController(
|
||||
vsync: const TestVSync(),
|
||||
length: tabs.length,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
boilerplate(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: new Center(
|
||||
child: new TabBar(
|
||||
isScrollable: true,
|
||||
controller: controller,
|
||||
tabs: tabs,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.firstRenderObject<RenderBox>(find.byType(TabBar)), paints..rect(
|
||||
style: PaintingStyle.fill,
|
||||
rect: new Rect.fromLTRB(2970.0, 20.0, 3000.0, 22.0),
|
||||
));
|
||||
|
||||
controller.animateTo(tabs.length - 1, duration: const Duration(seconds: 1), curve: Curves.linear);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(tester.firstRenderObject<RenderBox>(find.byType(TabBar)), paints..rect(
|
||||
style: PaintingStyle.fill,
|
||||
rect: new Rect.fromLTRB(742.5, 20.0, 772.5, 22.0), // (these values were derived empirically, not analytically)
|
||||
));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 501));
|
||||
|
||||
expect(tester.firstRenderObject<RenderBox>(find.byType(TabBar)), paints..rect(
|
||||
style: PaintingStyle.fill,
|
||||
rect: new Rect.fromLTRB(0.0, 20.0, 30.0, 22.0),
|
||||
));
|
||||
});
|
||||
|
||||
testWidgets('correct semantics', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user