Unverified Commit 0ca5e71f authored by chunhtai's avatar chunhtai Committed by GitHub

Implement system fonts system channel listener (#38930)

* Implement system fonts system channel listener
parent 9a66018f
......@@ -464,6 +464,23 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
}
previousHourIndex = selectedHour;
PaintingBinding.instance.systemFonts.addListener(_handleSystemFontsChange);
}
void _handleSystemFontsChange () {
setState(() {
// System fonts change might cause the text layout width to change.
// Clears cached width to ensure that they get recalculated with the
// new system fonts.
estimatedColumnWidths.clear();
});
}
@override
void dispose() {
PaintingBinding.instance.systemFonts.removeListener(_handleSystemFontsChange);
super.dispose();
}
@override
......@@ -791,6 +808,21 @@ class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> {
selectedYear = widget.initialDateTime.year;
dayController = FixedExtentScrollController(initialItem: selectedDay - 1);
PaintingBinding.instance.systemFonts.addListener(_handleSystemFontsChange);
}
void _handleSystemFontsChange() {
setState(() {
// System fonts change might cause the text layout width to change.
_refreshEstimatedColumnWidths();
});
}
@override
void dispose() {
PaintingBinding.instance.systemFonts.removeListener(_handleSystemFontsChange);
super.dispose();
}
@override
......@@ -803,6 +835,10 @@ class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> {
alignCenterLeft = textDirectionFactor == 1 ? Alignment.centerLeft : Alignment.centerRight;
alignCenterRight = textDirectionFactor == 1 ? Alignment.centerRight : Alignment.centerLeft;
_refreshEstimatedColumnWidths();
}
void _refreshEstimatedColumnWidths() {
estimatedColumnWidths[_PickerColumnType.dayOfMonth.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.dayOfMonth, localizations, context);
estimatedColumnWidths[_PickerColumnType.month.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.month, localizations, context);
estimatedColumnWidths[_PickerColumnType.year.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.year, localizations, context);
......@@ -1171,6 +1207,22 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
if (widget.mode != CupertinoTimerPickerMode.hm)
selectedSecond = widget.initialTimerDuration.inSeconds % 60;
PaintingBinding.instance.systemFonts.addListener(_handleSystemFontsChange);
}
void _handleSystemFontsChange() {
setState(() {
// System fonts change might cause the text layout width to change.
textPainter.markNeedsLayout();
_measureLabelMetrics();
});
}
@override
void dispose() {
PaintingBinding.instance.systemFonts.removeListener(_handleSystemFontsChange);
super.dispose();
}
@override
......@@ -1190,6 +1242,10 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
textDirection = Directionality.of(context);
localizations = CupertinoLocalizations.of(context);
_measureLabelMetrics();
}
void _measureLabelMetrics() {
textPainter.textDirection = textDirection;
final TextStyle textStyle = _textStyleFrom(context);
......
......@@ -630,7 +630,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
}
}
class _RenderRangeSlider extends RenderBox {
class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_RenderRangeSlider({
@required RangeValues values,
int divisions,
......@@ -930,6 +930,14 @@ class _RenderRangeSlider extends RenderBox {
markNeedsLayout();
}
@override
void systemFontsDidChange() {
super.systemFontsDidChange();
_startLabelPainter.markNeedsLayout();
_endLabelPainter.markNeedsLayout();
_updateLabelPainters();
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
......
......@@ -643,7 +643,7 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
}
}
class _RenderSlider extends RenderBox {
class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_RenderSlider({
@required double value,
int divisions,
......@@ -917,6 +917,13 @@ class _RenderSlider extends RenderBox {
markNeedsLayout();
}
@override
void systemFontsDidChange() {
super.systemFontsDidChange();
_labelPainter.markNeedsLayout();
_updateLabelPainter();
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
......
......@@ -871,7 +871,7 @@ class _TappableLabel {
}
class _DialPainter extends CustomPainter {
const _DialPainter({
_DialPainter({
@required this.primaryOuterLabels,
@required this.primaryInnerLabels,
@required this.secondaryOuterLabels,
......@@ -882,7 +882,7 @@ class _DialPainter extends CustomPainter {
@required this.activeRing,
@required this.textDirection,
@required this.selectedValue,
});
}) : super(repaint: PaintingBinding.instance.systemFonts);
final List<_TappableLabel> primaryOuterLabels;
final List<_TappableLabel> primaryInnerLabels;
......
......@@ -79,6 +79,50 @@ mixin PaintingBinding on BindingBase, ServicesBinding {
super.evict(asset);
imageCache.clear();
}
/// Listenable that notifies when the available fonts on the system have
/// changed.
///
/// System fonts can change when the system installs or removes new font. To
/// correctly reflect the change, it is important to relayout text related
/// widgets when this happens.
///
/// Objects that show text and/or measure text (e.g. via [TextPainter] or
/// [Paragraph]) should listen to this and redraw/remeasure.
Listenable get systemFonts => _systemFonts;
final _SystemFontsNotifier _systemFonts = _SystemFontsNotifier();
@override
Future<void> handleSystemMessage(Object systemMessage) async {
await super.handleSystemMessage(systemMessage);
final Map<String, dynamic> message = systemMessage;
final String type = message['type'];
switch (type) {
case 'fontsChange':
_systemFonts.notifyListeners();
break;
}
return;
}
}
class _SystemFontsNotifier extends Listenable {
final Set<VoidCallback> _systemFontsCallbacks = <VoidCallback>{};
void notifyListeners () {
for (VoidCallback callback in _systemFontsCallbacks) {
callback();
}
}
@override
void addListener(VoidCallback listener) {
_systemFontsCallbacks.add(listener);
}
@override
void removeListener(VoidCallback listener) {
_systemFontsCallbacks.remove(listener);
}
}
/// The singleton that implements the Flutter framework's image cache.
......
......@@ -161,6 +161,17 @@ class TextPainter {
ui.Paragraph _paragraph;
bool _needsLayout = true;
/// Marks this text painter's layout information as dirty and removes cached
/// information.
///
/// Uses this method to notify text painter to relayout in the case of
/// layout changes in engine. In most cases, updating text painter properties
/// in framework will automatically invoke this method.
void markNeedsLayout() {
_paragraph = null;
_needsLayout = true;
}
/// The (potentially styled) text to paint.
///
/// After this is set, you must call [layout] before the next call to [paint].
......@@ -180,8 +191,7 @@ class TextPainter {
if (_text?.style != value?.style)
_layoutTemplate = null;
_text = value;
_paragraph = null;
_needsLayout = true;
markNeedsLayout();
}
/// How the text should be aligned horizontally.
......@@ -196,8 +206,7 @@ class TextPainter {
if (_textAlign == value)
return;
_textAlign = value;
_paragraph = null;
_needsLayout = true;
markNeedsLayout();
}
/// The default directionality of the text.
......@@ -221,9 +230,8 @@ class TextPainter {
if (_textDirection == value)
return;
_textDirection = value;
_paragraph = null;
markNeedsLayout();
_layoutTemplate = null; // Shouldn't really matter, but for strict correctness...
_needsLayout = true;
}
/// The number of font pixels for each logical pixel.
......@@ -239,9 +247,8 @@ class TextPainter {
if (_textScaleFactor == value)
return;
_textScaleFactor = value;
_paragraph = null;
markNeedsLayout();
_layoutTemplate = null;
_needsLayout = true;
}
/// The string used to ellipsize overflowing text. Setting this to a non-empty
......@@ -267,8 +274,7 @@ class TextPainter {
if (_ellipsis == value)
return;
_ellipsis = value;
_paragraph = null;
_needsLayout = true;
markNeedsLayout();
}
/// The locale used to select region-specific glyphs.
......@@ -278,8 +284,7 @@ class TextPainter {
if (_locale == value)
return;
_locale = value;
_paragraph = null;
_needsLayout = true;
markNeedsLayout();
}
/// An optional maximum number of lines for the text to span, wrapping if
......@@ -297,8 +302,7 @@ class TextPainter {
if (_maxLines == value)
return;
_maxLines = value;
_paragraph = null;
_needsLayout = true;
markNeedsLayout();
}
/// {@template flutter.painting.textPainter.strutStyle}
......@@ -319,8 +323,7 @@ class TextPainter {
if (_strutStyle == value)
return;
_strutStyle = value;
_paragraph = null;
_needsLayout = true;
markNeedsLayout();
}
/// {@template flutter.painting.textPainter.textWidthBasis}
......@@ -333,8 +336,7 @@ class TextPainter {
if (_textWidthBasis == value)
return;
_textWidthBasis = value;
_paragraph = null;
_needsLayout = true;
markNeedsLayout();
}
......@@ -382,8 +384,7 @@ class TextPainter {
return placeholderCount;
}() == value.length);
_placeholderDimensions = value;
_needsLayout = true;
_paragraph = null;
markNeedsLayout();
}
List<PlaceholderDimensions> _placeholderDimensions;
......
......@@ -404,7 +404,7 @@ void debugDumpSemanticsTree(DebugSemanticsDumpOrder childOrder) {
/// that layer's binding.
///
/// See also [BindingBase].
class RenderingFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, SemanticsBinding, RendererBinding {
class RenderingFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, SemanticsBinding, PaintingBinding, RendererBinding {
/// Creates a binding for the rendering layer.
///
/// The `root` render box is attached directly to the [renderView] and is
......
......@@ -121,7 +121,7 @@ class TextSelectionPoint {
/// Keyboard handling, IME handling, scrolling, toggling the [showCursor] value
/// to actually blink the cursor, and other features not mentioned above are the
/// responsibility of higher layers and not handled by this object.
class RenderEditable extends RenderBox {
class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
/// Creates a render object that implements the visual aspects of a text field.
///
/// The [textAlign] argument must not be null. It defaults to [TextAlign.start].
......@@ -634,6 +634,14 @@ class RenderEditable extends RenderBox {
markNeedsLayout();
}
@override
void systemFontsDidChange() {
super.systemFontsDidChange();
_textPainter.markNeedsLayout();
_textLayoutLastMaxWidth = null;
_textLayoutLastMinWidth = null;
}
/// The text to display.
TextSpan get text => _textPainter.text;
final TextPainter _textPainter;
......
......@@ -3231,6 +3231,40 @@ mixin ContainerRenderObjectMixin<ChildType extends RenderObject, ParentDataType
}
}
/// Mixin for [RenderObject] that will call [systemFontsDidChange] whenever the
/// system fonts change.
///
/// System fonts can change when the OS install or remove a font. Use this mixin if
/// the [RenderObject] uses [TextPainter] or [Paragraph] to correctly update the
/// text when it happens.
mixin RelayoutWhenSystemFontsChangeMixin on RenderObject {
/// A callback that is called when system fonts have changed.
///
/// By default, [markNeedsLayout] is called on the [RenderObject]
/// implementing this mixin.
///
/// Subclass should override this method to clear any extra cache that depend
/// on font-related metrics.
@protected
@mustCallSuper
void systemFontsDidChange() {
markNeedsLayout();
}
@override
void attach(covariant Object owner) {
super.attach(owner);
PaintingBinding.instance.systemFonts.addListener(systemFontsDidChange);
}
@override
void detach() {
PaintingBinding.instance.systemFonts.removeListener(systemFontsDidChange);
super.detach();
}
}
/// Variant of [FlutterErrorDetails] with extra fields for the rendering
/// library.
class FlutterErrorDetailsForRendering extends FlutterErrorDetails {
......
......@@ -56,7 +56,8 @@ class TextParentData extends ContainerBoxParentData<RenderBox> {
/// A render object that displays a paragraph of text.
class RenderParagraph extends RenderBox
with ContainerRenderObjectMixin<RenderBox, TextParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, TextParentData> {
RenderBoxContainerDefaultsMixin<RenderBox, TextParentData>,
RelayoutWhenSystemFontsChangeMixin {
/// Creates a paragraph render object.
///
/// The [text], [textAlign], [textDirection], [overflow], [softWrap], and
......@@ -460,6 +461,12 @@ class RenderParagraph extends RenderBox
_textPainter.layout(minWidth: minWidth, maxWidth: widthMatters ? maxWidth : double.infinity);
}
@override
void systemFontsDidChange() {
super.systemFontsDidChange();
_textPainter.markNeedsLayout();
}
void _layoutTextWithConstraints(BoxConstraints constraints) {
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
}
......
......@@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart';
import 'asset_bundle.dart';
import 'binary_messenger.dart';
import 'system_channels.dart';
/// Listens for platform messages and directs them to the [defaultBinaryMessenger].
///
......@@ -26,6 +27,7 @@ mixin ServicesBinding on BindingBase {
window
..onPlatformMessage = defaultBinaryMessenger.handlePlatformMessage;
initLicenses();
SystemChannels.system.setMessageHandler(handleSystemMessage);
}
/// The current [ServicesBinding], if one has been created.
......@@ -47,6 +49,14 @@ mixin ServicesBinding on BindingBase {
return const _DefaultBinaryMessenger._();
}
/// Handler called for messages received on the [SystemChannels.system]
/// message channel.
///
/// Other bindings may override this to respond to incoming system messages.
@protected
@mustCallSuper
Future<void> handleSystemMessage(Object systemMessage) async { }
/// Adds relevant licenses to the [LicenseRegistry].
///
/// By default, the [ServicesBinding]'s implementation of [initLicenses] adds
......
......@@ -69,7 +69,8 @@ class BannerPainter extends CustomPainter {
assert(textDirection != null),
assert(location != null),
assert(color != null),
assert(textStyle != null);
assert(textStyle != null),
super(repaint: PaintingBinding.instance.systemFonts);
/// The message to show in the banner.
final String message;
......
......@@ -246,7 +246,7 @@ abstract class WidgetsBindingObserver {
}
/// The glue between the widgets layer and the Flutter engine.
mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
@override
void initInstances() {
super.initInstances();
......@@ -259,7 +259,6 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB
window.onLocaleChanged = handleLocaleChanged;
window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
SystemChannels.system.setMessageHandler(_handleSystemMessage);
FlutterErrorDetails.propertiesTransformers.add(transformDebugCreator);
}
......@@ -558,7 +557,9 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB
observer.didHaveMemoryPressure();
}
Future<void> _handleSystemMessage(Object systemMessage) async {
@override
Future<void> handleSystemMessage(Object systemMessage) async {
await super.handleSystemMessage(systemMessage);
final Map<String, dynamic> message = systemMessage;
final String type = message['type'];
switch (type) {
......
// Copyright 2016 The Chromium 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/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('RenderParagraph relayout upon system fonts changes', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Text('text widget'),
)
);
const Map<String, dynamic> data = <String, dynamic>{
'type': 'fontsChange',
};
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/system',
SystemChannels.system.codec.encodeMessage(data),
(ByteData data) { },
);
final RenderObject renderObject = tester.renderObject(find.text('text widget'));
expect(renderObject.debugNeedsLayout, isTrue);
});
testWidgets('RenderEditable relayout upon system fonts changes', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: SelectableText('text widget'),
)
);
const Map<String, dynamic> data = <String, dynamic>{
'type': 'fontsChange',
};
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/system',
SystemChannels.system.codec.encodeMessage(data),
(ByteData data) { },
);
final EditableTextState state = tester.state(find.byType(EditableText));
expect(state.renderEditable.debugNeedsLayout, isTrue);
});
testWidgets('Banner repaint upon system fonts changes', (WidgetTester tester) async {
await tester.pumpWidget(
const Banner(
message: 'message',
location: BannerLocation.topStart,
textDirection: TextDirection.ltr,
layoutDirection: TextDirection.ltr,
)
);
const Map<String, dynamic> data = <String, dynamic>{
'type': 'fontsChange',
};
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/system',
SystemChannels.system.codec.encodeMessage(data),
(ByteData data) { },
);
final RenderObject renderObject = tester.renderObject(find.byType(Banner));
expect(renderObject.debugNeedsPaint, isTrue);
});
testWidgets('CupertinoDatePicker reset cache upon system fonts change - date time mode', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: CupertinoDatePicker(
onDateTimeChanged: (DateTime dateTime) { },
)
)
);
final dynamic state = tester.state(find.byType(CupertinoDatePicker));
final Map<int, double> cache = state.estimatedColumnWidths;
expect(cache.isNotEmpty, isTrue);
const Map<String, dynamic> data = <String, dynamic>{
'type': 'fontsChange',
};
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/system',
SystemChannels.system.codec.encodeMessage(data),
(ByteData data) { },
);
// Cache should be cleaned.
expect(cache.isEmpty, isTrue);
final Element element = tester.element(find.byType(CupertinoDatePicker));
expect(element.dirty, isTrue);
});
testWidgets('CupertinoDatePicker reset cache upon system fonts change - date mode', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
onDateTimeChanged: (DateTime dateTime) { },
)
)
);
final dynamic state = tester.state(find.byType(CupertinoDatePicker));
final Map<int, double> cache = state.estimatedColumnWidths;
// Simulates font missing.
cache.clear();
const Map<String, dynamic> data = <String, dynamic>{
'type': 'fontsChange',
};
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/system',
SystemChannels.system.codec.encodeMessage(data),
(ByteData data) { },
);
// Cache should be replenished
expect(cache.isNotEmpty, isTrue);
final Element element = tester.element(find.byType(CupertinoDatePicker));
expect(element.dirty, isTrue);
});
testWidgets('CupertinoDatePicker reset cache upon system fonts change - time mode', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: CupertinoTimerPicker(
onTimerDurationChanged: (Duration d) { },
)
)
);
final dynamic state = tester.state(find.byType(CupertinoTimerPicker));
// Simulates wrong metrics due to font missing.
state.numberLabelWidth = 0.0;
state.numberLabelHeight = 0.0;
state.numberLabelBaseline = 0.0;
const Map<String, dynamic> data = <String, dynamic>{
'type': 'fontsChange',
};
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/system',
SystemChannels.system.codec.encodeMessage(data),
(ByteData data) { },
);
// Metrics should be refreshed
expect(state.numberLabelWidth - 46.0 < precisionErrorTolerance, isTrue);
expect(state.numberLabelHeight - 23.0 < precisionErrorTolerance, isTrue);
expect(state.numberLabelBaseline - 18.400070190429688 < precisionErrorTolerance, isTrue);
final Element element = tester.element(find.byType(CupertinoTimerPicker));
expect(element.dirty, isTrue);
});
testWidgets('RangeSlider relayout upon system fonts changes', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: RangeSlider(
values: const RangeValues(0.0, 1.0),
onChanged: (RangeValues values) { },
),
),
)
);
const Map<String, dynamic> data = <String, dynamic>{
'type': 'fontsChange',
};
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/system',
SystemChannels.system.codec.encodeMessage(data),
(ByteData data) { },
);
final RenderObject renderObject = tester.renderObject(find.byType(RangeSlider));
expect(renderObject.debugNeedsLayout, isTrue);
});
testWidgets('Slider relayout upon system fonts changes', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Slider(
value: 0.0,
onChanged: (double value) { },
),
),
)
);
const Map<String, dynamic> data = <String, dynamic>{
'type': 'fontsChange',
};
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/system',
SystemChannels.system.codec.encodeMessage(data),
(ByteData data) { },
);
final RenderObject renderObject = tester.renderObject(find.byType(Slider));
expect(renderObject.debugNeedsLayout, isTrue);
});
testWidgets('TimePicker relayout upon system fonts changes', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: Builder(
builder: (BuildContext context) {
return RaisedButton(
child: const Text('X'),
onPressed: () {
showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 7, minute: 0),
builder: (BuildContext context, Widget child) {
return Directionality(
key: const Key('parent'),
textDirection: TextDirection.ltr,
child: child,
);
},
);
},
);
},
),
),
),
)
);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
const Map<String, dynamic> data = <String, dynamic>{
'type': 'fontsChange',
};
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/system',
SystemChannels.system.codec.encodeMessage(data),
(ByteData data) { },
);
final RenderObject renderObject = tester.renderObject(
find.descendant(
of: find.byKey(const Key('parent')),
matching: find.byType(CustomPaint),
).first
);
expect(renderObject.debugNeedsPaint, isTrue);
});
}
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