Unverified Commit d50bfd5f authored by Shi-Hao Hong's avatar Shi-Hao Hong Committed by GitHub

Fix null issue with dynamically updating from zero tabs for TabBar (#69005)

parent 60a58ab7
......@@ -4,6 +4,7 @@
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
......@@ -331,8 +332,12 @@ class _IndicatorPainter extends CustomPainter {
final TabBarIndicatorSize? indicatorSize;
final List<GlobalKey> tabKeys;
late List<double> _currentTabOffsets;
late TextDirection _currentTextDirection;
// _currentTabOffsets and _currentTextDirection are set each time TabBar
// 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;
BoxPainter? _painter;
bool _needsPaint = false;
......@@ -344,38 +349,38 @@ class _IndicatorPainter extends CustomPainter {
_painter?.dispose();
}
void saveTabOffsets(List<double> tabOffsets, TextDirection textDirection) {
void saveTabOffsets(List<double>? tabOffsets, TextDirection? textDirection) {
_currentTabOffsets = tabOffsets;
_currentTextDirection = textDirection;
}
// _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;
int get maxTabIndex => _currentTabOffsets!.length - 2;
double centerOf(int tabIndex) {
assert(_currentTabOffsets != null);
assert(_currentTabOffsets.isNotEmpty);
assert(_currentTabOffsets!.isNotEmpty);
assert(tabIndex >= 0);
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) {
assert(_currentTabOffsets != null);
assert(_currentTextDirection != null);
assert(_currentTabOffsets.isNotEmpty);
assert(_currentTabOffsets!.isNotEmpty);
assert(tabIndex >= 0);
assert(tabIndex <= maxTabIndex);
double tabLeft, tabRight;
switch (_currentTextDirection) {
switch (_currentTextDirection!) {
case TextDirection.rtl:
tabLeft = _currentTabOffsets[tabIndex + 1];
tabRight = _currentTabOffsets[tabIndex];
tabLeft = _currentTabOffsets![tabIndex + 1];
tabRight = _currentTabOffsets![tabIndex];
break;
case TextDirection.ltr:
tabLeft = _currentTabOffsets[tabIndex];
tabRight = _currentTabOffsets[tabIndex + 1];
tabLeft = _currentTabOffsets![tabIndex];
tabRight = _currentTabOffsets![tabIndex + 1];
break;
}
......@@ -426,25 +431,13 @@ class _IndicatorPainter extends CustomPainter {
_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
bool shouldRepaint(_IndicatorPainter old) {
return _needsPaint
|| controller != old.controller
|| indicator != old.indicator
|| tabKeys.length != old.tabKeys.length
|| (!_tabOffsetsEqual(_currentTabOffsets, old._currentTabOffsets))
|| (!listEquals(_currentTabOffsets, old._currentTabOffsets))
|| _currentTextDirection != old._currentTextDirection;
}
}
......
......@@ -2571,6 +2571,81 @@ void main() {
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 {
const double indicatorWeight = 2.0;
......
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