mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
This method controls whether the builder needs to be called again again even if the layout constraints are the same. By default, the builder will always be called when the widget is updated because the logic in the callback might have changed. However, there are cases where subclasses of ConstrainedLayoutBuilder know that certain property updates only affect paint and not build. In these cases, we lack a way of expressing that the builder callback is not needed -- and we end up doing superfluous work. This PR gives subclasses the ability to know exactly when the callback needs to be called and when it can be skipped.
874 lines
28 KiB
Dart
874 lines
28 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
|
|
|
void main() {
|
|
testWidgetsWithLeakTracking('LayoutBuilder parent size', (WidgetTester tester) async {
|
|
late Size layoutBuilderSize;
|
|
final Key childKey = UniqueKey();
|
|
final Key parentKey = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 100.0, maxHeight: 200.0),
|
|
child: LayoutBuilder(
|
|
key: parentKey,
|
|
builder: (BuildContext context, BoxConstraints constraints) {
|
|
layoutBuilderSize = constraints.biggest;
|
|
return SizedBox(
|
|
key: childKey,
|
|
width: layoutBuilderSize.width / 2.0,
|
|
height: layoutBuilderSize.height / 2.0,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(layoutBuilderSize, const Size(100.0, 200.0));
|
|
final RenderBox parentBox = tester.renderObject(find.byKey(parentKey));
|
|
expect(parentBox.size, equals(const Size(50.0, 100.0)));
|
|
final RenderBox childBox = tester.renderObject(find.byKey(childKey));
|
|
expect(childBox.size, equals(const Size(50.0, 100.0)));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('SliverLayoutBuilder parent geometry', (WidgetTester tester) async {
|
|
late SliverConstraints parentConstraints1;
|
|
late SliverConstraints parentConstraints2;
|
|
final Key childKey1 = UniqueKey();
|
|
final Key parentKey1 = UniqueKey();
|
|
final Key childKey2 = UniqueKey();
|
|
final Key parentKey2 = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CustomScrollView(
|
|
slivers: <Widget>[
|
|
SliverLayoutBuilder(
|
|
key: parentKey1,
|
|
builder: (BuildContext context, SliverConstraints constraint) {
|
|
parentConstraints1 = constraint;
|
|
return SliverPadding(key: childKey1, padding: const EdgeInsets.fromLTRB(1, 2, 3, 4));
|
|
},
|
|
),
|
|
SliverLayoutBuilder(
|
|
key: parentKey2,
|
|
builder: (BuildContext context, SliverConstraints constraint) {
|
|
parentConstraints2 = constraint;
|
|
return SliverPadding(key: childKey2, padding: const EdgeInsets.fromLTRB(5, 7, 11, 13));
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(parentConstraints1.crossAxisExtent, 800);
|
|
expect(parentConstraints1.remainingPaintExtent, 600);
|
|
|
|
expect(parentConstraints2.crossAxisExtent, 800);
|
|
expect(parentConstraints2.remainingPaintExtent, 600 - 2 - 4);
|
|
final RenderSliver parentSliver1 = tester.renderObject(find.byKey(parentKey1));
|
|
final RenderSliver parentSliver2 = tester.renderObject(find.byKey(parentKey2));
|
|
|
|
// scrollExtent == top + bottom.
|
|
expect(parentSliver1.geometry!.scrollExtent, 2 + 4);
|
|
expect(parentSliver2.geometry!.scrollExtent, 7 + 13);
|
|
|
|
final RenderSliver childSliver1 = tester.renderObject(find.byKey(childKey1));
|
|
final RenderSliver childSliver2 = tester.renderObject(find.byKey(childKey2));
|
|
expect(childSliver1.geometry, parentSliver1.geometry);
|
|
expect(childSliver2.geometry, parentSliver2.geometry);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('LayoutBuilder stateful child', (WidgetTester tester) async {
|
|
late Size layoutBuilderSize;
|
|
late StateSetter setState;
|
|
final Key childKey = UniqueKey();
|
|
final Key parentKey = UniqueKey();
|
|
double childWidth = 10.0;
|
|
double childHeight = 20.0;
|
|
|
|
await tester.pumpWidget(
|
|
Center(
|
|
child: LayoutBuilder(
|
|
key: parentKey,
|
|
builder: (BuildContext context, BoxConstraints constraints) {
|
|
layoutBuilderSize = constraints.biggest;
|
|
return StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setter) {
|
|
setState = setter;
|
|
return SizedBox(
|
|
key: childKey,
|
|
width: childWidth,
|
|
height: childHeight,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(layoutBuilderSize, equals(const Size(800.0, 600.0)));
|
|
RenderBox parentBox = tester.renderObject(find.byKey(parentKey));
|
|
expect(parentBox.size, equals(const Size(10.0, 20.0)));
|
|
RenderBox childBox = tester.renderObject(find.byKey(childKey));
|
|
expect(childBox.size, equals(const Size(10.0, 20.0)));
|
|
|
|
setState(() {
|
|
childWidth = 100.0;
|
|
childHeight = 200.0;
|
|
});
|
|
await tester.pump();
|
|
parentBox = tester.renderObject(find.byKey(parentKey));
|
|
expect(parentBox.size, equals(const Size(100.0, 200.0)));
|
|
childBox = tester.renderObject(find.byKey(childKey));
|
|
expect(childBox.size, equals(const Size(100.0, 200.0)));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('SliverLayoutBuilder stateful descendants', (WidgetTester tester) async {
|
|
late StateSetter setState;
|
|
double childWidth = 10.0;
|
|
double childHeight = 20.0;
|
|
final Key parentKey = UniqueKey();
|
|
final Key childKey = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CustomScrollView(
|
|
slivers: <Widget>[
|
|
SliverLayoutBuilder(
|
|
key: parentKey,
|
|
builder: (BuildContext context, SliverConstraints constraint) {
|
|
return SliverToBoxAdapter(
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setter) {
|
|
setState = setter;
|
|
return SizedBox(
|
|
key: childKey,
|
|
width: childWidth,
|
|
height: childHeight,
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
RenderBox childBox = tester.renderObject(find.byKey(childKey));
|
|
RenderSliver parentSliver = tester.renderObject(find.byKey(parentKey));
|
|
expect(childBox.size.width, 800);
|
|
expect(childBox.size.height, childHeight);
|
|
expect(parentSliver.geometry!.scrollExtent, childHeight);
|
|
expect(parentSliver.geometry!.paintExtent, childHeight);
|
|
|
|
setState(() {
|
|
childWidth = 100.0;
|
|
childHeight = 200.0;
|
|
});
|
|
|
|
await tester.pump();
|
|
childBox = tester.renderObject(find.byKey(childKey));
|
|
parentSliver = tester.renderObject(find.byKey(parentKey));
|
|
expect(childBox.size.width, 800);
|
|
expect(childBox.size.height, childHeight);
|
|
expect(parentSliver.geometry!.scrollExtent, childHeight);
|
|
expect(parentSliver.geometry!.paintExtent, childHeight);
|
|
|
|
// Make child wider and higher than the viewport.
|
|
setState(() {
|
|
childWidth = 900.0;
|
|
childHeight = 900.0;
|
|
});
|
|
|
|
await tester.pump();
|
|
childBox = tester.renderObject(find.byKey(childKey));
|
|
parentSliver = tester.renderObject(find.byKey(parentKey));
|
|
expect(childBox.size.width, 800);
|
|
expect(childBox.size.height, childHeight);
|
|
expect(parentSliver.geometry!.scrollExtent, childHeight);
|
|
expect(parentSliver.geometry!.paintExtent, 600);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('LayoutBuilder stateful parent', (WidgetTester tester) async {
|
|
late Size layoutBuilderSize;
|
|
late StateSetter setState;
|
|
final Key childKey = UniqueKey();
|
|
double childWidth = 10.0;
|
|
double childHeight = 20.0;
|
|
|
|
await tester.pumpWidget(
|
|
Center(
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setter) {
|
|
setState = setter;
|
|
return SizedBox(
|
|
width: childWidth,
|
|
height: childHeight,
|
|
child: LayoutBuilder(
|
|
builder: (BuildContext context, BoxConstraints constraints) {
|
|
layoutBuilderSize = constraints.biggest;
|
|
return SizedBox(
|
|
key: childKey,
|
|
width: layoutBuilderSize.width,
|
|
height: layoutBuilderSize.height,
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(layoutBuilderSize, equals(const Size(10.0, 20.0)));
|
|
RenderBox box = tester.renderObject(find.byKey(childKey));
|
|
expect(box.size, equals(const Size(10.0, 20.0)));
|
|
|
|
setState(() {
|
|
childWidth = 100.0;
|
|
childHeight = 200.0;
|
|
});
|
|
await tester.pump();
|
|
box = tester.renderObject(find.byKey(childKey));
|
|
expect(box.size, equals(const Size(100.0, 200.0)));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('LayoutBuilder and Inherited -- do not rebuild when not using inherited', (WidgetTester tester) async {
|
|
int built = 0;
|
|
final Widget target = LayoutBuilder(
|
|
builder: (BuildContext context, BoxConstraints constraints) {
|
|
built += 1;
|
|
return Container();
|
|
},
|
|
);
|
|
expect(built, 0);
|
|
|
|
await tester.pumpWidget(MediaQuery(
|
|
data: const MediaQueryData(size: Size(400.0, 300.0)),
|
|
child: target,
|
|
));
|
|
expect(built, 1);
|
|
|
|
await tester.pumpWidget(MediaQuery(
|
|
data: const MediaQueryData(size: Size(300.0, 400.0)),
|
|
child: target,
|
|
));
|
|
expect(built, 1);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('LayoutBuilder and Inherited -- do rebuild when using inherited', (WidgetTester tester) async {
|
|
int built = 0;
|
|
final Widget target = LayoutBuilder(
|
|
builder: (BuildContext context, BoxConstraints constraints) {
|
|
built += 1;
|
|
MediaQuery.of(context);
|
|
return Container();
|
|
},
|
|
);
|
|
expect(built, 0);
|
|
|
|
await tester.pumpWidget(MediaQuery(
|
|
data: const MediaQueryData(size: Size(400.0, 300.0)),
|
|
child: target,
|
|
));
|
|
expect(built, 1);
|
|
|
|
await tester.pumpWidget(MediaQuery(
|
|
data: const MediaQueryData(size: Size(300.0, 400.0)),
|
|
child: target,
|
|
));
|
|
expect(built, 2);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('SliverLayoutBuilder and Inherited -- do not rebuild when not using inherited', (WidgetTester tester) async {
|
|
int built = 0;
|
|
final Widget target = Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CustomScrollView(
|
|
slivers: <Widget>[
|
|
SliverLayoutBuilder(
|
|
builder: (BuildContext context, SliverConstraints constraint) {
|
|
built++;
|
|
return SliverToBoxAdapter(child: Container());
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
expect(built, 0);
|
|
|
|
await tester.pumpWidget(MediaQuery(
|
|
data: const MediaQueryData(size: Size(400.0, 300.0)),
|
|
child: target,
|
|
));
|
|
expect(built, 1);
|
|
|
|
await tester.pumpWidget(MediaQuery(
|
|
data: const MediaQueryData(size: Size(300.0, 400.0)),
|
|
child: target,
|
|
));
|
|
expect(built, 1);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking(
|
|
'SliverLayoutBuilder and Inherited -- do rebuild when not using inherited',
|
|
(WidgetTester tester) async {
|
|
int built = 0;
|
|
final Widget target = Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CustomScrollView(
|
|
slivers: <Widget>[
|
|
SliverLayoutBuilder(
|
|
builder: (BuildContext context, SliverConstraints constraint) {
|
|
built++;
|
|
MediaQuery.of(context);
|
|
return SliverToBoxAdapter(child: Container());
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
expect(built, 0);
|
|
|
|
await tester.pumpWidget(MediaQuery(
|
|
data: const MediaQueryData(size: Size(400.0, 300.0)),
|
|
child: target,
|
|
));
|
|
expect(built, 1);
|
|
|
|
await tester.pumpWidget(MediaQuery(
|
|
data: const MediaQueryData(size: Size(300.0, 400.0)),
|
|
child: target,
|
|
));
|
|
expect(built, 2);
|
|
},
|
|
);
|
|
|
|
testWidgetsWithLeakTracking('nested SliverLayoutBuilder', (WidgetTester tester) async {
|
|
late SliverConstraints parentConstraints1;
|
|
late SliverConstraints parentConstraints2;
|
|
final Key childKey = UniqueKey();
|
|
final Key parentKey1 = UniqueKey();
|
|
final Key parentKey2 = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CustomScrollView(
|
|
slivers: <Widget>[
|
|
SliverLayoutBuilder(
|
|
key: parentKey1,
|
|
builder: (BuildContext context, SliverConstraints constraint) {
|
|
parentConstraints1 = constraint;
|
|
return SliverLayoutBuilder(
|
|
key: parentKey2,
|
|
builder: (BuildContext context, SliverConstraints constraint) {
|
|
parentConstraints2 = constraint;
|
|
return SliverPadding(key: childKey, padding: const EdgeInsets.fromLTRB(1, 2, 3, 4));
|
|
},
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(parentConstraints1, parentConstraints2);
|
|
|
|
expect(parentConstraints1.crossAxisExtent, 800);
|
|
expect(parentConstraints1.remainingPaintExtent, 600);
|
|
|
|
final RenderSliver parentSliver1 = tester.renderObject(find.byKey(parentKey1));
|
|
final RenderSliver parentSliver2 = tester.renderObject(find.byKey(parentKey2));
|
|
// scrollExtent == top + bottom.
|
|
expect(parentSliver1.geometry!.scrollExtent, 2 + 4);
|
|
|
|
final RenderSliver childSliver = tester.renderObject(find.byKey(childKey));
|
|
expect(childSliver.geometry, parentSliver1.geometry);
|
|
expect(parentSliver1.geometry, parentSliver2.geometry);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('localToGlobal works with SliverLayoutBuilder', (WidgetTester tester) async {
|
|
final Key childKey1 = UniqueKey();
|
|
final Key childKey2 = UniqueKey();
|
|
final ScrollController scrollController = ScrollController();
|
|
addTearDown(scrollController.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CustomScrollView(
|
|
controller: scrollController,
|
|
slivers: <Widget>[
|
|
const SliverToBoxAdapter(
|
|
child: SizedBox(height: 300),
|
|
),
|
|
SliverLayoutBuilder(
|
|
builder: (BuildContext context, SliverConstraints constraint) => SliverToBoxAdapter(
|
|
child: SizedBox(key: childKey1, height: 200),
|
|
),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: SizedBox(key: childKey2, height: 100),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderBox renderChild1 = tester.renderObject(find.byKey(childKey1));
|
|
final RenderBox renderChild2 = tester.renderObject(find.byKey(childKey2));
|
|
|
|
// Test with scrollController.scrollOffset = 0.
|
|
expect(
|
|
renderChild1.localToGlobal(const Offset(100, 100)),
|
|
const Offset(100, 300.0 + 100),
|
|
);
|
|
|
|
expect(
|
|
renderChild2.localToGlobal(const Offset(100, 100)),
|
|
const Offset(100, 300.0 + 200 + 100),
|
|
);
|
|
|
|
scrollController.jumpTo(100);
|
|
await tester.pump();
|
|
expect(
|
|
renderChild1.localToGlobal(const Offset(100, 100)),
|
|
// -100 because the scroll offset is now 100.
|
|
const Offset(100, 300.0 + 100 - 100),
|
|
);
|
|
|
|
expect(
|
|
renderChild2.localToGlobal(const Offset(100, 100)),
|
|
// -100 because the scroll offset is now 100.
|
|
const Offset(100, 300.0 + 100 + 200 - 100),
|
|
);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('hitTest works within SliverLayoutBuilder', (WidgetTester tester) async {
|
|
final ScrollController scrollController = ScrollController();
|
|
addTearDown(scrollController.dispose);
|
|
List<int> hitCounts = <int> [0, 0, 0];
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(50),
|
|
child: CustomScrollView(
|
|
controller: scrollController,
|
|
slivers: <Widget>[
|
|
SliverToBoxAdapter(
|
|
child: SizedBox(
|
|
height: 200,
|
|
child: GestureDetector(onTap: () => hitCounts[0]++),
|
|
),
|
|
),
|
|
SliverLayoutBuilder(
|
|
builder: (BuildContext context, SliverConstraints constraint) => SliverToBoxAdapter(
|
|
child: SizedBox(
|
|
height: 200,
|
|
child: GestureDetector(onTap: () => hitCounts[1]++),
|
|
),
|
|
),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: SizedBox(
|
|
height: 200,
|
|
child: GestureDetector(onTap: () => hitCounts[2]++),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Tap item 1.
|
|
await tester.tapAt(const Offset(300, 50.0 + 100));
|
|
await tester.pump();
|
|
expect(hitCounts, const <int> [1, 0, 0]);
|
|
|
|
// Tap item 2.
|
|
await tester.tapAt(const Offset(300, 50.0 + 100 + 200));
|
|
await tester.pump();
|
|
expect(hitCounts, const <int> [1, 1, 0]);
|
|
|
|
// Tap item 3. Shift the touch point up to ensure the touch lands within the viewport.
|
|
await tester.tapAt(const Offset(300, 50.0 + 200 + 200 + 10));
|
|
await tester.pump();
|
|
expect(hitCounts, const <int> [1, 1, 1]);
|
|
|
|
// Scrolling doesn't break it.
|
|
hitCounts = <int> [0, 0, 0];
|
|
scrollController.jumpTo(100);
|
|
await tester.pump();
|
|
|
|
// Tap item 1.
|
|
await tester.tapAt(const Offset(300, 50.0 + 100 - 100));
|
|
await tester.pump();
|
|
expect(hitCounts, const <int> [1, 0, 0]);
|
|
|
|
// Tap item 2.
|
|
await tester.tapAt(const Offset(300, 50.0 + 100 + 200 - 100));
|
|
await tester.pump();
|
|
expect(hitCounts, const <int> [1, 1, 0]);
|
|
|
|
// Tap item 3.
|
|
await tester.tapAt(const Offset(300, 50.0 + 100 + 200 + 200 - 100));
|
|
await tester.pump();
|
|
expect(hitCounts, const <int> [1, 1, 1]);
|
|
|
|
// Tapping outside of the viewport shouldn't do anything.
|
|
await tester.tapAt(const Offset(300, 1));
|
|
await tester.pump();
|
|
expect(hitCounts, const <int> [1, 1, 1]);
|
|
|
|
await tester.tapAt(const Offset(300, 599));
|
|
await tester.pump();
|
|
expect(hitCounts, const <int> [1, 1, 1]);
|
|
|
|
await tester.tapAt(const Offset(1, 100));
|
|
await tester.pump();
|
|
expect(hitCounts, const <int> [1, 1, 1]);
|
|
|
|
await tester.tapAt(const Offset(799, 100));
|
|
await tester.pump();
|
|
expect(hitCounts, const <int> [1, 1, 1]);
|
|
|
|
// Tap the no-content area in the viewport shouldn't do anything
|
|
hitCounts = <int> [0, 0, 0];
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CustomScrollView(
|
|
controller: scrollController,
|
|
slivers: <Widget>[
|
|
SliverToBoxAdapter(
|
|
child: SizedBox(
|
|
height: 100,
|
|
child: GestureDetector(onTap: () => hitCounts[0]++),
|
|
),
|
|
),
|
|
SliverLayoutBuilder(
|
|
builder: (BuildContext context, SliverConstraints constraint) => SliverToBoxAdapter(
|
|
child: SizedBox(
|
|
height: 100,
|
|
child: GestureDetector(onTap: () => hitCounts[1]++),
|
|
),
|
|
),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: SizedBox(
|
|
height: 100,
|
|
child: GestureDetector(onTap: () => hitCounts[2]++),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tapAt(const Offset(300, 301));
|
|
await tester.pump();
|
|
expect(hitCounts, const <int> [0, 0, 0]);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('LayoutBuilder does not call builder when layout happens but layout constraints do not change', (WidgetTester tester) async {
|
|
int builderInvocationCount = 0;
|
|
|
|
Future<void> pumpTestWidget(Size size) async {
|
|
await tester.pumpWidget(
|
|
// Center is used to give the SizedBox the power to determine constraints for LayoutBuilder
|
|
Center(
|
|
child: SizedBox.fromSize(
|
|
size: size,
|
|
child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
|
|
builderInvocationCount += 1;
|
|
return const _LayoutSpy();
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await pumpTestWidget(const Size(10, 10));
|
|
|
|
final _RenderLayoutSpy spy = tester.renderObject(find.byType(_LayoutSpy));
|
|
|
|
// The child is laid out once the first time.
|
|
expect(spy.performLayoutCount, 1);
|
|
expect(spy.performResizeCount, 1);
|
|
|
|
// The initial `pumpWidget` will trigger `performRebuild`, asking for
|
|
// builder invocation.
|
|
expect(builderInvocationCount, 1);
|
|
|
|
// Invalidate the layout without changing the constraints.
|
|
tester.renderObject(find.byType(LayoutBuilder)).markNeedsLayout();
|
|
|
|
// The second pump will not go through the `performRebuild` or `update`, and
|
|
// only judge the need for builder invocation based on constraints, which
|
|
// didn't change, so we don't expect any counters to go up.
|
|
await tester.pump();
|
|
expect(builderInvocationCount, 1);
|
|
expect(spy.performLayoutCount, 1);
|
|
expect(spy.performResizeCount, 1);
|
|
|
|
// Cause the `update` to be called (but not `performRebuild`), triggering
|
|
// builder invocation.
|
|
await pumpTestWidget(const Size(10, 10));
|
|
expect(builderInvocationCount, 2);
|
|
|
|
// The spy does not invalidate its layout on widget update, so no
|
|
// layout-related methods should be called.
|
|
expect(spy.performLayoutCount, 1);
|
|
expect(spy.performResizeCount, 1);
|
|
|
|
// Have the child request layout and verify that the child gets laid out
|
|
// despite layout constraints remaining constant.
|
|
spy.markNeedsLayout();
|
|
await tester.pump();
|
|
|
|
// Builder is not invoked. This was a layout-only pump with the same parent
|
|
// constraints.
|
|
expect(builderInvocationCount, 2);
|
|
|
|
// Expect performLayout to be called.
|
|
expect(spy.performLayoutCount, 2);
|
|
|
|
// performResize should not be called because the spy sets sizedByParent,
|
|
// and the constraints did not change.
|
|
expect(spy.performResizeCount, 1);
|
|
|
|
// Change the parent size, triggering constraint change.
|
|
await pumpTestWidget(const Size(20, 20));
|
|
|
|
// We should see everything invoked once.
|
|
expect(builderInvocationCount, 3);
|
|
expect(spy.performLayoutCount, 3);
|
|
expect(spy.performResizeCount, 2);
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('LayoutBuilder descendant widget can access [RenderBox.size] when rebuilding during layout', (WidgetTester tester) async {
|
|
Size? childSize;
|
|
int buildCount = 0;
|
|
|
|
Future<void> pumpTestWidget(Size size) async {
|
|
await tester.pumpWidget(
|
|
// Center is used to give the SizedBox the power to determine constraints for LayoutBuilder
|
|
Center(
|
|
child: SizedBox.fromSize(
|
|
size: size,
|
|
child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
|
|
buildCount++;
|
|
if (buildCount > 1) {
|
|
final _RenderLayoutSpy spy = tester.renderObject(find.byType(_LayoutSpy));
|
|
childSize = spy.size;
|
|
}
|
|
return const ColoredBox(
|
|
color: Color(0xffffffff),
|
|
child: _LayoutSpy(),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await pumpTestWidget(const Size(10.0, 10.0));
|
|
expect(childSize, isNull);
|
|
await pumpTestWidget(const Size(10.0, 10.0));
|
|
expect(childSize, const Size(10.0, 10.0));
|
|
});
|
|
|
|
testWidgetsWithLeakTracking('LayoutBuilder will only invoke builder if updateShouldRebuild returns true', (WidgetTester tester) async {
|
|
int buildCount = 0;
|
|
int paintCount = 0;
|
|
Offset? mostRecentOffset;
|
|
void handleChildWasPainted(Offset extraOffset) {
|
|
paintCount++;
|
|
mostRecentOffset = extraOffset;
|
|
}
|
|
|
|
Future<void> pumpWidget(String text, double offsetPercentage) async {
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: 100,
|
|
height: 100,
|
|
child: _SmartLayoutBuilder(
|
|
text: text,
|
|
offsetPercentage: offsetPercentage,
|
|
onChildWasPainted: handleChildWasPainted,
|
|
builder: (BuildContext context, BoxConstraints constraints) {
|
|
buildCount++;
|
|
return Text(text);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await pumpWidget('aaa', 0.2);
|
|
expect(find.text('aaa'), findsOneWidget);
|
|
expect(buildCount, 1);
|
|
expect(paintCount, 1);
|
|
expect(mostRecentOffset, const Offset(20, 20));
|
|
await pumpWidget('aaa', 0.4);
|
|
expect(find.text('aaa'), findsOneWidget);
|
|
expect(buildCount, 1);
|
|
expect(paintCount, 2);
|
|
expect(mostRecentOffset, const Offset(40, 40));
|
|
await pumpWidget('bbb', 0.6);
|
|
expect(find.text('aaa'), findsNothing);
|
|
expect(find.text('bbb'), findsOneWidget);
|
|
expect(buildCount, 2);
|
|
expect(paintCount, 3);
|
|
expect(mostRecentOffset, const Offset(60, 60));
|
|
});
|
|
}
|
|
|
|
class _SmartLayoutBuilder extends ConstrainedLayoutBuilder<BoxConstraints> {
|
|
const _SmartLayoutBuilder({
|
|
required this.text,
|
|
required this.offsetPercentage,
|
|
required this.onChildWasPainted,
|
|
required super.builder,
|
|
});
|
|
|
|
final String text;
|
|
final double offsetPercentage;
|
|
final _OnChildWasPaintedCallback onChildWasPainted;
|
|
|
|
@override
|
|
bool updateShouldRebuild(_SmartLayoutBuilder oldWidget) {
|
|
// Because this is a private widget and thus local to this file, we know
|
|
// that only the [text] property affects the builder; the other properties
|
|
// only affect painting.
|
|
return text != oldWidget.text;
|
|
}
|
|
|
|
@override
|
|
RenderObject createRenderObject(BuildContext context) {
|
|
return _RenderSmartLayoutBuilder(
|
|
offsetPercentage: offsetPercentage,
|
|
onChildWasPainted: onChildWasPainted,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(BuildContext context, _RenderSmartLayoutBuilder renderObject) {
|
|
renderObject
|
|
..offsetPercentage = offsetPercentage
|
|
..onChildWasPainted = onChildWasPainted;
|
|
}
|
|
}
|
|
|
|
typedef _OnChildWasPaintedCallback = void Function(Offset extraOffset);
|
|
|
|
class _RenderSmartLayoutBuilder extends RenderProxyBox
|
|
with RenderConstrainedLayoutBuilder<BoxConstraints, RenderBox> {
|
|
_RenderSmartLayoutBuilder({
|
|
required double offsetPercentage,
|
|
required this.onChildWasPainted,
|
|
}) : _offsetPercentage = offsetPercentage;
|
|
|
|
double _offsetPercentage;
|
|
double get offsetPercentage => _offsetPercentage;
|
|
set offsetPercentage(double value) {
|
|
if (value != _offsetPercentage) {
|
|
_offsetPercentage = value;
|
|
markNeedsPaint();
|
|
}
|
|
}
|
|
|
|
_OnChildWasPaintedCallback onChildWasPainted;
|
|
|
|
@override
|
|
bool get sizedByParent => true;
|
|
|
|
@override
|
|
Size computeDryLayout(BoxConstraints constraints) {
|
|
return constraints.biggest;
|
|
}
|
|
|
|
@override
|
|
void performLayout() {
|
|
rebuildIfNecessary();
|
|
child?.layout(constraints);
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
if (child != null) {
|
|
final Offset extraOffset = Offset(
|
|
size.width * offsetPercentage,
|
|
size.height * offsetPercentage,
|
|
);
|
|
context.paintChild(child!, offset + extraOffset);
|
|
onChildWasPainted(extraOffset);
|
|
}
|
|
}
|
|
}
|
|
|
|
class _LayoutSpy extends LeafRenderObjectWidget {
|
|
const _LayoutSpy();
|
|
|
|
@override
|
|
LeafRenderObjectElement createElement() => _LayoutSpyElement(this);
|
|
|
|
@override
|
|
RenderObject createRenderObject(BuildContext context) => _RenderLayoutSpy();
|
|
}
|
|
|
|
class _LayoutSpyElement extends LeafRenderObjectElement {
|
|
_LayoutSpyElement(super.widget);
|
|
}
|
|
|
|
class _RenderLayoutSpy extends RenderBox {
|
|
int performLayoutCount = 0;
|
|
int performResizeCount = 0;
|
|
|
|
@override
|
|
bool get sizedByParent => true;
|
|
|
|
@override
|
|
void performResize() {
|
|
performResizeCount += 1;
|
|
size = constraints.biggest;
|
|
}
|
|
|
|
@override
|
|
Size computeDryLayout(BoxConstraints constraints) {
|
|
return constraints.biggest;
|
|
}
|
|
|
|
@override
|
|
void performLayout() {
|
|
performLayoutCount += 1;
|
|
}
|
|
}
|