Fix null issue with dynamically updating from zero tabs for TabBar (#69005)
This commit is contained in:
parent
60a58ab780
commit
d50bfd5f66
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import 'dart:ui' show lerpDouble;
|
import 'dart:ui' show lerpDouble;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||||
@ -331,8 +332,12 @@ class _IndicatorPainter extends CustomPainter {
|
|||||||
final TabBarIndicatorSize? indicatorSize;
|
final TabBarIndicatorSize? indicatorSize;
|
||||||
final List<GlobalKey> tabKeys;
|
final List<GlobalKey> tabKeys;
|
||||||
|
|
||||||
late List<double> _currentTabOffsets;
|
// _currentTabOffsets and _currentTextDirection are set each time TabBar
|
||||||
late TextDirection _currentTextDirection;
|
// layout is completed. These values can be null when TabBar contains no
|
||||||
|
// tabs, since there are nothing to lay out.
|
||||||
|
List<double>? _currentTabOffsets;
|
||||||
|
TextDirection? _currentTextDirection;
|
||||||
|
|
||||||
Rect? _currentRect;
|
Rect? _currentRect;
|
||||||
BoxPainter? _painter;
|
BoxPainter? _painter;
|
||||||
bool _needsPaint = false;
|
bool _needsPaint = false;
|
||||||
@ -344,38 +349,38 @@ class _IndicatorPainter extends CustomPainter {
|
|||||||
_painter?.dispose();
|
_painter?.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveTabOffsets(List<double> tabOffsets, TextDirection textDirection) {
|
void saveTabOffsets(List<double>? tabOffsets, TextDirection? textDirection) {
|
||||||
_currentTabOffsets = tabOffsets;
|
_currentTabOffsets = tabOffsets;
|
||||||
_currentTextDirection = textDirection;
|
_currentTextDirection = textDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
// _currentTabOffsets[index] is the offset of the start edge of the tab at index, and
|
// _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.
|
// _currentTabOffsets[_currentTabOffsets.length] is the end edge of the last tab.
|
||||||
int get maxTabIndex => _currentTabOffsets.length - 2;
|
int get maxTabIndex => _currentTabOffsets!.length - 2;
|
||||||
|
|
||||||
double centerOf(int tabIndex) {
|
double centerOf(int tabIndex) {
|
||||||
assert(_currentTabOffsets != null);
|
assert(_currentTabOffsets != null);
|
||||||
assert(_currentTabOffsets.isNotEmpty);
|
assert(_currentTabOffsets!.isNotEmpty);
|
||||||
assert(tabIndex >= 0);
|
assert(tabIndex >= 0);
|
||||||
assert(tabIndex <= maxTabIndex);
|
assert(tabIndex <= maxTabIndex);
|
||||||
return (_currentTabOffsets[tabIndex] + _currentTabOffsets[tabIndex + 1]) / 2.0;
|
return (_currentTabOffsets![tabIndex] + _currentTabOffsets![tabIndex + 1]) / 2.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Rect indicatorRect(Size tabBarSize, int tabIndex) {
|
Rect indicatorRect(Size tabBarSize, int tabIndex) {
|
||||||
assert(_currentTabOffsets != null);
|
assert(_currentTabOffsets != null);
|
||||||
assert(_currentTextDirection != null);
|
assert(_currentTextDirection != null);
|
||||||
assert(_currentTabOffsets.isNotEmpty);
|
assert(_currentTabOffsets!.isNotEmpty);
|
||||||
assert(tabIndex >= 0);
|
assert(tabIndex >= 0);
|
||||||
assert(tabIndex <= maxTabIndex);
|
assert(tabIndex <= maxTabIndex);
|
||||||
double tabLeft, tabRight;
|
double tabLeft, tabRight;
|
||||||
switch (_currentTextDirection) {
|
switch (_currentTextDirection!) {
|
||||||
case TextDirection.rtl:
|
case TextDirection.rtl:
|
||||||
tabLeft = _currentTabOffsets[tabIndex + 1];
|
tabLeft = _currentTabOffsets![tabIndex + 1];
|
||||||
tabRight = _currentTabOffsets[tabIndex];
|
tabRight = _currentTabOffsets![tabIndex];
|
||||||
break;
|
break;
|
||||||
case TextDirection.ltr:
|
case TextDirection.ltr:
|
||||||
tabLeft = _currentTabOffsets[tabIndex];
|
tabLeft = _currentTabOffsets![tabIndex];
|
||||||
tabRight = _currentTabOffsets[tabIndex + 1];
|
tabRight = _currentTabOffsets![tabIndex + 1];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -426,25 +431,13 @@ class _IndicatorPainter extends CustomPainter {
|
|||||||
_painter!.paint(canvas, _currentRect!.topLeft, configuration);
|
_painter!.paint(canvas, _currentRect!.topLeft, configuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool _tabOffsetsEqual(List<double> a, List<double> b) {
|
|
||||||
// TODO(shihaohong): The following null check should be replaced when a fix
|
|
||||||
// for https://github.com/flutter/flutter/issues/40014 is available.
|
|
||||||
if (a == null || b == null || a.length != b.length)
|
|
||||||
return false;
|
|
||||||
for (int i = 0; i < a.length; i += 1) {
|
|
||||||
if (a[i] != b[i])
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldRepaint(_IndicatorPainter old) {
|
bool shouldRepaint(_IndicatorPainter old) {
|
||||||
return _needsPaint
|
return _needsPaint
|
||||||
|| controller != old.controller
|
|| controller != old.controller
|
||||||
|| indicator != old.indicator
|
|| indicator != old.indicator
|
||||||
|| tabKeys.length != old.tabKeys.length
|
|| tabKeys.length != old.tabKeys.length
|
||||||
|| (!_tabOffsetsEqual(_currentTabOffsets, old._currentTabOffsets))
|
|| (!listEquals(_currentTabOffsets, old._currentTabOffsets))
|
||||||
|| _currentTextDirection != old._currentTextDirection;
|
|| _currentTextDirection != old._currentTextDirection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2571,6 +2571,81 @@ void main() {
|
|||||||
expect(find.text('No tabs'), findsOneWidget);
|
expect(find.text('No tabs'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('TabBar - updating to and from zero tabs', (WidgetTester tester) async {
|
||||||
|
// Regression test for https://github.com/flutter/flutter/issues/68962.
|
||||||
|
final List<String> tabTitles = <String>[];
|
||||||
|
final List<Widget> tabContents = <Widget>[];
|
||||||
|
TabController _tabController = TabController(length: tabContents.length, vsync: const TestVSync());
|
||||||
|
|
||||||
|
void _onTabAdd(StateSetter setState) {
|
||||||
|
setState(() {
|
||||||
|
tabTitles.add('Tab ${tabTitles.length + 1}');
|
||||||
|
tabContents.add(
|
||||||
|
Container(
|
||||||
|
color: Colors.red,
|
||||||
|
height: 200,
|
||||||
|
width: 200,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_tabController = TabController(length: tabContents.length, vsync: const TestVSync());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTabRemove(StateSetter setState) {
|
||||||
|
setState(() {
|
||||||
|
tabTitles.removeLast();
|
||||||
|
tabContents.removeLast();
|
||||||
|
_tabController = TabController(length: tabContents.length, vsync: const TestVSync());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter setState) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
actions: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
key: const Key('Add tab'),
|
||||||
|
child: const Text('Add tab'),
|
||||||
|
onPressed: () => _onTabAdd(setState),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
key: const Key('Remove tab'),
|
||||||
|
child: const Text('Remove tab'),
|
||||||
|
onPressed: () => _onTabRemove(setState),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(40.0),
|
||||||
|
child: Expanded(
|
||||||
|
child: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
tabs: tabTitles
|
||||||
|
.map((String title) => Tab(text: title))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Tab 1'), findsNothing);
|
||||||
|
expect(find.text('Add tab'), findsOneWidget);
|
||||||
|
await tester.tap(find.byKey(const Key('Add tab')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Tab 1'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('Remove tab')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Tab 1'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('TabBar expands vertically to accommodate the Icon and child Text() pair the same amount it would expand for Icon and text pair.', (WidgetTester tester) async {
|
testWidgets('TabBar expands vertically to accommodate the Icon and child Text() pair the same amount it would expand for Icon and text pair.', (WidgetTester tester) async {
|
||||||
const double indicatorWeight = 2.0;
|
const double indicatorWeight = 2.0;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user