-
Notifications
You must be signed in to change notification settings - Fork 6k
Implementing TextScaler for nonlinear text scaling #42062
Implementing TextScaler for nonlinear text scaling #42062
Conversation
f76030e to
d58bb16
Compare
eb43895 to
1ce8f1c
Compare
1ce8f1c to
37132f2
Compare
lib/ui/platform_dispatcher.dart
Outdated
| _PlatformConfiguration _configuration = const _PlatformConfiguration(); | ||
| // The expression is evaluated when `platformTextScaler` is accessed for the | ||
| // first time. | ||
| late final bool _platformTextScalerImplemented = _getScaledFontSize(14) != -1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why these magic numbers: "14" and "-1"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
14 is arbitrarily chosen. -1 is the error code for unimplemented. I'll add that to the comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
even better, define a constant static const int _kUnimplementedErrorCode = -1
lib/ui/platform_dispatcher.dart
Outdated
| /// A callback that is invoked whenever [textScaleFactor] changes value. | ||
| /// Deprecated. Will be removed in a future version of Flutter. | ||
| /// | ||
| /// This field is renamed to [onTextScalerChanged]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: it is not just a rename, right? The semantics also changed, presumably, it fires when textScaler changes and not when textscalefactor changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now it is just a rename. It is invoked when the user changes the text scaling value. But in this PR we're not exposing textScaleFactor in public APIs so I thought the name "textScaleFactor" could be a bit confusing.
lib/ui/platform_dispatcher.dart
Outdated
| /// [onTextScaleFactorChanged] callback can be used to monitor such changes. | ||
| /// | ||
| /// Instead of directly using this getter, applications should typically use | ||
| /// `MediaQuery.scaledFontSizeOf` instead to retrive the scaled font size in a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
put MediaQuery.scaledFontSizeOf in [] to link to it in docs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that hasn't been added to the framework yet so the name is kinda fictional
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this only be mentioned when it is actually in the framework?
lib/ui/platform_dispatcher.dart
Outdated
| /// | ||
| /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to | ||
| /// observe when this callback is invoked. | ||
| TextScaler get platformTextScaler { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to double check: This is on the PlatformDispatcher because it is a global thing for the entire app? This is not going to be a per-view setting?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hasn't been confirmed yet, I'll add a comment once it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's been confirmed that this is per Android device not per monitor. I was also told device manufacturers will not be able to override the text scaling curve: maybe we can avoid using FFI/JNI and just hard-code the formula in dart.
For that we will need the Android build version and potentially some other information (it sounds like the curve is going to be different on TVs / watches than on phones). I think I'll ask for more information before proceeding since that sounds like a more performant and easier to implement solution than this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I add the TextScaler interface to the framework, what would be the easiest way to move it to dart:ui?
lib/ui/platform_dispatcher.dart
Outdated
| } | ||
|
|
||
| // Returns a negative number when there's an error: | ||
| // -1 : GetScaledFontSize is not implemented on the current platform. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering if there's a way to make errors a little more structured to make this API a little more ergonomic and avoid magic numbers like -1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an FFI call so using a primitive type is the easiest imo otherwise it would probably be a bit less efficient. The codomain can't go negative anyways.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if -1 and -2 were constants in dart and constants on the other side of ffi. They would still be magic numbers over the wire but would be logically constants. If we documented that they needed to change together than I think that would be better. I find myself wishing for proto generated constants (comment above) and a LINT.IFCHANGED in google3.
If you do use constants do not forget to update the documentation on line platform_configuration.h link 230 and engine.h line 308.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed error code -2. Is there a way to share the error code definition between java and dart? The -1 literal is used once in each language so would comments that explain the return value suffice?
lib/ui/platform_dispatcher.dart
Outdated
| throw StateError('GetScaledFontSize is not implemented on the platform. This should not be reached.'); | ||
| // Fallback to linear scaling when there's an error. Relevant error messages | ||
| // should already be printed to stderr. | ||
| case final double errorCode: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this case exist?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currently the android implementation returns -2 if the delegate is not set, for example.
| /// Deprecated. Will be removed in a future version of Flutter. | ||
| /// | ||
| /// This field is renamed to [onTextScalerChanged]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe, doc comments do not need to be duplicated in the web_ui
| } | ||
|
|
||
| abstract class TextScaler { | ||
| /// Creates a TextScaler. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove duplicated doc?
| } | ||
| } | ||
|
|
||
| final class _LinearTextScaler implements TextScaler { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to share the implementation of this with regular dart:ui? This is just a straight-up copy, right? Would make this easier to maintain...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same with _ClampedTextScaler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Makes me wonder if dart:ui should maybe just expose the _getScaledFontSize and the TestScaler stuff lives in the framework)...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some stuff are still depending on text scale factor. If we move TextScaler to the framework then we'd have to expose textScaleFactor as part of the public API.
lib/ui/platform_dispatcher.dart
Outdated
| } | ||
|
|
||
| final class _SystemTextScaler extends TextScaler { | ||
| const _SystemTextScaler(this.textScaleFactor); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the systemTextScaler dependent on the textScaleFactor?
lib/ui/platform_dispatcher.dart
Outdated
| _PlatformConfiguration _configuration = const _PlatformConfiguration(); | ||
| // The expression is evaluated when `platformTextScaler` is accessed for the | ||
| // first time. | ||
| late final bool _platformTextScalerImplemented = _getScaledFontSize(14) != -1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
even better, define a constant static const int _kUnimplementedErrorCode = -1
lib/ui/platform_dispatcher.dart
Outdated
| /// [onTextScaleFactorChanged] callback can be used to monitor such changes. | ||
| /// | ||
| /// Instead of directly using this getter, applications should typically use | ||
| /// `MediaQuery.scaledFontSizeOf` instead to retrive the scaled font size in a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this only be mentioned when it is actually in the framework?
lib/ui/platform_dispatcher.dart
Outdated
|
|
||
| /// Returns a new [TextScaler] that restricts the scaled font size to within | ||
| /// the range `[minScaleFactor * fontSize, maxScaleFactor * fontSize]`. | ||
| TextScaler clamp(double minScaleFactor, double maxScaleFactor) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you remind me again why we need this method? Also I imagine an app developer would probably more care about the clamping the font size after the scaling instead of clamping the scaling factor?
lib/ui/platform_dispatcher.dart
Outdated
| // should already be printed to stderr. | ||
| case final double errorCode: | ||
| assert(false, 'GetScaledFontSize failed with $errorCode'); | ||
| return fontSize * textScaleFactor; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a fallback behavior? shouldn't this be handled in embedding?
…engine into android-sync-text-scale
goderbauer
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Leaving it to the Android embedder people (i.e. @reidbaker) to give approval if they're ok with the changes there.
| return unscaledFontSize; | ||
| } | ||
|
|
||
| final int unscaledFloor = unscaledFontSize.floor(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still think we should leave a comment in the code explaining this.
lib/ui/platform_dispatcher.dart
Outdated
| return cachedValue; | ||
| } | ||
|
|
||
| final double unscaledFontSizeInt = unscaledFontSize.toDouble(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the variable has an Int suffix, but is typed double - shouldn't those two match?
lib/ui/platform_dispatcher.dart
Outdated
| // `unscaledFontSize`. | ||
| // | ||
| // Currently this is only implemented on newer versions of Android (SDK level | ||
| // 34, using the `TypedValue#applyDimension` API). This function returns -1 on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am slightly confused about the error codes. Line 1366 made it seem as if -1 means you provided an invalid configuration ID, this documents that -1 means the platform doesn't support non-linear text scaling. Is it intentional that both use the same error code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see also line 1394 below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I forgot to update the comments. Updated now.
lib/ui/platform_dispatcher.dart
Outdated
| // (`_configuration.configurationId`). Using an incorrect id could result in | ||
| // an unrecoverable error. | ||
| // | ||
| // Returns -1 when the specified configurationId does not match any configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
document here that if the platform hasn't provided a configurationId int the _PlatformConfiguration it means this feature is not supported and _getScaledFontSize must not be called?
| /// | ||
| /// @return The scaled font size in logical pixels, or -1 when the given | ||
| /// configuration_id did not match a valid configuration. | ||
| /// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be worthwhile to call out here as well that this must only be called if you received a non-null configurationId from . And that otherwise the platform doesn't support this kind of scaling.
| /// | ||
| /// @return The scaled font size in logical pixels, or -1 if the given | ||
| /// configuration_id did not match a valid configuration. | ||
| /// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be worthwhile to call out here as well that this must only be called if you received a non-null configurationId from . And that otherwise the platform doesn't support this kind of scaling.
| /// | ||
| /// @return The scaled font size in logical pixels, or -1 if the given | ||
| /// configuration_id did not match a valid configuration. | ||
| /// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same
| private static final String PLATFORM_BRIGHTNESS = "platformBrightness"; | ||
| private static final String CONFIGURATION_ID = "configurationId"; | ||
|
|
||
| // When hasNonlinearTextScalingSupport() returns false, this will not be initialized. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this because Java will only initialize static fields on first access and when hasNonlinearTextScalingSupport is false this is never accessed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
| * Flutter framework invokes a function to access the configuration with a generation identifier, | ||
| * this queue finds the configuration with that identifier and also cleans up configurations that | ||
| * are no longer needed. | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Somewhere (maybe here?) can we leave a breadcrumb to the future Android API that could replace all this and make this simpler? Just as a reminder for future readers of this code.
|
Hi @reidbaker could you take a look when you get a chance? I think I went through all comments but let me know if I missed any. |
| verify(executor).send(eq("flutter/settings"), messageCaptor.capture(), isNull()); | ||
| } | ||
|
|
||
| @Test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this test have a config = 34?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll give that a try. But iirc Robolectric does not support that api level yet and the test will fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it gave me
io.flutter.embedding.engine.systemchannels.SettingsChannelTest > configurationQueueWorks FAILED
java.lang.IllegalArgumentException: API level 34 is not available
at org.robolectric.plugins.UnknownSdk.verifySupportedSdk(UnknownSdk.java:45)
at org.robolectric.RobolectricTestRunner.getSandbox(RobolectricTestRunner.java:298)
at org.robolectric.RobolectricTestRunner.getSandbox(RobolectricTestRunner.java:66)
at org.robolectric.internal.SandboxTestRunner$2.evaluate(SandboxTestRunner.java:244)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But you are calling an api 34 method. If roboletric does not support api 34 then update the dependency. Or at minimum use config minSdk to api 33 so that when api 34 is added this test starts running against api 34.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None of these methods directly or indirectly called is API 34 only I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this code does not indirectly call 34 then shouldnt there be a test for the opposite of setDisplayMetricsDoesNothingOnAPILevel33. Something like setDisplayMetricsDoesXyzBehaviorOnApiLevel34?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
robolectric/robolectric#8404, it seems there's no api 34 support yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
edit: I was mistaken about roboletric api 34 support.
OK, can you file a follow up bug to add a test when we have a roboletric version that supports api 34?
platformTextScalerdoesn't need to be per window, assuming the SP -> DiP mapping is always the same on the same device (unless the user changes the preference), and does not depend on the display device's pixel density (it's confirmed).Design doc: https://docs.google.com/document/d/1-DlElw3zWRDlqoc9z4YfkU9PbBwRTnc7NhLM97ljGwk/edit#
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.