From b2a7f4bf8f2cfcd5a72534ada8b575f0cacafc5e Mon Sep 17 00:00:00 2001 From: gspencergoog Date: Fri, 29 Sep 2017 13:19:06 -0700 Subject: [PATCH] Add support for system text scale factor. (#4124) Adds support for system text scale factor, including hooks for Android system settings changes. iOS hooks will be added in another PR. --- lib/ui/hooks.dart | 5 ++ lib/ui/window.dart | 32 ++++++++++ lib/ui/window/window.cc | 12 ++++ lib/ui/window/window.h | 1 + runtime/runtime_controller.cc | 7 ++ runtime/runtime_controller.h | 2 + shell/common/engine.cc | 43 ++++++++++++- shell/common/engine.h | 5 +- shell/platform/android/AndroidManifest.xml | 2 +- .../android/io/flutter/view/FlutterView.java | 9 +++ .../dart/window_hooks_integration_test.dart | 64 ++++++++++++++++--- 11 files changed, 168 insertions(+), 14 deletions(-) diff --git a/lib/ui/hooks.dart b/lib/ui/hooks.dart index 239dc89e7b1..cb5a8738f5e 100644 --- a/lib/ui/hooks.dart +++ b/lib/ui/hooks.dart @@ -32,6 +32,11 @@ void _updateLocale(String languageCode, String countryCode) { _invoke(window.onLocaleChanged, window._onLocaleChangedZone); } +void _updateTextScaleFactor(double textScaleFactor) { + window._textScaleFactor = textScaleFactor; + _invoke(window.onTextScaleFactorChanged, window._onTextScaleFactorChangedZone); +} + void _updateSemanticsEnabled(bool enabled) { window._semanticsEnabled = enabled; _invoke(window.onSemanticsEnabledChanged, window._onSemanticsEnabledChangedZone); diff --git a/lib/ui/window.dart b/lib/ui/window.dart index 04710f99708..97be8525379 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -254,6 +254,38 @@ class Window { _onLocaleChangedZone = Zone.current; } + /// The system-reported text scale. + /// + /// This establishes the text scaling factor to use when rendering text, + /// according to the user's platform preferences. + /// + /// The [onTextScaleFactorChanged] callback is called whenever this value + /// changes. + /// + /// See also: + /// + /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to + /// observe when this value changes. + double get textScaleFactor => _textScaleFactor; + double _textScaleFactor = 1.0; + + /// A callback that is invoked whenever [textScaleFactor] changes value. + /// + /// The framework invokes this callback in the same zone in which the + /// callback was set. + /// + /// See also: + /// + /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to + /// observe when this callback is invoked. + VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged; + VoidCallback _onTextScaleFactorChanged; + Zone _onTextScaleFactorChangedZone; + set onTextScaleFactorChanged(VoidCallback callback) { + _onTextScaleFactorChanged = callback; + _onTextScaleFactorChangedZone = Zone.current; + } + /// A callback that is invoked to notify the application that it is an /// appropriate time to provide a scene using the [SceneBuilder] API and the /// [render] method. When possible, this is driven by the hardware VSync diff --git a/lib/ui/window/window.cc b/lib/ui/window/window.cc index 54ac6895590..b8429804430 100644 --- a/lib/ui/window/window.cc +++ b/lib/ui/window/window.cc @@ -163,6 +163,18 @@ void Window::UpdateLocale(const std::string& language_code, }); } +void Window::UpdateTextScaleFactor(double text_scale_factor) { + tonic::DartState* dart_state = library_.dart_state().get(); + if (!dart_state) + return; + tonic::DartState::Scope scope(dart_state); + + DartInvokeField(library_.value(), "_updateTextScaleFactor", + { + ToDart(static_cast(text_scale_factor)), + }); +} + void Window::UpdateSemanticsEnabled(bool enabled) { tonic::DartState* dart_state = library_.dart_state().get(); if (!dart_state) diff --git a/lib/ui/window/window.h b/lib/ui/window/window.h index c3ac6564b6c..df590195447 100644 --- a/lib/ui/window/window.h +++ b/lib/ui/window/window.h @@ -45,6 +45,7 @@ class Window { void UpdateWindowMetrics(const ViewportMetrics& metrics); void UpdateLocale(const std::string& language_code, const std::string& country_code); + void UpdateTextScaleFactor(double text_scale_factor); void UpdateSemanticsEnabled(bool enabled); void DispatchPlatformMessage(fxl::RefPtr message); void DispatchPointerDataPacket(const PointerDataPacket& packet); diff --git a/runtime/runtime_controller.cc b/runtime/runtime_controller.cc index ea6f430f323..7c88b044c00 100644 --- a/runtime/runtime_controller.cc +++ b/runtime/runtime_controller.cc @@ -66,6 +66,13 @@ void RuntimeController::SetLocale(const std::string& language_code, GetWindow()->UpdateLocale(language_code_, country_code_); } +void RuntimeController::SetTextScaleFactor(double text_scale_factor) { + if (text_scale_factor_ == text_scale_factor) + return; + text_scale_factor_ = text_scale_factor; + GetWindow()->UpdateTextScaleFactor(text_scale_factor_); +} + void RuntimeController::SetSemanticsEnabled(bool enabled) { if (semantics_enabled_ == enabled) return; diff --git a/runtime/runtime_controller.h b/runtime/runtime_controller.h index a24f851c6c9..f79d9122675 100644 --- a/runtime/runtime_controller.h +++ b/runtime/runtime_controller.h @@ -35,6 +35,7 @@ class RuntimeController : public WindowClient, public IsolateClient { void SetViewportMetrics(const ViewportMetrics& metrics); void SetLocale(const std::string& language_code, const std::string& country_code); + void SetTextScaleFactor(double textScaleFactor); void SetSemanticsEnabled(bool enabled); void BeginFrame(fxl::TimePoint frame_time); @@ -66,6 +67,7 @@ class RuntimeController : public WindowClient, public IsolateClient { RuntimeDelegate* client_; std::string language_code_; std::string country_code_; + double text_scale_factor_ = 1.0; bool semantics_enabled_ = false; std::unique_ptr dart_controller_; diff --git a/shell/common/engine.cc b/shell/common/engine.cc index 85b9f760339..ce8617c4493 100644 --- a/shell/common/engine.cc +++ b/shell/common/engine.cc @@ -44,6 +44,7 @@ constexpr char kAssetChannel[] = "flutter/assets"; constexpr char kLifecycleChannel[] = "flutter/lifecycle"; constexpr char kNavigationChannel[] = "flutter/navigation"; constexpr char kLocalizationChannel[] = "flutter/localization"; +constexpr char kSystemChannel[] = "flutter/system"; bool PathExists(const std::string& path) { return access(path.c_str(), R_OK) == 0; @@ -74,6 +75,7 @@ Engine::Engine(PlatformView* platform_view) platform_view->GetVsyncWaiter(), this)), load_script_error_(tonic::kNoError), + text_scale_factor_(1.0), activity_running_(false), have_surface_(false), weak_factory_(this) {} @@ -312,7 +314,12 @@ void Engine::DispatchPlatformMessage( if (HandleLifecyclePlatformMessage(message.get())) return; } else if (message->channel() == kLocalizationChannel) { - if (HandleLocalizationPlatformMessage(std::move(message))) + if (HandleLocalizationPlatformMessage(message.get())) + return; + } else if (message->channel() == kSystemChannel) { + // This only handles textScaleFactor changes: other system messages are + // handled by DispatchPlatformMessage below. + if (HandleSystemPlatformMessage(message.get())) return; } @@ -367,7 +374,7 @@ bool Engine::HandleNavigationPlatformMessage( } bool Engine::HandleLocalizationPlatformMessage( - fxl::RefPtr message) { + blink::PlatformMessage* message) { const auto& data = message->data(); rapidjson::Document document; @@ -396,6 +403,37 @@ bool Engine::HandleLocalizationPlatformMessage( return true; } +bool Engine::HandleSystemPlatformMessage(blink::PlatformMessage* message) { + const auto& data = message->data(); + rapidjson::Document document; + document.Parse(reinterpret_cast(data.data()), data.size()); + + if (document.HasParseError() || !document.IsObject()) + return false; + + auto root = document.GetObject(); + auto type = root.FindMember("type"); + if (type == root.MemberEnd() || type->value != "systemSettings") + return false; + + // This only handles textScaleFactor changes: other system messages + // are handled by DispatchPlatformMessage. + auto text_scale_factor = root.FindMember("textScaleFactor"); + if (text_scale_factor == root.MemberEnd() || + !text_scale_factor->value.IsDouble()) { + return false; + } + text_scale_factor_ = text_scale_factor->value.GetDouble(); + if (runtime_) { + runtime_->SetTextScaleFactor(text_scale_factor_); + if (have_surface_) + ScheduleFrame(); + } + // If the only members were "type" and "textScaleFactor", then we're done. + // If there are more members, then we need to send it on to other handlers. + return root.MemberCount() == 2; +} + void Engine::DispatchPointerDataPacket(const PointerDataPacket& packet) { if (runtime_) runtime_->DispatchPointerDataPacket(packet); @@ -445,6 +483,7 @@ void Engine::ConfigureRuntime(const std::string& script_uri, default_isolate_snapshot_instr, platform_kernel); runtime_->SetViewportMetrics(viewport_metrics_); runtime_->SetLocale(language_code_, country_code_); + runtime_->SetTextScaleFactor(text_scale_factor_); runtime_->SetSemanticsEnabled(semantics_enabled_); } diff --git a/shell/common/engine.h b/shell/common/engine.h index 1baf245cb06..8da6a1a49e2 100644 --- a/shell/common/engine.h +++ b/shell/common/engine.h @@ -92,8 +92,8 @@ class Engine : public blink::RuntimeDelegate { bool HandleLifecyclePlatformMessage(blink::PlatformMessage* message); bool HandleNavigationPlatformMessage( fxl::RefPtr message); - bool HandleLocalizationPlatformMessage( - fxl::RefPtr message); + bool HandleLocalizationPlatformMessage(blink::PlatformMessage* message); + bool HandleSystemPlatformMessage(blink::PlatformMessage* message); void HandleAssetPlatformMessage(fxl::RefPtr message); bool GetAssetAsBuffer(const std::string& name, std::vector* data); @@ -106,6 +106,7 @@ class Engine : public blink::RuntimeDelegate { blink::ViewportMetrics viewport_metrics_; std::string language_code_; std::string country_code_; + double text_scale_factor_; bool semantics_enabled_ = false; // TODO(abarth): Unify these two behind a common interface. fxl::RefPtr asset_store_; diff --git a/shell/platform/android/AndroidManifest.xml b/shell/platform/android/AndroidManifest.xml index 3b4cb6894fa..1599c802a6a 100644 --- a/shell/platform/android/AndroidManifest.xml +++ b/shell/platform/android/AndroidManifest.xml @@ -11,7 +11,7 @@ - message = new HashMap<>(); + message.put("type", "systemSettings"); + message.put("textScaleFactor", textScaleFactor); + mFlutterSystemChannel.send(message); + } + private void setLocale(Locale locale) { mFlutterLocalizationChannel.invokeMethod("setLocale", Arrays.asList(locale.getLanguage(), locale.getCountry())); @@ -287,6 +295,7 @@ public class FlutterView extends SurfaceView protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); setLocale(newConfig.locale); + setTextScaleFactor(newConfig.fontScale); } float getDevicePixelRatio() { diff --git a/testing/dart/window_hooks_integration_test.dart b/testing/dart/window_hooks_integration_test.dart index f9f6f12fe4e..d7a33b0cc8f 100644 --- a/testing/dart/window_hooks_integration_test.dart +++ b/testing/dart/window_hooks_integration_test.dart @@ -34,6 +34,7 @@ void main() { VoidCallback originalOnSemanticsEnabledChanged; SemanticsActionCallback originalOnSemanticsAction; PlatformMessageCallback originalOnPlatformMessage; + VoidCallback originalOnTextScaleFactorChanged; setUp(() { originalOnMetricsChanged = window.onMetricsChanged; @@ -44,6 +45,7 @@ void main() { originalOnSemanticsEnabledChanged = window.onSemanticsEnabledChanged; originalOnSemanticsAction = window.onSemanticsAction; originalOnPlatformMessage = window.onPlatformMessage; + originalOnTextScaleFactorChanged = window.onTextScaleFactorChanged; }); tearDown(() { @@ -55,55 +57,65 @@ void main() { window.onSemanticsEnabledChanged = originalOnSemanticsEnabledChanged; window.onSemanticsAction = originalOnSemanticsAction; window.onPlatformMessage = originalOnPlatformMessage; + window.onTextScaleFactorChanged = originalOnTextScaleFactorChanged; }); test('onMetricsChanged preserves callback zone', () { Zone innerZone; Zone runZone; + double devicePixelRatio; runZoned(() { innerZone = Zone.current; window.onMetricsChanged = () { runZone = Zone.current; + devicePixelRatio = window.devicePixelRatio; }; }); window.onMetricsChanged(); - _updateWindowMetrics(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + _updateWindowMetrics(0.1234, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); expect(runZone, isNotNull); expect(runZone, same(innerZone)); + expect(devicePixelRatio, equals(0.1234)); }); test('onLocaleChanged preserves callback zone', () { Zone innerZone; Zone runZone; + Locale locale; runZoned(() { innerZone = Zone.current; window.onLocaleChanged = () { runZone = Zone.current; + locale = window.locale; }; }); _updateLocale('en', 'US'); expect(runZone, isNotNull); expect(runZone, same(innerZone)); + expect(locale, equals(const Locale('en', 'US'))); }); test('onBeginFrame preserves callback zone', () { Zone innerZone; Zone runZone; + Duration start; runZoned(() { innerZone = Zone.current; - window.onBeginFrame = (_) { + window.onBeginFrame = (Duration value) { runZone = Zone.current; + start = value; }; }); - _beginFrame(0); + _beginFrame(1234); expect(runZone, isNotNull); expect(runZone, same(innerZone)); + expect(start, equals(const Duration(microseconds: 1234))); }); test('onDrawFrame preserves callback zone', () { @@ -125,65 +137,99 @@ void main() { test('onPointerDataPacket preserves callback zone', () { Zone innerZone; Zone runZone; + PointerDataPacket data; runZoned(() { innerZone = Zone.current; - window.onPointerDataPacket = (_) { + window.onPointerDataPacket = (PointerDataPacket value) { runZone = Zone.current; + data = value; }; }); - _dispatchPointerDataPacket(new ByteData.view(new Uint8List(0).buffer)); + final ByteData testData = new ByteData.view(new Uint8List(0).buffer); + _dispatchPointerDataPacket(testData); expect(runZone, isNotNull); expect(runZone, same(innerZone)); + expect(data.data, equals(_unpackPointerDataPacket(testData).data)); }); test('onSemanticsEnabledChanged preserves callback zone', () { Zone innerZone; Zone runZone; + bool enabled; runZoned(() { innerZone = Zone.current; window.onSemanticsEnabledChanged = () { runZone = Zone.current; + enabled = window.semanticsEnabled; }; }); _updateSemanticsEnabled(window._semanticsEnabled); expect(runZone, isNotNull); expect(runZone, same(innerZone)); + expect(enabled, isNotNull); + expect(enabled, equals(window._semanticsEnabled)); }); test('onSemanticsAction preserves callback zone', () { Zone innerZone; Zone runZone; + int action; runZoned(() { innerZone = Zone.current; - window.onSemanticsAction = (_, __) { + window.onSemanticsAction = (int value, _) { runZone = Zone.current; + action = value; }; }); - _dispatchSemanticsAction(0, 0); + _dispatchSemanticsAction(1234, 0); expect(runZone, isNotNull); expect(runZone, same(innerZone)); + expect(action, equals(1234)); }); test('onPlatformMessage preserves callback zone', () { Zone innerZone; Zone runZone; + String name; runZoned(() { innerZone = Zone.current; - window.onPlatformMessage = (_, __, ___) { + window.onPlatformMessage = (String value, _, __) { runZone = Zone.current; + name = value; }; }); - _dispatchPlatformMessage(null, null, null); + _dispatchPlatformMessage('testName', null, null); expect(runZone, isNotNull); expect(runZone, same(innerZone)); + expect(name, equals('testName')); + }); + + test('onTextScaleFactorChanged preserves callback zone', () { + Zone innerZone; + Zone runZone; + double textScaleFactor; + + runZoned(() { + innerZone = Zone.current; + window.onTextScaleFactorChanged = () { + runZone = Zone.current; + textScaleFactor = window.textScaleFactor; + }; + }); + + window.onTextScaleFactorChanged(); + _updateTextScaleFactor(0.5); + expect(runZone, isNotNull); + expect(runZone, same(innerZone)); + expect(textScaleFactor, equals(0.5)); }); }); }