Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 3d0607b

Browse files
Defer systemFontsDidChange to the transientCallbacks phase (#117123)
* Always defer systemFontsDidChange to transientCallbacks phase * unnecessary import
1 parent 9102f2f commit 3d0607b

File tree

3 files changed

+110
-100
lines changed

3 files changed

+110
-100
lines changed

packages/flutter/lib/src/rendering/object.dart

+41-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:flutter/animation.dart';
1010
import 'package:flutter/foundation.dart';
1111
import 'package:flutter/gestures.dart';
1212
import 'package:flutter/painting.dart';
13+
import 'package:flutter/scheduler.dart';
1314
import 'package:flutter/semantics.dart';
1415

1516
import 'debug.dart';
@@ -4011,13 +4012,18 @@ mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ParentDataType
40114012
/// Mixin for [RenderObject] that will call [systemFontsDidChange] whenever the
40124013
/// system fonts change.
40134014
///
4014-
/// System fonts can change when the OS install or remove a font. Use this mixin if
4015-
/// the [RenderObject] uses [TextPainter] or [Paragraph] to correctly update the
4016-
/// text when it happens.
4015+
/// System fonts can change when the OS installs or removes a font. Use this
4016+
/// mixin if the [RenderObject] uses [TextPainter] or [Paragraph] to correctly
4017+
/// update the text when it happens.
40174018
mixin RelayoutWhenSystemFontsChangeMixin on RenderObject {
40184019

40194020
/// A callback that is called when system fonts have changed.
40204021
///
4022+
/// The framework defers the invocation of the callback to the
4023+
/// [SchedulerPhase.transientCallbacks] phase to ensure that the
4024+
/// [RenderObject]'s text layout is still valid when user interactions are in
4025+
/// progress (which usually take place during the [SchedulerPhase.idle] phase).
4026+
///
40214027
/// By default, [markNeedsLayout] is called on the [RenderObject]
40224028
/// implementing this mixin.
40234029
///
@@ -4029,15 +4035,44 @@ mixin RelayoutWhenSystemFontsChangeMixin on RenderObject {
40294035
markNeedsLayout();
40304036
}
40314037

4038+
bool _hasPendingSystemFontsDidChangeCallBack = false;
4039+
void _scheduleSystemFontsUpdate() {
4040+
assert(
4041+
SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle,
4042+
'${objectRuntimeType(this, "RelayoutWhenSystemFontsChangeMixin")}._scheduleSystemFontsUpdate() '
4043+
'called during ${SchedulerBinding.instance.schedulerPhase}.',
4044+
);
4045+
if (_hasPendingSystemFontsDidChangeCallBack) {
4046+
return;
4047+
}
4048+
_hasPendingSystemFontsDidChangeCallBack = true;
4049+
SchedulerBinding.instance.scheduleFrameCallback((Duration timeStamp) {
4050+
assert(_hasPendingSystemFontsDidChangeCallBack);
4051+
_hasPendingSystemFontsDidChangeCallBack = false;
4052+
assert(
4053+
attached || (debugDisposed ?? true),
4054+
'$this is detached during ${SchedulerBinding.instance.schedulerPhase} but is not disposed.',
4055+
);
4056+
if (attached) {
4057+
systemFontsDidChange();
4058+
}
4059+
});
4060+
}
4061+
40324062
@override
4033-
void attach(covariant PipelineOwner owner) {
4063+
void attach(PipelineOwner owner) {
40344064
super.attach(owner);
4035-
PaintingBinding.instance.systemFonts.addListener(systemFontsDidChange);
4065+
// If there's a pending callback that would imply this node was detached
4066+
// between the idle phase and the next transientCallbacks phase. The tree
4067+
// can not be mutated between those two phases so that should never happen.
4068+
assert(!_hasPendingSystemFontsDidChangeCallBack);
4069+
PaintingBinding.instance.systemFonts.addListener(_scheduleSystemFontsUpdate);
40364070
}
40374071

40384072
@override
40394073
void detach() {
4040-
PaintingBinding.instance.systemFonts.removeListener(systemFontsDidChange);
4074+
assert(!_hasPendingSystemFontsDidChangeCallBack);
4075+
PaintingBinding.instance.systemFonts.removeListener(_scheduleSystemFontsUpdate);
40414076
super.detach();
40424077
}
40434078
}

packages/flutter/lib/src/rendering/paragraph.dart

+2-30
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, Gradient, LineMetrics
88

99
import 'package:flutter/foundation.dart';
1010
import 'package:flutter/gestures.dart';
11-
import 'package:flutter/scheduler.dart';
1211
import 'package:flutter/semantics.dart';
1312
import 'package:flutter/services.dart';
1413

@@ -654,37 +653,10 @@ class RenderParagraph extends RenderBox
654653
);
655654
}
656655

657-
bool _systemFontsChangeScheduled = false;
658656
@override
659657
void systemFontsDidChange() {
660-
final SchedulerPhase phase = SchedulerBinding.instance.schedulerPhase;
661-
switch (phase) {
662-
case SchedulerPhase.idle:
663-
case SchedulerPhase.postFrameCallbacks:
664-
if (_systemFontsChangeScheduled) {
665-
return;
666-
}
667-
_systemFontsChangeScheduled = true;
668-
SchedulerBinding.instance.scheduleFrameCallback((Duration timeStamp) {
669-
assert(_systemFontsChangeScheduled);
670-
_systemFontsChangeScheduled = false;
671-
assert(
672-
attached || (debugDisposed ?? true),
673-
'$this is detached during $phase but not disposed.',
674-
);
675-
if (attached) {
676-
super.systemFontsDidChange();
677-
_textPainter.markNeedsLayout();
678-
}
679-
});
680-
break;
681-
case SchedulerPhase.transientCallbacks:
682-
case SchedulerPhase.midFrameMicrotasks:
683-
case SchedulerPhase.persistentCallbacks:
684-
super.systemFontsDidChange();
685-
_textPainter.markNeedsLayout();
686-
break;
687-
}
658+
super.systemFontsDidChange();
659+
_textPainter.markNeedsLayout();
688660
}
689661

690662
// Placeholder dimensions representing the sizes of child inline widgets.

packages/flutter/test/painting/system_fonts_test.dart

+67-64
Original file line numberDiff line numberDiff line change
@@ -11,74 +11,72 @@ import 'package:flutter/rendering.dart';
1111
import 'package:flutter/services.dart';
1212
import 'package:flutter_test/flutter_test.dart';
1313

14-
void main() {
15-
testWidgets('RenderParagraph relayout upon system fonts changes', (WidgetTester tester) async {
16-
await tester.pumpWidget(
17-
const MaterialApp(
18-
home: Text('text widget'),
19-
),
20-
);
21-
final RenderObject renderObject = tester.renderObject(find.text('text widget'));
14+
Future<void> verifyMarkedNeedsLayoutDuringTransientCallbacksPhase(WidgetTester tester, RenderObject renderObject) async {
15+
assert(!renderObject.debugNeedsLayout);
2216

23-
const Map<String, dynamic> data = <String, dynamic>{
24-
'type': 'fontsChange',
25-
};
26-
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
27-
'flutter/system',
28-
SystemChannels.system.codec.encodeMessage(data),
29-
(ByteData? data) { },
30-
);
17+
const Map<String, dynamic> data = <String, dynamic>{
18+
'type': 'fontsChange',
19+
};
20+
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
21+
'flutter/system',
22+
SystemChannels.system.codec.encodeMessage(data),
23+
(ByteData? data) { },
24+
);
3125

32-
final Completer<bool> animation = Completer<bool>();
33-
tester.binding.scheduleFrameCallback((Duration timeStamp) {
34-
animation.complete(renderObject.debugNeedsLayout);
35-
});
36-
expect(renderObject.debugNeedsLayout, isFalse);
37-
await tester.pump();
38-
expect(await animation.future, isTrue);
26+
final Completer<bool> animation = Completer<bool>();
27+
tester.binding.scheduleFrameCallback((Duration timeStamp) {
28+
animation.complete(renderObject.debugNeedsLayout);
3929
});
4030

41-
testWidgets('Safe to query RenderParagraph for text layout after system fonts changes', (WidgetTester tester) async {
31+
// The fonts change does not mark the render object as needing layout
32+
// immediately.
33+
expect(renderObject.debugNeedsLayout, isFalse);
34+
await tester.pump();
35+
expect(await animation.future, isTrue);
36+
}
37+
38+
void main() {
39+
testWidgets('RenderParagraph relayout upon system fonts changes', (WidgetTester tester) async {
4240
await tester.pumpWidget(
4341
const MaterialApp(
4442
home: Text('text widget'),
4543
),
4644
);
47-
const Map<String, dynamic> data = <String, dynamic>{
48-
'type': 'fontsChange',
49-
};
50-
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
51-
'flutter/system',
52-
SystemChannels.system.codec.encodeMessage(data),
53-
(ByteData? data) { },
54-
);
55-
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.text('text widget'));
56-
Object? exception;
57-
try {
58-
paragraph.getPositionForOffset(Offset.zero);
59-
paragraph.hitTest(BoxHitTestResult(), position: Offset.zero);
60-
} catch (e) {
61-
exception = e;
62-
}
63-
expect(exception, isNull);
45+
final RenderObject renderObject = tester.renderObject(find.text('text widget'));
46+
await verifyMarkedNeedsLayoutDuringTransientCallbacksPhase(tester, renderObject);
6447
});
6548

49+
testWidgets(
50+
'Safe to query a RelayoutWhenSystemFontsChangeMixin for text layout after system fonts changes',
51+
(WidgetTester tester) async {
52+
final _RenderCustomRelayoutWhenSystemFontsChange child = _RenderCustomRelayoutWhenSystemFontsChange();
53+
await tester.pumpWidget(
54+
Directionality(
55+
textDirection: TextDirection.ltr,
56+
child: WidgetToRenderBoxAdapter(renderBox: child),
57+
),
58+
);
59+
const Map<String, dynamic> data = <String, dynamic>{
60+
'type': 'fontsChange',
61+
};
62+
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
63+
'flutter/system',
64+
SystemChannels.system.codec.encodeMessage(data),
65+
(ByteData? data) { },
66+
);
67+
expect(child.hasValidTextLayout, isTrue);
68+
},
69+
);
70+
6671
testWidgets('RenderEditable relayout upon system fonts changes', (WidgetTester tester) async {
6772
await tester.pumpWidget(
6873
const MaterialApp(
6974
home: SelectableText('text widget'),
7075
),
7176
);
72-
const Map<String, dynamic> data = <String, dynamic>{
73-
'type': 'fontsChange',
74-
};
75-
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
76-
'flutter/system',
77-
SystemChannels.system.codec.encodeMessage(data),
78-
(ByteData? data) { },
79-
);
77+
8078
final EditableTextState state = tester.state(find.byType(EditableText));
81-
expect(state.renderEditable.debugNeedsLayout, isTrue);
79+
await verifyMarkedNeedsLayoutDuringTransientCallbacksPhase(tester, state.renderEditable);
8280
});
8381

8482
testWidgets('Banner repaint upon system fonts changes', (WidgetTester tester) async {
@@ -210,11 +208,8 @@ void main() {
210208
SystemChannels.system.codec.encodeMessage(data),
211209
(ByteData? data) { },
212210
);
213-
final RenderObject renderObject = tester.renderObject(find.byType(RangeSlider));
214-
215-
late bool sliderBoxNeedsLayout;
216-
renderObject.visitChildren((RenderObject child) {sliderBoxNeedsLayout = child.debugNeedsLayout;});
217-
expect(sliderBoxNeedsLayout, isTrue);
211+
final RenderObject renderObject = tester.renderObject(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_RangeSliderRenderObjectWidget'));
212+
await verifyMarkedNeedsLayoutDuringTransientCallbacksPhase(tester, renderObject);
218213
});
219214

220215
testWidgets('Slider relayout upon system fonts changes', (WidgetTester tester) async {
@@ -228,17 +223,9 @@ void main() {
228223
),
229224
),
230225
);
231-
const Map<String, dynamic> data = <String, dynamic>{
232-
'type': 'fontsChange',
233-
};
234-
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
235-
'flutter/system',
236-
SystemChannels.system.codec.encodeMessage(data),
237-
(ByteData? data) { },
238-
);
239226
// _RenderSlider is the last render object in the tree.
240227
final RenderObject renderObject = tester.allRenderObjects.last;
241-
expect(renderObject.debugNeedsLayout, isTrue);
228+
await verifyMarkedNeedsLayoutDuringTransientCallbacksPhase(tester, renderObject);
242229
});
243230

244231
testWidgets('TimePicker relayout upon system fonts changes', (WidgetTester tester) async {
@@ -289,3 +276,19 @@ void main() {
289276
expect(renderObject.debugNeedsPaint, isTrue);
290277
});
291278
}
279+
280+
class _RenderCustomRelayoutWhenSystemFontsChange extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
281+
bool hasValidTextLayout = false;
282+
283+
@override
284+
void systemFontsDidChange() {
285+
super.systemFontsDidChange();
286+
hasValidTextLayout = false;
287+
}
288+
289+
@override
290+
void performLayout() {
291+
size = constraints.biggest;
292+
hasValidTextLayout = true;
293+
}
294+
}

0 commit comments

Comments
 (0)