Skip to content

Commit 7cadbea

Browse files
authored
Fix CarouselView crashes when initlal viewportDimension is 0.0 (flutter#167271)
## Description This PR fixes CarouselView crashes due to viewportDimension being 0.0. At startup, a warm-up frame can be produced before the Flutter engine has reported the initial view metrics. As a result, the first frame can be produced with a size of zero. In the context of CarouselView this leads to some problems mainly related to division by zero. ## Related Issue Fixes flutter#163436 Fixes flutter#160679 ## Tests Adds 5 tests.
1 parent 810024f commit 7cadbea

File tree

2 files changed

+132
-2
lines changed

2 files changed

+132
-2
lines changed

packages/flutter/lib/src/material/carousel.dart

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ class _CarouselViewState extends State<CarouselView> {
503503
Axis.vertical => constraints.maxHeight,
504504
};
505505
_itemExtent =
506-
_itemExtent == null ? _itemExtent : clampDouble(_itemExtent!, 0, mainAxisExtent);
506+
widget.itemExtent == null ? null : clampDouble(widget.itemExtent!, 0, mainAxisExtent);
507507

508508
return Scrollable(
509509
axisDirection: axisDirection,
@@ -1405,7 +1405,7 @@ class _CarouselPosition extends ScrollPositionWithSingleContext implements _Caro
14051405
if (_itemExtent == value) {
14061406
return;
14071407
}
1408-
if (hasPixels && _itemExtent != null) {
1408+
if (hasPixels && _itemExtent != null && viewportDimension != 0.0) {
14091409
final double leadingItem = getItemFromPixels(pixels, viewportDimension);
14101410
final double newPixel = getPixelsFromItem(leadingItem, flexWeights, value);
14111411
forcePixels(newPixel);
@@ -1472,6 +1472,9 @@ class _CarouselPosition extends ScrollPositionWithSingleContext implements _Caro
14721472

14731473
double getPixelsFromItem(double item, List<int>? flexWeights, double? itemExtent) {
14741474
double fraction;
1475+
if (viewportDimension == 0.0) {
1476+
return 0.0;
1477+
}
14751478
if (itemExtent != null) {
14761479
fraction = itemExtent / viewportDimension;
14771480
} else {
@@ -1512,6 +1515,18 @@ class _CarouselPosition extends ScrollPositionWithSingleContext implements _Caro
15121515
return result;
15131516
}
15141517

1518+
@override
1519+
void absorb(ScrollPosition other) {
1520+
super.absorb(other);
1521+
1522+
if (other is! _CarouselPosition) {
1523+
return;
1524+
}
1525+
1526+
_cachedItem = other._cachedItem;
1527+
_itemExtent = other._itemExtent;
1528+
}
1529+
15151530
@override
15161531
_CarouselMetrics copyWith({
15171532
double? minScrollExtent,

packages/flutter/test/material/carousel_test.dart

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,6 +1474,121 @@ void main() {
14741474
expect(tester.takeException(), isNull);
14751475
});
14761476

1477+
// Regression test for https://github.com/flutter/flutter/issues/160679.
1478+
testWidgets('Does not crash when parent size is zero', (WidgetTester tester) async {
1479+
await tester.pumpWidget(
1480+
const MaterialApp(
1481+
home: Scaffold(
1482+
body: SizedBox(
1483+
width: 0,
1484+
child: CarouselView(itemExtent: 40.0, children: <Widget>[FlutterLogo()]),
1485+
),
1486+
),
1487+
),
1488+
);
1489+
1490+
expect(tester.takeException(), isNull);
1491+
});
1492+
1493+
testWidgets('itemExtent can be set to double.infinity', (WidgetTester tester) async {
1494+
await tester.pumpWidget(
1495+
const MaterialApp(
1496+
home: Scaffold(
1497+
body: CarouselView(itemExtent: double.infinity, children: <Widget>[FlutterLogo()]),
1498+
),
1499+
),
1500+
);
1501+
1502+
// Item extent is clamped to screen size.
1503+
final Size logoSize = tester.getSize(find.byType(FlutterLogo));
1504+
const double itemHorizontalPadding = 8.0; // Default padding.
1505+
expect(logoSize.width, 800.0 - itemHorizontalPadding);
1506+
});
1507+
1508+
// Regression test for https://github.com/flutter/flutter/issues/163436.
1509+
testWidgets('Does not crash when initial viewport dimension is zero and itemExtent is fixed', (
1510+
WidgetTester tester,
1511+
) async {
1512+
await tester.binding.setSurfaceSize(Size.zero);
1513+
addTearDown(() => tester.binding.setSurfaceSize(null));
1514+
1515+
const double fixedItemExtent = 60.0;
1516+
await tester.pumpWidget(
1517+
const MaterialApp(
1518+
home: Scaffold(
1519+
body: CarouselView(itemExtent: fixedItemExtent, children: <Widget>[FlutterLogo()]),
1520+
),
1521+
),
1522+
);
1523+
1524+
expect(tester.takeException(), isNull);
1525+
});
1526+
1527+
// Regression test for https://github.com/flutter/flutter/issues/163436.
1528+
testWidgets('Does not crash when initial viewport dimension is zero and itemExtent is infinite', (
1529+
WidgetTester tester,
1530+
) async {
1531+
await tester.binding.setSurfaceSize(Size.zero);
1532+
addTearDown(() => tester.binding.setSurfaceSize(null));
1533+
1534+
await tester.pumpWidget(
1535+
const MaterialApp(
1536+
home: Scaffold(
1537+
body: CarouselView(itemExtent: double.infinity, children: <Widget>[FlutterLogo()]),
1538+
),
1539+
),
1540+
);
1541+
1542+
expect(tester.takeException(), isNull);
1543+
});
1544+
1545+
// Regression test for https://github.com/flutter/flutter/issues/163436.
1546+
testWidgets('itemExtent is applied when viewport dimension is updated', (
1547+
WidgetTester tester,
1548+
) async {
1549+
addTearDown(() => tester.binding.setSurfaceSize(null));
1550+
1551+
const double itemExtent = 60.0;
1552+
bool showScrollbars = false;
1553+
1554+
Future<void> updateSurfaceSizeAndPump(Size size) async {
1555+
await tester.binding.setSurfaceSize(size);
1556+
1557+
// At startup, a warm-up frame can be produced before the Flutter engine has reported the
1558+
// initial view metrics. As a result, the first frame can be produced with a size of zero.
1559+
// This leads to several instances of _CarouselPosition being created and
1560+
// _CarouselPosition.absorb to be called.
1561+
// To correctly simulate this behavior in the test environment, one solution is to
1562+
// update the ScrollConfiguration. For instance by changing the ScrollBehavior.scrollbars
1563+
// value on each build.
1564+
showScrollbars = !showScrollbars;
1565+
1566+
await tester.pumpWidget(
1567+
MaterialApp(
1568+
home: Scaffold(
1569+
body: Center(
1570+
child: ScrollConfiguration(
1571+
behavior: const ScrollBehavior().copyWith(scrollbars: showScrollbars),
1572+
child: const CarouselView(
1573+
itemExtent: itemExtent,
1574+
children: <Widget>[FlutterLogo()],
1575+
),
1576+
),
1577+
),
1578+
),
1579+
),
1580+
);
1581+
}
1582+
1583+
// Simulate an initial zero viewport dimension.
1584+
await updateSurfaceSizeAndPump(Size.zero);
1585+
await updateSurfaceSizeAndPump(const Size(500, 400));
1586+
1587+
final Size logoSize = tester.getSize(find.byType(FlutterLogo));
1588+
const double itemHorizontalPadding = 8.0; // Default padding.
1589+
expect(logoSize.width, itemExtent - itemHorizontalPadding);
1590+
});
1591+
14771592
group('CarouselController.animateToItem', () {
14781593
testWidgets('CarouselView.weighted horizontal, not reversed, flexWeights [7,1]', (
14791594
WidgetTester tester,

0 commit comments

Comments
 (0)