Supporting the Text Scale setting in Delphi (iOS and Android)

In Delphi (Firemonkey), when you drop a TLabel onto a form, it will have a default font size of 12. Have you ever noticed that changing the size to 11 or 13 doesn’t give the results you expect, and both are usually smaller than when set to 12. Strange!

Is it a bug? Well no, it actually isn’t.

This is because Delphi treats font size 12 as a special value which isn’t necessary 12 pts in size. It’s best to think of FontSize: 12 as “default” instead and will apply the font scaling setting from your phone.

The problem is that the font scaling only applies when your font size is 12, so your app won’t honour the phone text scaling option for headings, captions etc which have smaller font sizes applied. Ideally you’d want all test within your UI to uniformly scale up or down as needed.

Luckily, it’s easy to ask the operating system for the scaling and applying it yourself.

{$IFDEF IOS}
uses iOSapi.UIKit;
{$ENDIF IOS}
{$IFDEF ANDROID}
uses Androidapi.JNI.GraphicsContentViewText;
{$ENDIF ANDROID}

function FontScale: Single;
{$IFDEF ANDROID}
var
  Resources: JResources;

  Configuration: JConfiguration;
{$ENDIF ANDROID}
{$IFDEF IOS}
var
  f: UIFontDescriptor;

{$ENDIF IOS}
begin

  Result := 1.0;

  {$IFDEF ANDROID}

  if TAndroidHelper.Context <> nil then

  begin

    Resources := TAndroidHelper.Context.getResources;
    if Resources <> nil then

    begin
      Configuration := Resources.getConfiguration;
      if Configuration <> nil then
        Result := Configuration.fontScale;

    end;

  end;

  {$ENDIF ANDROID}
  {$IFDEF IOS}
  f := TUIFontDescriptor.OCClass.preferredFontDescriptorWithTextStyle(
          UIFontTextStyleBody);
  Result := f.pointSize / 17.0;

  {$ENDIF IOS}
end;

Note: When designing your app and deciding on the font sizes for labels, you should do this relative to a scaling of 1.0 so they’re consistent. Applying the FontScale() function value above will turn these “logical” font sizes into “actual” font sizes which respect the phone’s font scaling setting.

The function returns a scaling value you could directly multiply your logical font size with before applying to your control to get the desired effect. E.g.:

label1.Font.Size:=14 * FontScale;

The end result will be a UI which respects the text scaling setting on the device.

Tip: For ease I recommend creating a helper function which returns the actual font size from the logical one, such as:

function LogToActualFontSize(const logicalSize: Single): Single;
begin
  Result:=logicalSize * FontScale;
end;

How does this work?

Android is fairly easy – it returns the actual scaling factor to apply. E.g. the default is 1.0, smaller may be 0.8 and larger may be 1.5. We can apply these directly.

iOS is a little trickier as we can’t get a scaling directly (for good reason). Instead, we get what we need by asking the type system for the current size of the body text style.
This will already be scaled by the text size setting, so we can simply divide it with the default body style font size at “normal” test sizing (17.0) to get the ratio – i.e. scaling factor.

Caveats and Things to be wary of…

The above will give you an easy way to achieve a scaling app but doesn’t address a few issues listed below. Note that I have no solution for these but if anyone discovers one, please add a comment.

  • Font Sizes of 12 will still apply the scaling regardless of the above, so you will want to ignore those cases to avoid double-scaling. Personally, I use a font.size of 12.1 to avoid this but you could put a “if logicalSize = 12 then return 12;” line in the helper function.
  • If the user changes the font scaling setting on their phone while the app is open in the background, the app won’t get notified of the change. There are ways to do this through observers but it’s beyond the scope of this article. See this post for more details:
    https://stackoverflow.com/questions/18951332/how-to-detect-dynamic-font-size-changes-from-ios-settings
  • Test very carefully for all text scale values! Your labels are likely to be a fixed size in your UI but your font size won’t stay the same as when it was designed so may introduce cropping or overflow issues.
  • If your font text doesn’t change then the most likely culprit will be the StyledSettings property of the label/control. By default, all labels will use the default font size set by the style unless the font size is changed at design time. If you’re setting the size at runtime instead, the new size will be ignored. Uncheck the Size option in this property to avoid Firemonkey ignoring the font size you set in code, or do the following in your code:

    label.StyledSettings:=label.StyledSettings – [TStyledSetting.Size];

I hope this helps you achieve a better behaving and more accessible user experience in your Delphi app.

For reference, the following articles were really useful in discovering how to do what’s described in this article:

https://programmersought.com/article/15291678656/

https://gist.github.com/zacwest/916d31da5d03405809c4