Emoji size is off on iOS

Emojis do not render with the correct height or baseline on iOS

Related: https://bugs.chromium.org/p/skia/issues/detail?id=9976&q=emoji&can=2

/cc @GaryQian @goderbauer

For people who are looking for a "today solution" try this on for size. Thanks to @Miyoyo for the generous assistance.

class EmojiTextSpan extends TextSpan {
  EmojiTextSpan({
    TextStyle style,
    String text,
    List<TextSpan> children,
    GestureRecognizer recognizer,
  }) : super(
            style: style,
            children: _parse(style, text)..addAll(children ?? []),
            recognizer: recognizer);

  static final regex = RegExp(
      r"((?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|"
      r"[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|"
      r"\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|"
      r"[\ud83c\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c\ude32-\ude3a]|"
      r"[\ud83c\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]"
      r"|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b"
      r"|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]"
      r"|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])+)");

  static List<TextSpan> _parse(TextStyle _style, String text) {
    final textStyle = _style ?? TextStyle(fontSize: 14);

    final emojiStyle = textStyle.copyWith(
      fontSize: (textStyle.fontSize) * 1.25,
      height: 0.1,
    );

    final spans = <TextSpan>[];

    text.splitMapJoin(
      regex,
      onMatch: (m) {
        spans.add(TextSpan(text: m.group(0), style: emojiStyle));
      },
      onNonMatch: (s) {
        spans.add(TextSpan(text: s));
      },
    );

    return spans;
  }
}

You can try to use the new feature of 'Strut' to lock line heights. Android will often times aggressively force line heights to be consistent, whereas Flutter tends to let the lines dynamically adjust.
In this case, the emoji font is taller than the alphanumeric font, so we provide it with more vertical space.
You should be able to provide a StrutStyle to Text with the properties of your alphanumeric font and then set forceStrutHeight: true to lock all lines to the provided metrics.

@GaryQian Is this on Master? I don't see it on Dev.

Yes, this should be on master. It was introduced very recently.
Text(
  'blah',
  style: TextStyle(...)
  strutStyle: StrutStyle(
    ... // The same as your primary font you wish to lock to.
    forceStrutHeight: true
  ),
)

@GaryQian that only seems to lock the line height. That's only part of the problem. The more important part is that the emoji's don't render at the appropriate size. When I say "the appropriate size" I am using native text fields as a reference which render emojis approximately 25% larger fontSize than the surrounding text.

Ahh I see, I can look into this. We currently do not treat emojis differently than any other text, so it would be interesting to see why android and iOS render emojis significantly larger.

They do it because it's better for user experience. I can only imagine that they handle them differently on purpose.

@GaryQian Is there any way to shift the baseline of a TextSpan down 2-3 points? I am not seeing any tools for this.

We were considering adding fine tuned leading control to TextStyle. A potential API would allow custom amounts of top and bottom leading. If the custom top leading was positive and the bottom leading was negative, this would in effect shift it down.
We have not yet added such a feature, but could consider it in the future.

I would be very surprised if the other platforms were applying a hardcoded scaling like 25 or 30%. It is much more likely that they are allowing emoji runs to fully expand as tall as the line they occupy, although that could lead to strange behavior in cases where a line has a span with a significant larger font size than the rest of the line. In all likelihood, the numerical scaling here is simply a symptom of a more dynamic policy based on font metrics.
In addition, expanding to line height would require ignoring the textstyle provided for the emoji span itself, and is a type of behavior we would like to avoid as it is inconsistent and to developers not familiar with the issue, would be very unexpected and confusing to have certain spans ignore the styling provided.

That all makes sense. My only question is, is that going to block having emojis render properly on Flutter?

@lukepighetti can you help us get some data/screen shots of how emojis render when a single line has multiple font sizes?
Particularly, what happens when the character before the emoji is of a smaller font size than the character after, and vice versa?

I used this HTML in Safari to get Native text rendering on iOS.
<!DOCTYPE html>

    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>

      8px -> jjJJ
      |🤔jjJJ <- 16px

      16px -> jjJJ🤔|
      jjJJ <- 8px

      8px => j
      j
      j
      j
      j <= 24px

      8px => 🤔
      🤔
      🤔
      🤔
      🤔 <= 24px

      8px => jJ🤔
      jJ🤔
      jJ🤔
      jJ🤔
      jJ🤔 <= 24px

So you are using a safari view to render this, which uses webkit. Webkit is for browsers and HTML, and the native iOS applications do not make use of webkit to render text.
To properly demonstrate this, you should make a bare-bones iOS native app with the desired text. This will properly use the coretext libraries that we are trying to compare to here.

Yeah, should, can and will are three different things. I have never built a native iOS app in my life so I won't be able to tackle that for a while. I have never noticed a difference between text rendered in Safari and text rendered elsewhere on iOS.

@GaryQian we should be able to set up our own comparison of text emojis and share the results here. It might not match exactly what @lukepighetti is seeing, but should still be useful for everyone.

Here it is in React Native on iOS
import React from "react";
import { StyleSheet, Text, View } from "react-native";

export default class App extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>
          <Text style={styles.eight}>8px jjJJ</Text>
          <Text style={styles.sixteen}>🤔jjJJ 16px</Text>
        </Text>
        <Text>
          <Text style={styles.sixteen}>16px jjJJ🤔</Text>
          <Text style={styles.eight}>jjJJ 8px</Text>
        </Text>
        <Text>
          <Text style={styles.sixteen}>16px jjJJ🤔</Text>
          <Text style={styles.eight}>jjJJ 8px</Text>
        </Text>
        <Text>
          <Text style={styles.eight}>8px j</Text>
          <Text style={styles.twelve}>j</Text>
          <Text style={styles.sixteen}>j</Text>
          <Text style={styles.twenty}>j</Text>
          <Text style={styles.twentyFour}>j 24px</Text>
        </Text>
        <Text>
          <Text style={styles.eight}>8px 🤔</Text>
          <Text style={styles.twelve}>🤔</Text>
          <Text style={styles.sixteen}>🤔</Text>
          <Text style={styles.twenty}>🤔</Text>
          <Text style={styles.twentyFour}>🤔 24px</Text>
        </Text>
        <Text>
          <Text style={styles.eight}>8px 🤔jJ</Text>
          <Text style={styles.twelve}>🤔jJ</Text>
          <Text style={styles.sixteen}>🤔jJ</Text>
          <Text style={styles.twenty}>🤔jJ</Text>
          <Text style={styles.twentyFour}>🤔jJ 24px</Text>
        </Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center"
  },
  eight: {
    fontSize: 8
  },
  twelve: {
    fontSize: 12
  },
  sixteen: {
    fontSize: 16
  },
  twenty: {
    fontSize: 20
  },
  twentyFour: {
    fontSize: 24
  }
});

In Flutter on iOS
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Emojis',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        body: SafeArea(
          child: Center(
            child: _Body(),
          ),
        ),
      ),
    );
  }
}

class _Body extends StatelessWidget {
  TextStyle get _base => TextStyle(color: Colors.black);

  TextStyle get _eight => _base.copyWith(fontSize: 8);
  TextStyle get _twelve => _base.copyWith(fontSize: 12);
  TextStyle get _sixteen => _base.copyWith(fontSize: 16);
  TextStyle get _twenty => _base.copyWith(fontSize: 20);
  TextStyle get _twentyFour => _base.copyWith(fontSize: 24);

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        ///
        RichText(
          text: TextSpan(
            children: <TextSpan>[
              TextSpan(text: "8px jjJJ", style: _eight),
              TextSpan(text: "🤔jjJJ 16px", style: _sixteen),
            ],
          ),
        ),

        ///
        RichText(
          text: TextSpan(
            children: <TextSpan>[
              TextSpan(text: "16px jjJJ🤔", style: _sixteen),
              TextSpan(text: "jjJJ 8px", style: _eight),
            ],
          ),
        ),

        ///
        RichText(
          text: TextSpan(
            children: <TextSpan>[
              TextSpan(text: "8px j", style: _eight),
              TextSpan(text: "j", style: _twelve),
              TextSpan(text: "j", style: _sixteen),
              TextSpan(text: "j", style: _twenty),
              TextSpan(text: "j 24px", style: _twentyFour),
            ],
          ),
        ),

        ///
        RichText(
          text: TextSpan(
            children: <TextSpan>[
              TextSpan(text: "8px 🤔", style: _eight),
              TextSpan(text: "🤔", style: _twelve),
              TextSpan(text: "🤔", style: _sixteen),
              TextSpan(text: "🤔", style: _twenty),
              TextSpan(text: "🤔 24px", style: _twentyFour),
            ],
          ),
        ),

        ///
        RichText(
          text: TextSpan(
            children: <TextSpan>[
              TextSpan(text: "8px 🤔jJ", style: _eight),
              TextSpan(text: "🤔jJ", style: _twelve),
              TextSpan(text: "🤔jJ", style: _sixteen),
              TextSpan(text: "🤔jJ", style: _twenty),
              TextSpan(text: "🤔jJ 24px", style: _twentyFour),
            ],
          ),
        ),
      ],
    );
  }
}

Side by side comparison

Looks like emoji scaling is not the same on iOS, it might be logarithmic?

Android
iOS

30
27

45
40

60
55

75
60

90
65

Thanks for the reference images/plots, I'll investigate!
It looks like we are rendering emojis at the same size as native Android on Android devices (Tried with pixel 2xl, pocophone). This means the emoji size problem is isolated to iOS. This means we may be incorrectly passing something to coretext, whereas opentype is doing it correctly.

Yes, In Android they look much better. Any idea when you release a fix (especially in emoji size on iOS). Highly appreciated.

Emoji's are still jacked up on iOS. It makes chat/social apps look horrible. Are there any blockers I can assist with?

https://github.com/flutter/engine/pull/9406
Currently attempting a harfbuzz update (last update was ~11/2018) before looking further, as the bug is likely buried quite deep.

So it looks like the harfbuzz upgrade did not resolve this (although it resolves some other things).
I'll have to dig deeper!

Any updates on this?

@lukepighetti @miyoyo your solution is great and works, but I've found a problem: gender based emojis are rendered only as one gender, then added another gender modifier symbol.
How it should render:

How it renders

The above solution is an impartial shim, non-exhaustive, and the issue should be resolved by Flutter at the framework level.

For anyone else struggling with my problem, this is the solution I've ended up using, no more need for luke's EmojiTextSpan fix:
TextField(
  readOnly: true, // Important
  maxLines: null, // Important
  textAlign: TextAlign.center,
  controller: TextEditingController(text: /* Your text */),
  style: TextStyle(
    // Your style
  ),
  decoration: InputDecoration.collapsed(hintText: ''), // Important
),

So weird that works, but I'm glad you found a solution. That also handles the gender symbols?

@lvaskuu I am trying to use your solution, except the emojis are still small. Have tried this on multiple versions of iOS on many devices. Does someone who is able to use @Ivaskuu 's workaround mind posting a screenshot comparison between the TextField and Text versions, as well as the code? I would like to use this as a lead to find the problem but need to repro first.

This is going to be a blocker on one of our apps that we're building for a client. Requesting internal review of this issue.

We should make sure that we're honoring the font metrics precisely. It may be that Android and iOS are doing weird things like bumping the font size for some glyphs or something. We should definitely not do that. That's the kind of decision that leads to continuous bugs and more bugs and hacks upon hacks.

The best way to help us resolve this bug would be to create a test font that verifies that a glyph in the emoji space renders identically to an identical glyph outside the emoji space, and then compare that result in Flutter to Android and iOS with the same font. That would help narrow down the cause of the difference. If it turns out they behave differently for emoji then the problem is with Android and iOS, not Flutter. If with such a font, Android, iOS, and Flutter all do the same thing, then we're probably misinterpreting the actual emoji fonts somehow; we should then investigate exactly what is happening there.

I'm rage quitting this issue. Ping me if I can be helpful.

"It may be that Android and iOS are doing weird things like bumping the font size for some glyphs or something. We should definitely not do that."
I think that's great in principle, but the reality is that IF (and I'm not presuming anything) that is what they're doing then they're doing it for a reason.
Assuming this is an intentional design choice by Apple and Google, by not following what the native platforms are doing it creates a discrepancy between a Flutter app and a native app that would be pretty obvious if the user is using, for example, a native chat app and a chat app made in Flutter with similar UI components.
It's simple to rationalize it in technical terms but, from a user experience perspective, it can be pretty jarring. How is that explained to the end user / client if they report that the emojis look off. You can't exactly say "well .. iOS and android are actually wrong and we're right" or something to that degree.

I think the question here is, is Flutter about being correct, or is it about being accurate?

My understanding is that Flutter's goal is to replicate the native platforms at such a degree of accuracy that there is no discernible difference between Flutter and the native platforms.

If it turns out that we're doing what the font standards say is correct, and for some reason Android and iOS are not, then we would address this by having a widget or other mechanism that replicates the Android and iOS behavior at a high level, not in the engine.
That's a big if though. We don't know. Maybe we're not doing the right thing per the standards. Hence my earlier comment about how to make progress.

I definitely know we won't have time to make a font to demonstrate this issue. That's a pretty tall ask, no? Not saying you are asking us to, but we definitely wont be.
If someone in the community can't make a font, can we have it escalated internally to do the validation? Otherwise I foresee this languishing.

I'm not sure what you mean by "internally". We're all just one team here. You, me, Gary, everyone. That's what it means for this to be an open source project.
Re "validation", there doesn't seem to be any ambiguity that there's a difference between what iOS does and what Flutter does, that was made clear in your original comment from March last year. This bug is definitely a valid bug.
Re "languishing": We've been trying to address this for almost a year, see e.g. Gary's comments on March 5th, June 20th, or November 19th, and you yourself have been making significant contributions in the form of showing how other frameworks generate different output than we do.
As mentioned above, the next step that I think we need to do is probably to experiment with carefully-crafted fonts to see if we can figure out where the difference is happening. For example, if one were to put the smile emoji into the position for "A", would iOS render it the same or different than it renders other emoji? Maybe we are rendering emoji differently for some reason; would a font that put the identical "A" glyph in the spot for the smiley emoji cause Flutter to render it differently than in the "A" position? These are all things to investigate.
Currently the person most qualified to look at this (Gary) is focused on much more critical bugs like keyboards crashing on Samsung devices, I assume you would agree that keyboard crashes are more important than emoji sizing. But that doesn't mean this bug is unimportant, and if someone is available to work on it and there's nothing more critical to work on then I would encourage them to take this up and determine the cause.

Building on top of previous workarounds mentioned here I wrote a widget to simplify abstraction
Just use it as a widget like this:
// Your child widget that will have
child: EmojiText(
  text: text,
  style: TextStyle(
    fontSize: 16,
  emojiFontMultiplier: 1.3, // Default value to increase emojis size by 30%
  ),
),

import 'package:flutter/material.dart';

class EmojiText extends StatelessWidget {
  final String text;
  final TextStyle style;
  final double emojiFontMultiplier;

  const EmojiText({
    @required this.text,
    @required this.style,
    this.emojiFontMultiplier = 1.3, // Increase by 30%
  });

  // Regex to match emojis
  static final regex = RegExp(
      "(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])");

  List<TextSpan> generateTextSpans(String text) {
    List<TextSpan> spans = [];
    final TextStyle emojiStyle = style.copyWith(
      fontSize: (style.fontSize * emojiFontMultiplier),
      letterSpacing: 2,
    );

    text.splitMapJoin(
      regex,
      onMatch: (m) {
        spans.add(
          TextSpan(
            text: m.group(0),
            style: emojiStyle,
          ),
        );
        return "";
      },
      onNonMatch: (s) {
        spans.add(TextSpan(text: s));
        return "";
      },
    );
    return spans;
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: RichText(
        text: TextSpan(children: generateTextSpans(text), style: style),
      ),
    );
  }
}

@nicogranuja Tried your example and it doesn't work with skin tones. It puts a box after the emoji with the skin tone.

Building on top of previous workarounds mentioned here I wrote a widget to simplify abstraction
Just use it as a widget like this:
// Your child widget that will have
child: EmojiText(
  text: text,
  style: TextStyle(
    fontSize: 16,
  emojiFontMultiplier: 1.3, // Default value to increase emojis size by 30%
  ),
),

import 'package:flutter/material.dart';

class EmojiText extends StatelessWidget {
  final String text;
  final TextStyle style;
  final double emojiFontMultiplier;

  const EmojiText({
    @required this.text,
    @required this.style,
    this.emojiFontMultiplier = 1.3, // Increase by 30%
  });

  // Regex to match emojis
  static final regex = RegExp(
      "(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])");

  List<TextSpan> generateTextSpans(String text) {
    List<TextSpan> spans = [];
    final TextStyle emojiStyle = style.copyWith(
      fontSize: (style.fontSize * emojiFontMultiplier),
      letterSpacing: 2,
    );

    text.splitMapJoin(
      regex,
      onMatch: (m) {
        spans.add(
          TextSpan(
            text: m.group(0),
            style: emojiStyle,
          ),
        );
        return "";
      },
      onNonMatch: (s) {
        spans.add(TextSpan(text: s));
        return "";
      },
    );
    return spans;
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: RichText(
        text: TextSpan(children: generateTextSpans(text), style: style),
      ),
    );
  }
}

This works but it strips the skin tones and adds the skin tone emoji after it, it also does't work with unicode 13.

Maybe we should just build a twemoji package and give up on Flutter displaying Emoji's properly on native channels, there clearly isn't an appetite for maintaining fidelity across different platforms.

What if the render details are not because of emoji themselves, but the font used?
Apple's Emoji are already using custom extensions to TTF, what if they use one that overrides font size and scaling?
Could someone try to see if, when forcing a different emoji font on native iOS, the emoji size is also off?
If this is the case, that the font itself is the reason why emoji are differently-sized, maybe a special case could be made in the framework for only glyphs represented by the apple emoji font? (I think there's about 3000?)
Also, couldn't help but notice that chrome itself gets it wrong, so, that sounds like a problem with harfbuzz itself.

Is there a effective hack around it? iOS chat UI is getting screwed up when the users starts using emoji's @nicogranuja 's widget solves some of it but as @EliasDeuss mentioned it doesn't go well with emoji's that have modifier's, the regex would need a way to do grouping with modifier's first if possible and according to the internet the mentioned regex works for emoji's till Dec 2018.
Slightly depressing this happens, difficult to explain to clients its not really a flutter problem but would happen in flutter apps and not in native or even react native for that matter. Thing's like this makes simpler minded people to go against using such a powerful platform like Flutter.
Thanks for any help and all the work and effort so far!

Add a plus on the reggex

@miyoyo could you elaborate, unicode characters are a bit beyond me, where exactly should I add it here?
(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])

((?:\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+)
This wraps the original part in a non capturing group, and allows for the capture of more than one character per textspan, which should avoid the breakage of some groups you'd expect in unicode
If something does not match, can you post an example here (If possible, the exact bytes making up the characters)?

((?:\u00a9|\u00ae|[\u2000-\u3300]|\ufe0f|[\ud83c-\ud83e][\udc00-\udfff]|\udb40[\udc61-\udc7f])+)
This is an "almost perfect" matcher according to the apple emoji font, every character it supports in the lower ranges, and more than it supports in the higher range
I extracted the supported letters from the apple color emoji font, and converted them into a regex, then did some hand simplification, note the addition of unicode tags \udb40[\udc61-\udc7f] as well as the variation selector, which the previous regex does not include.

@miyoyo this ((?:\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+)
by itself saved my day from hours of grief with an adamant client, and this
((?:\u00a9|\u00ae|[\u2000-\u3300]|\ufe0f|[\ud83c-\ud83e][\udc00-\udfff]|\udb40[\udc61-\udc7f])+) is just perfection, I have EmojiText working only for iOS only anyway with a if(Platform.isIOS) check so all's fine for now, thank you @miyoyo this COVID 19 lockdown has my MacBook Pro several states away and with the triple lockdown in my country I was having difficulty doing any iOS specific stuff

I think it's a bug with skia, track the progress here.

@droidluv can you post up your latest EmojiTextSpan hack? We need it for a project very soon.

@lukepighetti If I understand what was done, it's effectively replacing the single character, broken regex with the multi character fixed regex
import 'package:flutter/material.dart';

class EmojiText extends StatelessWidget {
  final String text;
  final TextStyle style;
  final double emojiFontMultiplier;

  const EmojiText({
    @required this.text,
    @required this.style,
    this.emojiFontMultiplier = 1.3, // Increase by 30%
  });

  // Regex to match emojis
  static final regex = RegExp(
      "((?:\u00a9|\u00ae|[\u2000-\u3300]|\ufe0f|[\ud83c-\ud83e][\udc00-\udfff]|\udb40[\udc61-\udc7f])+)");

  List<TextSpan> generateTextSpans(String text) {
    List<TextSpan> spans = [];
    final TextStyle emojiStyle = style.copyWith(
      fontSize: (style.fontSize * emojiFontMultiplier),
      letterSpacing: 2,
    );

    text.splitMapJoin(
      regex,
      onMatch: (m) {
        spans.add(
          TextSpan(
            text: m.group(0),
            style: emojiStyle,
          ),
        );
        return "";
      },
      onNonMatch: (s) {
        spans.add(TextSpan(text: s));
        return "";
      },
    );
    return spans;
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: RichText(
        text: TextSpan(children: generateTextSpans(text), style: style),
      ),
    );
  }
}

@droidluv can you post up your latest EmojiTextSpan hack? We need it for a project very soon.

I did exactly what @miyoyo has said, my chat UI was getting crazy when people started using emoji's and multiple sentence's, my solution was to tweak the emoji's alone so they don't mess with the line height and the emojiFontMultiplier key in EmojiText class should help, the downside is you'll to make a compromise on resizing the emoji's so for me .75 worked, emoji's are 25% tinier but apparently a tiny emoji is acceptable over uneven spacing between multiple lines

I always seem to get asked to build chat features and this emoji issue is nearly unforgivable. Our only saving grace on this last project is that our stakeholders are all on Android haha.

@lukaspili did you find any workaround for this?

Some nicely exposed settings for emoji rendering would be awesome and would elevate Flutter from any other framework.
I built some choose your path chat story apps with e.g. Adobe Air, and let me tell you: adding emoji support was quite the hassle, especially through gpu rendering and custom fonts.
I tested EmojiTextSpan and think it works quite well.
I added one tweak that renders text vertically centered with the emojis (and added a param for the size multiplication), so the spacings actually look even:
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

/// A "polyfix" [TextSpan] that properly renders Emojis at the correct size
/// and vertical-alignment for the given [style].
///
/// See https://github.com/flutter/flutter/issues/28894
class EmojiTextSpan extends TextSpan {
  EmojiTextSpan({
    TextStyle style,
    String text,
    List<TextSpan> children,
    GestureRecognizer recognizer,
    double emojiSizeMultiply
  }) : super(
            style: style,
            children: _parse(style, text, emojiSizeMultiply)..addAll(children ?? []),
            recognizer: recognizer);

  static final regex = RegExp(
      r"((?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|"
      r"[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|"
      r"\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|"
      r"[\ud83c\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c\ude32-\ude3a]|"
      r"[\ud83c\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]"
      r"|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b"
      r"|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]"
      r"|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])+)");

  static List<TextSpan> _parse(TextStyle _style, String text, double emojiSizeMultiply) {
    final textStyle = _style ?? TextStyle(fontSize: 14);
    final multiplySize = emojiSizeMultiply ?? 1.30;

    final emojiStyle = textStyle.copyWith(
      fontSize: (textStyle.fontSize) * multiplySize,
      height: 1,
      letterSpacing: 2,
    );

    final spans = <InlineSpan>[];

    text.splitMapJoin(
      regex,
      onMatch: (m) {
        spans.add(WidgetSpan(
          alignment: PlaceholderAlignment.middle,
          child: Text(m.group(0), style: emojiStyle),
        ));
        return;
      },
      onNonMatch: (s) {
        spans.add(TextSpan(text: s));
        return;
      },
    );

    return [
      TextSpan(
        children: spans
      )
    ];
  }
}

Here's a helper for placing a single emoji into a widget. It's flakey, the edges clip randomly, and it generally doesn't look right. But it may be useful to someone.
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:twic/extensions.dart';
import 'package:twic/style.dart';
class EmojiBadge extends StatelessWidget {
  EmojiBadge(this.emoji, {this.diameter = AppStyle.p20})
      : assert(diameter >= 10),
        assert(diameter <= 120),
        debugPainting = false;
  EmojiBadge.debug(this.emoji, {@required this.diameter})
      : debugPainting = true;
  final String emoji;
  final double diameter;
  final bool debugPainting;
  @override
  Widget build(BuildContext context) {
    return SizedBox.fromSize(
      size: Size.square(diameter),
      child: Stack(
        children: <Widget>[
          /// Emoji, hand tailored for each platform
          if (Platform.isIOS)
            Transform.scale(
              scale: 1.1,
              child: Text(
                emoji,
                style: TextStyle(
                  fontSize: diameter * 1,
                  color: Colors.white,
                  height: 1.0,
                ),
              ),
            ).nudge(y: diameter * 0.1)
          else if (Platform.isAndroid)
            Transform.scale(
              scale: 0.98,
              child: Text(
                emoji,
                style: TextStyle(
                  fontSize: diameter * 0.95,
                  color: Colors.white,
                  height: 1.0,
                ),
              ),
            ).nudge(x: diameter * -0.09, y: diameter * 0.07 + 0.2)
          else
            /// Best guess for unimplemented platforms
            Transform.scale(
              scale: 1.04,
              child: Text(
                emoji,
                style: TextStyle(
                  fontSize: diameter * 0.98,
                  color: Colors.white,
                  height: 1.0,
                ),
              ),
            ).nudge(y: diameter * 0.085),
          /// Debug overlay
          if (debugPainting)
            Container(
              decoration: BoxDecoration(
                  borderRadius: AppStyle.stadiumRadius,
                  color: Colors.black87),
            ),
        ],
      ),
    );
  }
}

Preview

complete code sample
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Material App',
      theme: ThemeData.dark(),
      home: Home(),
    );
  }
}

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Material App Bar'),
      ),
      body: Center(
        child: Container(
          child: Text('Flutter 🤣🤣🤣 Fluter'),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {},
      ),
    );
  }
}

flutter doctor -v
[✓] Flutter (Channel master, 1.24.0-2.0.pre.111, on Mac OS X 10.15.7 19H2
    darwin-x64, locale en-GB)
    • Flutter version 1.24.0-2.0.pre.111 at
      /Users/tahatesser/Code/flutter_master
    • Framework revision fe189baf34 (10 hours ago), 2020-10-21 22:25:53 -0400
    • Engine revision f459a86610
    • Dart version 2.11.0 (build 2.11.0-242.0.dev)

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
    • Android SDK at /Users/tahatesser/Code/sdk
    • Platform android-30, build-tools 30.0.2
    • ANDROID_HOME = /Users/tahatesser/Code/sdk
    • Java binary at: /Applications/Android
      Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build
      1.8.0_242-release-1644-b3-6222593)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 12.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 12.1, Build version 12A7403
    • CocoaPods version 1.10.0.rc.1

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 4.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build
      1.8.0_242-release-1644-b3-6222593)

[✓] VS Code (version 1.50.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.15.1

[✓] Connected device (4 available)
    • iPhone 12 (mobile) • 722C6A4C-3FCA-4FC6-9D30-9702A0E784B8 • ios
      • com.apple.CoreSimulator.SimRuntime.iOS-14-1 (simulator)
    • macOS (desktop)    • macos                                • darwin-x64
      • Mac OS X 10.15.7 19H2 darwin-x64
    • Web Server (web)   • web-server                           • web-javascript
      • Flutter Tools
    • Chrome (web)       • chrome                               • web-javascript
      • Google Chrome 86.0.4240.80

• No issues found!

This issue is still happening on Flutter (Channel dev, 1.27.0-1.0.pre)

This issue is still happening on Flutter (Channel dev, 1.27.0-1.0.pre)

@lukepighetti I don't believe anyone is working on it (also has no milestone).

I think you're right, but it is still broken. 😅

Emojis are also changing baseline on iOS. Haven't tried other platforms.

Still not fixed?

Bump. Why is this still not fixed? It is obviously needed by a lot of developers and apps...

EmojiTextSpan

Can you show an example usage of this?

gesture

Can you show an example usage of this?

For people who are looking for a "today solution" try this on for size. Thanks to @miyoyo for the generous assistance.
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';

/// A "polyfix" [TextSpan] that properly renders Emojis at the correct size
/// and vertical-alignment for the given [style].
///
/// See https://github.com/flutter/flutter/issues/28894
class EmojiTextSpan extends TextSpan {
  EmojiTextSpan({
    TextStyle style,
    String text,
    List<TextSpan> children,
    GestureRecognizer recognizer,
  }) : super(
            style: style,
            children: _parse(style, text)..addAll(children ?? []),
            recognizer: recognizer);

  static final regex = RegExp(
      r"((?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|"
      r"[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|"
      r"\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|"
      r"[\ud83c\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c\ude32-\ude3a]|"
      r"[\ud83c\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]"
      r"|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b"
      r"|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]"
      r"|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])+)");

  static List<TextSpan> _parse(TextStyle _style, String text) {
    final textStyle = _style ?? TextStyle(fontSize: 14);

    final emojiStyle = textStyle.copyWith(
      fontSize: (textStyle.fontSize) * 1.30,
      height: 1,
      letterSpacing: 2,
    );

    final spans = <TextSpan>[];

    text.splitMapJoin(
      regex,
      onMatch: (m) {
        spans.add(TextSpan(text: m.group(0), style: emojiStyle));
      },
      onNonMatch: (s) {
        spans.add(TextSpan(text: s));
      },
    );

    return spans;
  }
}

can you show an example usage of this?

Is there any news about this bug? Noticed it is P4 while it should be P2. P4 reads like we cannot really build chat and social media apps in flutter. I attach a pic below. As properly reported in the thread, emojis render tiny and it also changes height line (see red mark)
Any help with this would be much appreciated!

Is there any news about this bug? Noticed it is P4 while it should be P2. P4 reads like we cannot really build chat and social media apps in flutter. I attach a pic below. As properly reported in the thread, emojis render tiny and it also changes height line (see red mark)
Any help with this would be much appreciated!

I have had my eye on this issue for the past 3 years, about a year ago they add it to p4 but nothing has really come of it.

@lukepighetti - 👏  thanks for raising this issue, we've received a lot of comments from people who know how to change the size of emojis in TextSpan widgets.
@justinmc @darrenaustin - 🙏 The solutions mentioned here do not solve small emojis inside TextField. When you look at apps like WhatsApp and Messenger, the emojis appear almost 2⃣❎ the size of normal text and really 'pop' out. End users feel more engaged when the emojis are bigger and sharper, and help to propel apps made with Flutter.
⭐ It's the joint 26th highest commented open issue, we would all love to see the flutter team takes this to the next level by allowing users to pass in an optional property that would enable emojis to be rendered bigger.
Examples - see how much bigger the emoji is compared to the 'e':
Non-flutter app 1: https://user-images.githubusercontent.com/25297786/141166925-fca4919c-2c30-41c6-9ed9-c197ad075896.png
Non-flutter app 2: https://user-images.githubusercontent.com/25297786/141167419-e751bf9d-6ca6-4a54-8d2f-abf25cde95f3.png
My flutter app: https://user-images.githubusercontent.com/25297786/141167617-9d5cfd56-2c38-419c-8e1b-e33f9e7bf0b9.png

To be perfectly frank I'm quite disappointed in how this issue has been handled to date

My stance is this should be P3, but if someone like Stream felt this was an issue, P2. cc @imtoori @Nash0x7e2 @GroovinChip

we felt this issue, but I'd be ok with P3

I wish this was higher priority, but looking at the priority docs I think P3 is accurate unless its blocking a large consumer. Feel free to justify a higher priority if I missed something.

I'm not sure how issues are prioritised by the Flutter team but this issue appears to have been raised for 2 years 8 months. Given we are all rooting for this product it would be good to see it escalated further, or given some time frames.

It’s a chicken-egg problem. There won’t be any large consumer on flutter untill flutter is not ready to build a premium app on top of it.
As you suggest, P3 should be fine. But #15953 camera issue is P3 and still unfixed after 3yrs. We have a workaround for that popular camera issue, but no workaround for emojis

Yeah, there don't appear to be any provisions for giving something more 'priority' because it's been sitting so long.

Yeah I recall the acceleration after “Flutter exposed…” video from that guy TechLead? It is sad not to have instruments to take action on time, nor being able to provide a deadline. As a company we would have gone native in hindsight. Now we are into this after the investment.
Hope you guys will report this situation to whom it may concern.

For those who are struggling with this issue, a possible workaround is to combine the textspan trick with native textfield https://pub.dev/packages/flutter_native_text_input
Below a pic showing the differences in emojii size. However, there are other features of native textfield that flutter missed to implement. As for the mentioned package, it seems it is affected by a few minor bugs. Everyone is welcome to contribute to fix them. I conclude remarking that is both unbeliveable and unacceptable not having bug-free camera and textfield in flutter. It is like building the roof when the house still misses the walls.

Thanks for this suggestion @iosephmagno the package you shared seems to only cover iOS for now. Despite all of this recent activity in this thread - it's disheartening to see no-one from Flutter is providing any comments

@lukepighetti could you please rename the issue to "Emoji size is too small in TextField on iOS and Android" - so that we can get to the crux of the issue? The sizing in regular text spans is not an issue from my perspective because we have sufficiently good workarounds, however, emoji sizes cannot be altered in TextFields as is.

@ken-ng-esotec emojis size should be fine on android. However, we and Henry are fixing this issue by wrapping native. We will cover android as well if it is needed.
From our first-hand experience, flutter still needs serious improvements (eg. camera) but can be used as alternative for kotlin. For ios we will problably go native as soon as possible. In a nutshell it is cross platform only on paper or for tiny undemanding projects.

Hello again. Agree that Flutter Text Field for Android is on par with standard Android Native sizing of emojis. What I am looking for is summarised in the below image. Once again, thanks for the package!

This problem still persists

Is there any plan to fix this soon ?
It is really problematic for chat applications because flutter_native_text_input has other issues regarding the focus even if the keyboard is actually what we expect in iOS (with a smooth drag_to_dismiss) regarding the design and behavior.

Is there any plan to fix this soon ?
It is really problematic for chat applications because flutter_native_text_input has other issues regarding the focus.

Hi Tom, may you check again on focus issues with latest 1.2.4 release?
And let me know if there are still issues with that. Thanks:)

Thanks for your package @henryleunghk by the way !
However the focus is still an issue as the keyboard is dismissed and then re-pushed.
I think we will continue this discussion in your repo :)

Thanks for your package @henryleunghk by the way !
However the focus is still an issue as the keyboard is dismissed and then re-pushed.
I think we will continue this discussion in your repo :)

Thanks for reporting this.
Please check for version 1.3.0🙌🏻

@jason-simmons is this resolved with skparagraph?

SkParagraph does not change how emojis are rendered

It is worth noting that above mentioned plugin would not only fix the emojis rendering issue but also a number of other pending issues related to ios keyboard such as wrong behaviors on tapping gestures and the likes.

@iosephmagno But the plugin solves the issues only for the text input, right? There is also a problem with handling emojis on iOS in regular Text widgets as well. I understand that the plugin can be used to create read-only text inputs to imitate Text but that would create unnecessary controllers for those fields. Or did I go wrong somewhere?

cc @TahaTesser is there anyone who needs to be made aware of this issue?

@fatadel there is a good workaround for that mentioned in this thread. Also, @henryleunghk  is wrapping Text as well, you can follow his work and donate if you guys cannot contribute in other way.

Let me extend the invite to everyone here.
Hope flutter team will fix this soon. Meanwhile community is invited to help with above mentioned wrapper by both coding and donating.

@fatadel there is a good workaround for that mentioned in this thread. Also, @henryleunghk is wrapping Text as well, you can follow his work and donate if you guys cannot contribute in other way.

@iosephmagno By "good workaround" do you mean the one that uses large regular expressions to produce text spans?
If so, I don't think it's a good solution since checking for such large regexps is quite heavy + you do it for every single text view and every single build (unless the text is constant). Please correct me, if I am wrong.

For people who are looking for a "today solution" try this on for size. Thanks to @miyoyo for the generous assistance.
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';

/// A "polyfix" [TextSpan] that properly renders Emojis at the correct size
/// and vertical-alignment for the given [style].
///
/// See https://github.com/flutter/flutter/issues/28894
class EmojiTextSpan extends TextSpan {
  EmojiTextSpan({
    TextStyle style,
    String text,
    List<TextSpan> children,
    GestureRecognizer recognizer,
  }) : super(
            style: style,
            children: _parse(style, text)..addAll(children ?? []),
            recognizer: recognizer);

  static final regex = RegExp(
      r"((?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|"
      r"[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|"
      r"\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|"
      r"[\ud83c\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c\ude32-\ude3a]|"
      r"[\ud83c\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]"
      r"|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b"
      r"|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]"
      r"|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])+)");

  static List<TextSpan> _parse(TextStyle _style, String text) {
    final textStyle = _style ?? TextStyle(fontSize: 14);

    final emojiStyle = textStyle.copyWith(
      fontSize: (textStyle.fontSize) * 1.30,
      height: 1,
      letterSpacing: 2,
    );

    final spans = <TextSpan>[];

    text.splitMapJoin(
      regex,
      onMatch: (m) {
        spans.add(TextSpan(text: m.group(0), style: emojiStyle));
      },
      onNonMatch: (s) {
        spans.add(TextSpan(text: s));
      },
    );

    return spans;
  }
}

@lukepighetti Thanks a lot for your workaround! Works quite nice, but I've encountered an issue that it doesn't render some emojis properly. Try for example these ones - 👷🏻♀👳🏼♂🧑🏻⚕👩👩👧👨👩👦. Flutter natively renders them as expected, so I guess something is wrong with the regex.

@fatadel Please test with these ones and report here your results:
https://stackoverflow.com/questions/24840667/what-is-the-regex-to-extract-all-the-emojis-from-a-string/50026845#50026845
https://stackoverflow.com/questions/24070515/rendering-or-deleting-emoji/25437515#25437515

As for the native textfield plugin, we have made huge progress with it (eg. fixing focus issue, etc).

@lukepighetti can we please set this issue to P2? It is so much sensitive we cannot understand why it is P4. From our extensive tests, it is fair to say flutter textfield for ios is not usable for any typing intensive app (social media, chat and text editor apps) due to #28894 #48434 and #12920. We are trying to workaround issues with a native wrapper, by real solution should be getting a fix from you guys.
Thx for any help you could provide 🙏

@lukepighetti @iosephmagno
The regex from https://www.regextester.com/106421 seems to be a much better alternative -
r'((\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+)'

At least it parses correctly the ones I have reported (apart from others).

@lukepighetti @iosephmagno
The regex from https://www.regextester.com/106421 seems to be a much better alternative -
r'((\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+)'

At least it parses correctly the ones I have reported (apart from others).

Or even more thorough approach - https://raw.githubusercontent.com/mathiasbynens/emoji-test-regex-pattern/main/dist/latest/javascript.txt, but it's quite massive.

@fatadel 👍 this looks promising. Please keep testing and let us know. Desire formula would cover the most emojis but still be computationally light

@fatadel That shim I posted above is not a good one. If you come up with a better one please post the complete version in here, and I will link it in my original comment.

Guys, can you get emoji baseline rendered properly with textspan workaround? In our sample, emoji is not vertical aligned, pic below.
Any help with this would be much appreciated.

@fatadel are you experience same above issue?

@lukepighetti @TahaTesser Any idea on this baseline issue? I attach below two pics, one from native and one from flutter regex workaround. As you can see, in our case the emoji is not vertical aligned to text.

Please see this comment: https://github.com/flutter/flutter/issues/28894#issuecomment-1100920604

We have an idea but would be like a code mess. Or we can wrap UILabel as done with textfield. If anyone has fixed this somehow, is invited to share 🤦♂😊

This is from Collin. What you guys think?
https://github.com/collinjackson/flutter_native_label

The flutter_native_label plugin is now published to pub.dev.
It is a workaround for this issue. Feedback welcome!

Please notice that unlike UITextView you cannot select and copy text of a label, but you dont need this feature in a chat app context (bubbles, etc). Pros is that UILabel consumes less resources and performs well. To recap, Native InputField + Flutter_native_label hopefully fix following flutter’s text issues on ios: #28894 #48434 and #12920

Please notice that unlike UITextView you cannot select and copy text of a label, but you dont need this feature in a chat app context (bubbles, etc). Pros is that UILabel consumes less resources and performs well. To recap, Native InputField + Flutter_native_label hopefully fix following flutter’s text issues on ios: #28894 #48434 and #12920

Support for copying is implemented.

Hi, I tried on latest stable, master and beta channel and I find them similar between Flutter and iOS native app

Demo

Flutter
iOS native
iOS native

Sample code
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Material App',
      theme: ThemeData.dark(),
      home: Home(),
    );
  }
}

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      backgroundColor: Colors.white,
      body: SafeArea(
        child: GestureDetector(
          onTap: () {
            FocusScope.of(context).unfocus();
          },
          child: Center(
            child: SingleChildScrollView(
              child: Column(
                children: [
                  Text('Flutter',
                    style: TextStyle(color: Colors.black, fontSize: 24),
                  ),
                  SizedBox(height: 50,),
                  Text(
                    'Flutter 🤣😅😅 Fluter Flutter 🙏 🙏 🙏  Fluter Flutter ⭐⭐⭐ ',
                    style: TextStyle(color: Colors.black, fontSize: 24),
                  ),
                  SizedBox(height: 50,),
                  Container(
                    margin: EdgeInsets.symmetric(horizontal: 12),
                    child: TextField(
                      maxLines: 5,
                        textAlign: TextAlign.center,
                        controller: TextEditingController()..text = 'Flutter 🤣😅😅 Fluter Flutter 🙏 🙏 🙏  Fluter Flutter ⭐⭐⭐ ',
                        decoration: InputDecoration(
                            isDense: true,
                            contentPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 4),
                            border: OutlineInputBorder(
                              borderSide: BorderSide(color: Colors.grey, width: 1.0),
                            ),
                            enabledBorder: OutlineInputBorder(
                              borderSide: BorderSide(color: Colors.grey, width: 1.0),
                            )),

                        style: TextStyle(color: Colors.black, fontSize: 24)),
                  )
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

flutter doctor -v
[✓] Flutter (Channel stable, 3.0.0, on macOS 12.2.1 21D62 darwin-x64, locale en-VN)
    • Flutter version 3.0.0 at /Users/huynq/Documents/GitHub/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision ee4e09cce0 (2 days ago), 2022-05-09 16:45:18 -0700
    • Engine revision d1b9a6938a
    • Dart version 2.17.0
    • DevTools version 2.12.2

[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
    • Android SDK at /Users/huynq/Library/Android/sdk
    • Platform android-31, build-tools 31.0.0
    • ANDROID_HOME = /Users/huynq/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • CocoaPods version 1.11.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2021.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)

[✓] Android Studio (version 4.1)
    • Android Studio at /Users/huynq/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/201.7042882/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)

[✓] Android Studio
    • Android Studio at /Users/huynq/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-1/203.7185775/Android Studio Preview.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6842174)

[✓] IntelliJ IDEA Community Edition (version 2020.3.3)
    • IntelliJ at /Applications/IntelliJ IDEA CE.app
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart

[✓] VS Code (version 1.67.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.40.0

[✓] Connected device (3 available)
    • iPhone 13 (mobile) • 5F5D609C-3B0B-4C48-872C-961928157361 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-15-2 (simulator)
    • macOS (desktop)    • macos                                • darwin-x64     • macOS 12.2.1 21D62 darwin-x64
    • Chrome (web)       • chrome                               • web-javascript • Google Chrome 101.0.4951.54

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!

[✓] Flutter (Channel master, 3.1.0-0.0.pre.666, on macOS 12.2.1 21D62 darwin-x64, locale en-VN)
    • Flutter version 3.1.0-0.0.pre.666 at /Users/huynq/Documents/GitHub/flutter_master
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 496049263e (36 minutes ago), 2022-05-11 23:14:11 -0700
    • Engine revision b8ca6a5e0e
    • Dart version 2.18.0 (build 2.18.0-106.0.dev)
    • DevTools version 2.13.1

[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
    • Android SDK at /Users/huynq/Library/Android/sdk
    • Platform android-31, build-tools 31.0.0
    • ANDROID_HOME = /Users/huynq/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • CocoaPods version 1.11.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2021.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)

[✓] Android Studio (version 4.1)
    • Android Studio at /Users/huynq/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/201.7042882/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)

[!] Android Studio
    • Android Studio at /Users/huynq/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-1/203.7185775/Android Studio Preview.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    ✗ Unable to find bundled Java version.
    • Try updating or re-installing Android Studio.

[✓] IntelliJ IDEA Community Edition (version 2020.3.3)
    • IntelliJ at /Applications/IntelliJ IDEA CE.app
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart

[✓] VS Code (version 1.67.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.40.0

[✓] Connected device (3 available)
    • iPhone 13 (mobile) • 5F5D609C-3B0B-4C48-872C-961928157361 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-15-2 (simulator)
    • macOS (desktop)    • macos                                • darwin-x64     • macOS 12.2.1 21D62 darwin-x64
    • Chrome (web)       • chrome                               • web-javascript • Google Chrome 101.0.4951.54

[✓] HTTP Host Availability
    • All required HTTP hosts are available

! Doctor found issues in 1 category.

[✓] Flutter (Channel beta, 2.13.0-0.4.pre, on macOS 12.2.1 21D62 darwin-x64, locale en-VN)
    • Flutter version 2.13.0-0.4.pre at /Users/huynq/Documents/GitHub/flutter_beta
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 25caf1461b (6 days ago), 2022-05-05 14:23:09 -0700
    • Engine revision c5caf749fe
    • Dart version 2.17.0 (build 2.17.0-266.8.beta)
    • DevTools version 2.12.2

[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
    • Android SDK at /Users/huynq/Library/Android/sdk
    • Platform android-31, build-tools 31.0.0
    • ANDROID_HOME = /Users/huynq/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • CocoaPods version 1.11.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2021.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)

[✓] Android Studio (version 4.1)
    • Android Studio at /Users/huynq/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/201.7042882/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)

[✓] Android Studio
    • Android Studio at /Users/huynq/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-1/203.7185775/Android Studio Preview.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.8+10-b944.6842174)

[✓] IntelliJ IDEA Community Edition (version 2020.3.3)
    • IntelliJ at /Applications/IntelliJ IDEA CE.app
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart

[✓] VS Code (version 1.67.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.40.0

[✓] Connected device (3 available)
    • iPhone 13 (mobile) • 5F5D609C-3B0B-4C48-872C-961928157361 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-15-2 (simulator)
    • macOS (desktop)    • macos                                • darwin-x64     • macOS 12.2.1 21D62 darwin-x64
    • Chrome (web)       • chrome                               • web-javascript • Google Chrome 101.0.4951.54

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!

@lukepighetti Can you pls try again and update here your result?

@huycozy the emojis are not rendered the same. Please double check your image above. Maybe they now have a more similar size but spacing is wrong

@huycozy native on the rightside, your image.

Hi, I tried on latest stable, master and beta channel and I find them similar between Flutter and iOS native app
Demo
Sample code
flutter doctor -v
@lukepighetti Can you pls try again and update here your result?

Looks good to me on 3.0.0 on stable. If there are other issues I think we should open a new ticket.

It'd be interesting to know what commit fixed this, or whether it's a particular iOS version. It's great that this looks better, but it'd also be good to have some kind of regression test for it.

I have no clue which commit fixed this, I would also like to know.

https://github.com/flutter/flutter/issues/28894#issuecomment-470507172 doesn't look like it's changed at all to me on master.

reopened as @lukepighetti couldn't on the locked thread.

Nice to hear that. We would appreciate if the fix would guarantee also correct VA (letter spacing). The feeling is that focus is mostly on the emojis size issue, but that’s just a part of it

Surely not a P4. This should be urgent.

I doubt the priority is going to change anytime soon. If we can document more issues maybe it will help put some heat under it.

If you mean comments, I doubt we’ll get more heat than this. People are disheartened.

I don't think angry comments increase the heat, but adding actual examples of where this is falling apart might.

@lukepighetti what examples? Emojis’ size and letter spacing is always off, no specific case needed.
Saying that people dont comment because disheartened is not angry reply, just the truth.
3yrs passed by, no deadline, p4 and no one really on it. Why do you guys expect more comments in the thread?

@lukepighetti btw pur dev has made this repo
https://github.com/indjec/ui-label
You can see comparison between UIlabel native wrapper and flutter text. Maybe it would be easier to compare them and used to test your fix, when available.

Any update on this? Crazy that this bug is not solved after 3.5 years.

Just going to add this again because it got burried, but this is a Skia bug.
Here is the issue, it's currently absolutely ignored: https://bugs.chromium.org/p/skia/issues/detail?id=9976&q=emoji&can=2
@lukepighetti You may want to add that link to the original post for quick reference

@miyoyo thx for trying to move this forward.
It is sad to read again “medium” priority at that link.
Our point is that if an issue halts the launch of entire categories of products, a fix should land with high urgency.
Unfortunately, current policy didnt allow a priority change. Can I suggest to reconsider the rule?

Any chance this fix will land on master anytime soon?

Can we please change the priority? 90% of time people write on phone, they add emojis

Thanks @lukepighetti, I adjusted your code a bit to center the emojis.
class EmojiText extends StatelessWidget {
  final String text;
  final TextStyle style;
  final double emojiFontMultiplier;

  const EmojiText({
    required this.text,
    required this.style,
    this.emojiFontMultiplier = 1.4, // Increase by 40%
  });

  // Regex to match emojis
  static final regex = RegExp("(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])");

  List<InlineSpan> generateTextSpans(String text) {
    List<InlineSpan> spans = [];
    final TextStyle emojiStyle = style.copyWith(
      fontSize: (style.fontSize! * emojiFontMultiplier),
      letterSpacing: 0,
      height: 1,
    );

    text.splitMapJoin(
      regex,
      onMatch: (m) {
        spans.add(
          WidgetSpan(
            alignment: PlaceholderAlignment.middle,
            child: Text(m.group(0) ?? '', style: emojiStyle),
          ),
        );
        return "";
      },
      onNonMatch: (s) {
        spans.add(TextSpan(text: s));
        return "";
      },
    );
    return spans;
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: RichText(
        strutStyle: StrutStyle(
          forceStrutHeight: true,
        ),
        text: TextSpan(
          children: generateTextSpans(text),
          style: style,
        ),
      ),
    );
  }
}

Usage:
EmojiText(
  text: 'Hello world 😊',
  style: TextStyle(
    fontSize: 15,
  ),
)

@simplenotezy unfortunately there is no workaround for this issue so far. Since issue is severe and pending for such a long time, I think flutter team cannot fix it due to some technical limitation related to skia.
I see you are coding a chat app, here a few remarks you might want to know:

if you try to improve emojis baseline, they will get cut (you can see issue in your very image attached below)
that regex formula doesnt work well. Several emojis will not render or will not render properly for a specific color. To discover them, you should try them all.
emojis will render badly inside textfield. For a chat app, this is a deal breaker. I would suggest you to use Reactive Native. Discord for ios is made in React, performance are very good, you can give it a look.

My emoji fix is not a good one. I wouldn't recommend anyone use it. But maybe it can help people who are feeling extreme pain. Probably the best thing you can do is improve the regex. I'm not sure what the emoji unicode regex actually is.

Yep formula would improve by adding ufe0f, udb40 and udc61-udc7f.
However, improving formula is not beneficial because emojis issue will still be visible inside textfields and end result would be sort of frankenstein (emojis looking good inside Text and bad inside TextField).
Hope this issue will be fixed soon, all people trying my app candidly described textfield as “weird” and “defective”: weird/ugly emojis, weird keyboard’s movement (it is the unsync animation issue), weird look (it is the other known fidelity issues).

Guys this is serious. Please see below 2 mockups to understand the gravity of the situation. One pic is native style and the other one is real flutter text.
I do understand there is some pesky bug on skia that you still couldnt fix, but we cannot ship app with this bug. Also, I think at this point it would be fair to write clearly on flutter website that text on ios is not production ready and mention this bug.

priority p4, opened in 2019...
Definitely adding this to the list I share with the people who ask me "should I use Flutter or not?"

Definitely adding this to the list I share with the people who ask me "should I use Flutter or not?"

Agree on this if you are trying to start a social / chat app now.
I have found a new ugly Emoji issue maybe linked to this one and opened a new issue #118154.
For those who have created chat app I suggest you to check your BouncingScrollPhysics on iOS for ListView and TextField and go to workaround this...

Flutter team is making good steps forward in improving text/textfield, but yes they are not production ready. Beyond emojis issues, there are known bugs related to keyboard animation, selection, magnifier lens and platform fidelity. They are working on them but it will take time.
@mariopepe from my understanding this p1 has been tagged as p4 not because flutter team thinks it is p4, but because there is a pesky issue on Skia and p4 warns the community not to expect a fix anytime soon.
Another point which might deserve a second thought, is whether the flutter rendering approach should be applied to textfield. Texfield is not “just another widget”. Both Apple and Google are adding so many features to it that the only way to provide a native-like experience is to wrap the native one. For example, see Translate, Lookup, etc (pics below). Then there is UI fidelity which is tricky to catch up on iOS but also on android, where vendors customize toolbars and users love to change themes and fonts.
Another thought: I noticed that community is starting to spread word about not to use flutter for serious apps or apps where text is key. If this trend becomes pervasive, it will be hard to revert. Maybe proving a temporary workaround to community might be necessary, same as with shaders warmup. The two widgets might coexist inside app: flutter textfield widget for form fields and search fields, flutter native_textfield widget for chat textfields or text editors.

Maybe proving a temporary workaround to community might be necessary, same as with shaders warmup. The two widgets might coexist inside app: flutter textfield widget for form fields and search fields, flutter native_textfield widget for chat textfields or text editors.

I think you have a point that it often feels to be limited by the Flutter text fields compared to native ones. Some apps add additional functionality (passwords managers or dictionaries apps), and the selection experience is usually much more natural. On the other hand Flutter text fields look consistent across various system versions and vendors, and you can customize them to your own liking.
There were some attempts in showing the native text field like flutter_native_text_input to use platform views hosting text fields. It may be fine in cases like static sign in page, but in more complex environments like scrolling lists or multiline views it may not be the best experience.
I wonder if Flutter team has any plans to make a significant change in how text fields are displayed, or are they going to continue with the current approach, whilst trying to make them as close to platform as possible.

That plugin you mentioned was sort of abandoned due to issues with platform views, especially in multiline mode and on the android side.
As for performance, one single native textfield inside a chat screen or a text-editor screen performs very well.
Keep improving flutter textfield to mimic native experience might be a dead-end street due to issues listed above. Also, questions are: How long will it take? It is a fact that we couldn’t fix emojis in 3yrs. Will it really feel native? Very unlikely. Do we want flutter to be perceived as the go-to platform to code quick cheap apps? That’s a real risk.

The only real solution here is to wait for impeller, or to somehow wake the Skia issue up.
Clearly, nobody on the Chrome team for macOS cares either, since nobody complains regularly about it.
No point complaining here, again, this is more relevant: https://bugs.chromium.org/p/skia/issues/detail?id=9976&q=emoji&can=2

The only real solution here is to wait for impeller, or to somehow wake the Skia issue up.
Clearly, nobody on the Chrome team for macOS cares either, since nobody complains regularly about it.
No point complaining here, again, this is more relevant: https://bugs.chromium.org/p/skia/issues/detail?id=9976&q=emoji&can=2

Check reply there in 2020. From then after no responses at all. If they don’t want / cant answer, I dont see the use of keeping asking.
But your worry about Chrome team not working on ios looks groundless. Desktop flutter counts 1% of flutter projects. Issue is that skia text is buggy and difficult to fix. As bungeman says in his comment, it is a miracle that things ever work at all. Waiting for Impeller won’t help since it seems to use Skia for text rendering.

My humble advice here is:

try to put more people on this issue because it is so severe that prevents apps from being shipped. Unfortunately community cant help much here, issue is so tricky that even flutter team has an hard time fixing it.
inform on website that text on ios is not production ready and mention main text/textfield issues. People cant imagine issues with text and tend to discover this problem several months after coding.

 Ofc some will switch to React Native but will still think good of flutter and might use it for next projects.

@delfme

your worry about Chrome team not working on ios looks groundless

It isn't on iOS, as iOS effectively bans other browser engines, however, on macOS, Chrome does have the exact same bug, this is how this image was produced:

It still isn't impossible to use a separate component that isn't Skia for text layout and shaping, that's just the current plan, text rendering is already planned to be done by impeller.

Pic makes sense.
On dart side, I had to apply a Sizemultiplier which is 1.22 on ios and 1.1 on android (emojis size is a bit off on android as well). These values reproduce what I see on telegram with 100% fidelity.
Also, I had to Offset emojis to compensate their baseline that breaks when size is increased. This is tricky because it must be dynamically parametrized with both font size and text height properties and requires 12 dioptres and testing with letter “g” ;) Formula first calculates Offset by leveraging on font size, then it further adjusts the value accordingly to text height (otherwise emojis baseline would break if user changes text height).
I dont remember whether I had to change also VA for emojis, but maybe VA adjusts automatically as soon as emojis size is set correctly.

@delfme

It still isn't impossible to use a separate component that isn't Skia for text layout and shaping, that's just the current plan, text rendering is already planned to be done by impeller.

I might be wrong but Impeller will keep using Skia for text layout (SkParagraph), so fix is needed there. But a text-wizard coder with design skill can do it easily by following my fix above. Im not into skia/engine or I would have done it myself

@delfme

It still isn't impossible to use a separate component that isn't Skia for text layout and shaping, that's just the current plan, text rendering is already planned to be done by impeller.

I might be wrong but Impeller will keep using Skia for text layout (SkParagraph), so fix is needed there. But a text-wizard coder with design skill can do it easily by following my fix above. Im not into skia/engine or I would have done it myself

Actually, I think this isn't a text shaping problem, but a rendering problem, from what I can see in chrome, the emojis take the correct amount of space in text, but they're just drawn completely wrong.
Without a direct 1:1 comparison though, it'll be hard to point to the exact problem, and yeah, without proper rendering, the only solution would be to track shaping boxes (and I'm far too lazy to do that)

@miyoyo you can use my trick above to fix emojis rendering for Text.
To fix emojis rendering inside Textfield we have to code a native textfield wrapper.
Coding a native textfield wrapper to workaround emojis issue might seem overkilling, but flutter textfield still suffers of several issues and we couldn’t use it anyway inside a social media/messaging app where textfield issues get amplified. For example, users wouldn’t accept the lack of the native toolbar and there are other known issues like these ones:
#119121
#100342
#50563
#38530
#115400
#120373
#105028
#72029

@miyoyo Forgot to mention the keyboard animation issue on android #11683 (where I wrote a workaround).
I also added #120551
The idea is to help flutter team improve textfield and try to wrap native textfield which should be used to workaround emojis issue and inside more demanding apps (where platform fidelity is key).
Impeller team said that emojis fix would require detaching from Skia.
They said this is not trivial and cannot be done anytime soon.

I've spent the last day or so looking at this. Here's what I can tell:

Skia is doing exactly what we ask it to - rendering an emoji from Apple Color Emoji at the exact font size we're asking for.
A simple program using CoreText shows the same behavior.
A simple program purely using Skia shows the same behavior.
On macOS, a SwiftUI "Text" widget shows the same behavior.
Safari and Chrome show the same behavior on macOS.

iOS seems to have some kind of special handling in Safari/Chrome/UIKit/SwiftUI, where AppleColorEmoji are scaled up a bit from what their normal size would be. I initially thought this was a multiplier of something like 125% (which is about what the scaling difference is when the font size is 16), but at larger sizes it goes away. I tried a test of rendering emoji at increasing sizes and the increase seems to be nearly linear for a 🤔 emoji:

Font Size
Glyph Height

11
28

12
30

13
32

14
34

15
38

16
40

17
40

18
40

19
41

20
42

21
43

22
44

23
46

24
48

25
50

26
52

27
54

28
56

29
56

30
58

You can see this in https://github.com/indjec/ui-label if you change the font size to something sufficiently large - there are still some spacing differences but the size difference is much less noticeable at larger font sizes.
I'm not quite sure what to do about this. One option would be to have some mode in Skia that lets us bump up the size a bit when the font is Apple Color Emoji and use that only on iOS. That's probably good enough to improve the fidelity. It's a bit disappointing that UIKit isn't open source to be able to see what it's doing with this or why it renders things any differently here. I tried finding relevant code in WebKit and couldn't.
Some sample programs:
A basic CoreText program that renders some Apple Color Emoji more-or-less the way Skia does to a png:
#include <Foundation/foundation.h>

#include <CoreFoundation/CoreFoundation.h>
#include <CoreGraphics/CoreGraphics.h>
#include <CoreText/CTFontManager.h>
#include <CoreText/CoreText.h>
#include <ImageIO/ImageIO.h>
#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h>

int main() {
  CTFontRef ct_font = CTFontCreateWithName((CFStringRef) @"Apple Color Emoji",
                                           (CGFloat)30, nullptr);

  if (!ct_font) {
    printf("Canot find Apple Color Emoji?\n");
    return -1;
  }

  CGGlyph glyphs[2];
  glyphs[0] = 1089;
  glyphs[1] = 2427;
  CGPoint positions[2];
  positions[0] = CGPointMake(50, 50);
  positions[1] = CGPointMake(90, 50);

  const CGBitmapInfo bitmapInfo =
      kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst;

  size_t width = 200;
  size_t height = 100;
  size_t row_bytes = width * sizeof(uint32_t);

  unsigned short image[height * row_bytes];

  CGContextRef context =
      CGBitmapContextCreate(image, width, height, 8, row_bytes,
                            CGColorSpaceCreateDeviceRGB(), bitmapInfo);

  CTFontDrawGlyphs(ct_font, glyphs, positions, 2, context);

  CGContextFlush(context);

  CGImageRef out_image = CGBitmapContextCreateImage(context);
  CFURLRef url =
      (__bridge CFURLRef)[[NSURL alloc]<EMAIL_ADDRESS>  CGImageDestinationRef destination = CGImageDestinationCreateWithURL(
      url, (__bridge CFStringRef)UTTypePNG.identifier, 1, nil);
  CGImageDestinationAddImage(destination, out_image, nil);
  CGImageDestinationFinalize(destination);
  CFRelease(destination);

  return 0;
}

A Skia test (only really makes sense on iOS/macOS):
    auto manager = SkFontMgr::RefDefault();
    auto aceType = manager->matchFamilyStyle("Apple Color Emoji", SkFontStyle());
    auto arialType = manager->matchFamilyStyle("Arial", SkFontStyle());

    auto fontSizes = {12, 16, 24, 30, 32, 64, 128};
    SkScalar y = 100;
    for (auto fontSize : fontSizes) {
        SkTextBlobBuilder textBlobBuilder;
        SkTextBlobBuilder textBlobBuilder2;
        SkFont aceFont(aceType, fontSize);
        SkFont arialFont(arialType, fontSize);

        const SkTextBlobBuilder::RunBuffer& run1 = textBlobBuilder.allocRun(arialFont, 4, 10, y);
        run1.glyphs[0] = 39;
        run1.glyphs[1] = 72;
        run1.glyphs[2] = 80;
        run1.glyphs[3] = 82;

        SkRect bounds[4];
        arialFont.getBounds(run1.glyphs, 4, bounds, nullptr);
        SkScalar advance = 0;
        for (size_t i = 0; i < 4; i++) {
            advance += bounds[i].width();
        }
        const SkTextBlobBuilder::RunBuffer& run2 =
                textBlobBuilder2.allocRun(aceFont, 2, advance + fontSize / 2, y);
        run2.glyphs[0] = 1089;
        run2.glyphs[1] = 2427;
        sk_sp<const SkTextBlob> blob = textBlobBuilder.make();
        sk_sp<const SkTextBlob> blob2 = textBlobBuilder2.make();

        SkPaint paint;
        paint.setColor(SK_ColorBLUE);
        canvas->drawTextBlob(blob.get(), 0, 0, paint);
        canvas->drawTextBlob(blob2.get(), 0, 0, paint);
        y += aceFont.getMetrics(nullptr) * 2;
    }

@delfme - What does your fix look like with large text?
If what I'm seeing is right, then I'd expect at larger font sizes (say larger than size 30), your change would make the emoji look bigger than iOS native labels.

Yes we noticed that and procrastinated a fix since we dont use big size. But at first glance it looked like the parameters should be adjusted according to a few sets. Say
< 18
[18-25]
[25-35]
etc…
I wrote just random numbers.

Yeah, I think what's happening is that Apple is making some kind of asymptotic change to the size, so smaller sizes get scaled up more (at 16pt, it's about 125%, but by the time you hit 30 it's equal).

Very interesting! Thanks for digging into this.

I tried a test of rendering emoji at increasing sizes

Huh, those results are quite puzzling.
For each +1 increment in the font size, the glyph height first increases by +2 for a while; later +1 for a bit; then back to +2. With that faster-slower-faster behavior, it doesn't look like any kind of straight-line relationship, even with any kind of rounding or quantization.
Then there's also the odd wrinkle from 14 up to 18 where it jumps once by +4, then hangs out (+0) for a few increments. Plus a smaller wrinkle at 28-30.
I'm not sure how to compare those results to a multiplier like 125%. What glyph heights would you expect as normal behavior?

@gnprice yeah no straightline relationship apart partial paths, we had to manually play around with complex texts and do side by side comparison with native.
Size apart, line height is an issue too. Have you tried a long text? On dart it took us some time to match height fidelity in a multiline scenario.
Quick mention: idk if the skia fix can be made CP, but on android we also adjust size for perfect fidelity. With a 16.5-17pt font size, we scale up by 1.22 on iOS and 1.1 on android.

If someone can figure out the exact function or mapping table that iOS is using, we could probably try to do something to emulate this. But it'll be kind of painful because:

Besides the sizing differences, we don't really match the way iOS draws text pixel for pixel anyway (for example kerning is sometimes a little different).
That makes it really difficult to write any kind of golden test for this to catch regressions - so if iOS changes the way that it resizes emoji in iOS vNext, we'll be slightly off again and need to repeat an expensive manual process to figure out what's going on.

@dnfield @gnprice wait for me on this one.

@dnfield look at text height
We should instruct Skia on how to change 5 properties based on text fontsize: 2 for the text (kerning and height) and 3 for emojis (kerning, fontsize, offset). If this is sth you can do, then we have a fix 😉
https://user-images.githubusercontent.com/53510751/236710562-24396546-430b-4554-880d-5f6ad8eead54.MOV

@delfme it would be helpful if you could share some code.
I think if we have to treat rendering Apple Color Emoji specially on iOS that's probably ok. We just need to know what the parameters are.

@dnfield was a bit swamped. Here is code extracted from a closed codebase https://github.com/delfme/text-widget
Inviting you there. Also inviting @jonahwilliams, @bdero, @chinmaygarde.
You guys can use it for an engine/skia fix if you want to. Have a nice Sunday.

@delfme by policy we're not allowed to look at closed source code. If you're interested in sharing, please make an example public with a permissive license. Thanks!

@jonahwilliams we have open and close code, just followed our policy. However, I see there is nothing there justifying it being close. Hope it helps https://github.com/delfme/text-widget
@dnfield idk skia/impeller, but on dart fix involves text properties too. Details inside Readme.

Thank you!

FYI @dnfield @jonahwilliams A guy raised this issue and made we think fontWeight should also be taken into account. https://github.com/delfme/text-widget/issues/1#issuecomment-1686523618

Sharing this to keep sponsoring a discussion around the rationale that flutter should go native for texfields.
This is flutter:
https://github.com/flutter/flutter/assets/53510751/c52389e4-90ae-4dd2-8042-225fc4174c4c

User notices: 1) non-native magnifier shape. 2) magnifier is also buggy coz doesnt display the underneath text in some cases. 3) non-native menu toolbar and text selection. UI 4) the emoji size issue 5) other known minor open issues related to either emojis or textfield/keyboard.
This is same screen in native:
https://github.com/flutter/flutter/assets/53510751/654d5ce1-3ee3-4474-911f-63d924953cc1

Thanks @delfme for keeping the conversation going.
This is such an important issue, so sad no one is working on it 😞.

An idea might be:

for anything unrelated to emojis and text eding (ie: otp code verification fields, form fields, search field) we would use flutter textfield
for anything related to emojis and text editing (ie. chat, note, posts) we would use flutter native_textfield

To everyone into/struggling with this issue, this is what we are going to do:

Remove flutter textfields everywhere but OTP code validation screen.
Use a wrapper to provide native text input. Note: on android system, Hybrid Composition is required, otherwise loupe/magnifer wont work for both Text and Texfield. We tested it and fortunately the performance is good (no delays or similar issues).
Use a wrapper to provide native Text everywhere a text is displayed && text selection is provided. So for example, there is no need to do that for chat bubbles inside a chatscreen (where we imagine we would have perf issues) or app labels.
use our widget text-widget to display Text if selection is not required but the text might have emojis inside it. Thanks to this widget, emojis will look native-like.

By adopting above points, user wont be hit by any non-native behaviour/UI and several pending text/emojis related bugs will be fixed as consequence.
Also, with the current level of performance provided by Impeller, it will be hard for users to notice that app is not native.
Hope this helps.

Sharing this to keep sponsoring a discussion around the rationale that flutter should go native for texfields.

@delfme Please file a new thread for that discussion. This thread is for a specific issue, which is that the size of emoji is wrong. There's useful technical progress above (starting at https://github.com/flutter/flutter/issues/28894#issuecomment-1526978974) on fixing this issue. When unrelated comments get added to the thread, it pushes that useful technical discussion up into the dreaded GitHub "hidden comments" section, which makes it harder for someone to work on this issue and get it fixed.
I just tried searching the tracker for an existing issue on using native text fields, and didn't find one. It's a recurring question and would be a good discussion to have — but again, not mixed up in this thread for this other, specific, issue. So a fresh issue thread for that idea would be great.
Please also review our guidelines for commenting in issue threads:
https://github.com/flutter/flutter/wiki/Issue-hygiene#comments

User notices: 1) non-native magnifier shape. 2) magnifier is also buggy coz doesnt display the underneath text in some cases. 3) non-native menu toolbar and text selection 4) the emoji wrong size and emojis wrong letter spacing issues 5) other known minor open issues related to either emojis or textfield/keyboard.

Similarly, if there are any of these that don't already have issues in the tracker, it would be great to have them filed with specifics. There are a number of known issues in this area, but many others have gotten fixed, and reporting the issue is the first step for that.

@gnprice to recap with proper focus on the issue, a good workaround while waiting for a fix, is:

For emoji inside text: adopt our widget mentioned here https://github.com/delfme/text-widget/issues/1#issuecomment-1686523618 . Previous solutions were not working due to wrong Regex formula and also because the fix itself required a more complex implementation
For emoji inside textfield, adopt a native textfield so that emoji size will be correct.

The combination of 1) and 2) will do the job.

Here is performance on iOS.
https://github.com/flutter/flutter/assets/53510751/5641b350-4b62-4deb-a290-95cd67f788e1
Hiding my previous comment since it refers to android.

@everyone found a workaround to issue https://github.com/flutter/flutter/issues/119623#issuecomment-1535669615
Sorry, but I have no time to add it to our public class but anyone who needs it can do it this way:

Wrap the text inside Padding, you can copy/paste this snippet.

WidgetSpan(
  baseline: TextBaseline.ideographic,
  alignment: PlaceholderAlignment.top,
  child: Builder(builder: (context) {
    return Transform.translate(
      //Emoji baseline will be off-center after increasing its size, Offset will
      //fix baseline to look native-like
      offset: Offset(0, emojiOffsetMultiplier),
      // Correct issue with emoji not being centered on iOS
      // https://github.com/flutter/flutter/issues/119623#issuecomment-1535669615
      child: Padding(
        padding: (Platform.isIOS) ?
            EdgeInsets.only(left:1)
            :
            EdgeInsets.only(left:0),
        child:Text(
        e,
        style: emojiStyle,
        maxLines: maxLines,
        overflow: overflow,
      ),),
    );
  }),
),

Now that you shifted the emoji to the right in order to fix above issue, you must compensate this change by reducing of 1px the letterSpacing inside text_widget_utils_IOS.dart.
This would be enough for most cases. But to those who also needs TextWidget to overflow with breaking-words (ie. chat bubbles UI), you must fix this issue too https://github.com/flutter/flutter/issues/61081. To do it, use runes to add an invisible white space breaking-word '\u200B'. Now text will overflow/wrap correctly but you will see a small space between chars. To compensate it, adjust letterSpacing inside text_widget_utils_IOS.dart (as in point 2).

Final result is close to native.
However, we found no way to workaround https://github.com/flutter/flutter/issues/120857 coz it is out of our reach.
If flutter team can fix it, we will finally have a real workaround for text on iOS meantime it becomes production-ready. Until that moment, we don't feel comfy to show our app to users.

🎉 I fixed also this
(https://github.com/flutter/flutter/issues/120857)
https://github.com/flutter/flutter/issues/120857#issuecomment-1762887590
Unorthodox and meh resource-wise, but it does the job. Should be added to TextWidget, see return Text.rich.
Hope this helps.

@dnfield wrote at https://github.com/flutter/flutter/issues/28894#issuecomment-1534111286:

If someone can figure out the exact function or mapping table that iOS is using, we could probably try to do something to emulate this.

It seems like CoreText provides the answer for this, if one calls CTFontGetAdvancesForGlyphs.
I'm taking that from the investigation @jason-simmons did in April on the related issue #119623, at https://github.com/flutter/flutter/issues/119623#issuecomment-1513908032: it turns out that Skia is calling that function to determine the advance. (And the mismatch between the scaled-up value it gets there, and the actual visual width of the emoji, then causes the emoji to appear off-center, which is #119623.)
In at least the example in that comment, a font size of 15 gives a width there of 19, which I believe agrees with the corresponding row in the table at https://github.com/flutter/flutter/issues/28894#issuecomment-1526978974 (though I'm a bit puzzled because all the heights in that table seem a factor of 2 larger than I'd expect from the discussion around it).
So perhaps that function can be used to drive this adjustment.
It'd still be a hack, for sure. But no more of a hack than it seems like Apple themselves are doing (based on the investigation at https://github.com/flutter/flutter/issues/28894#issuecomment-1526978974). And if in some future version of iOS they tweak the exact mapping of sizes, then using this CoreText function would probably be more robust to that change than if we reverse-engineered out an explicit mapping table — in order to get the text laid out correctly, they'd probably adjust that CoreText function to match.

Hello guys, would be great if we could fix this issue during next weeks.
As we know, wrong emojis size and off-center emojis https://github.com/flutter/flutter/issues/119623 is a real pain for Flutter apps, coz almost all texts typed in on a phone contain an emoji.
We provided a workaround here https://github.com/delfme/text-widget, along with a sample code that can be useful for testing fixes in a multiline scenario. In our case, several manipulations must be performed on the text and that affects performance if not done at engine level. Also, we noticed that textPainter.width provides a wrong result when emojis are inside the text, which means that we currently cannot determine the correct width of a text (which is a much needed func).
@gnprice does this https://github.com/flutter/flutter/issues/28894#issuecomment-1828630104 take into account the required changes to emojis Offset after emoji size is increased? Coz increasing the emoji fontSize inside a line must be compensated via Offset to avoid to break baseline. This is indeed the hack that Apple is doing. They also hack the emoji letterSpacing, coz user might type several emojis in a row like this: "😊😊😊" and that WidgetSpan uses a specific letterSpacing that varies based on fontSize.
@dnfield @jonahwilliams is there a reason why we cannot just replicate at engine level the fix from https://github.com/delfme/text-widget ?

Well, indeed performance are not bad in spite of the many computations. So lets consider vid below, as shown here https://github.com/delfme/text-widget
we:

Break text into spans to fetch emojs
Apply a custom style to emojis to increase fontSize and compensate the visual impact of it via dynamic offset and height.
Apply dynamic custom letterSpacing to make it looks native (coz emojis letterSpacing is also broken).
Add a left padding to fix issue https://github.com/flutter/flutter/issues/119623
Then paint two texts for each text, coz a foreground painter will add a stroke and fix this other key issue https://github.com/flutter/flutter/issues/120857

https://github.com/flutter/flutter/assets/53510751/04abddd2-196f-4ccc-a551-af7f9a2aa6b1
No idea whether these points can be added at engine level without causing janks. If not, at least there is this workaround. Note: workaround may be unperformant if texts are many. Also, those who will use it need to integrate a native textfield to uniform how texts look throughout the app.
Hope this help.

I would vote against this workaround, as it isn't a fix as much as it's a hack. It could be made as a package instead.
This is a fundamental issue related to Skia/Harfbuzz/CoreText's handling of all SBIX fonts, not just Apple's Emojis. This fix also does not cover expansions to the font, or different sbix fonts that do not have the same emoji patterns, so this would require both maintenance, and not completely fix the issues with all fonts.
It would be more productive for the flutter team to just ask for https://issues.skia.org/issues/40041310 (Almost four years old, prepare the cake!) to be fixed, as this would fix the issue for all SBIX fonts, as well as the same problem in Chrome (and everything depending on harfbuzz/skia).
Or we have to hope for Impeller to handle SBIX properly, I can't comment on that myself though.

Reproduces using the sample code in https://github.com/flutter/flutter/issues/28894#issuecomment-470507172

flutter doctor -v
[✓] Flutter (Channel stable, 3.19.5, on macOS 14.4.1 23E224 darwin-arm64, locale en-GB)
    • Flutter version 3.19.5 on channel stable at /Users/nexus/dev/sdks/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 300451adae (2 weeks ago), 2024-03-27 21:54:07 -0500
    • Engine revision e76c956498
    • Dart version 3.3.3
    • DevTools version 2.31.1

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at /Users/nexus/Library/Android/sdk
    • Platform android-34, build-tools 34.0.0
    • Java binary at: /Users/nexus/Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 17.0.7+0-17.0.7b1000.6-10550314)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 15.3)
    • Xcode at /Applications/Xcode-15.3.0.app/Contents/Developer
    • Build 15E204a
    • CocoaPods version 1.14.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2023.1)
    • Android Studio at /Users/nexus/Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.7+0-17.0.7b1000.6-10550314)

[✓] IntelliJ IDEA Ultimate Edition (version 2023.2.5)
    • IntelliJ at /Users/nexus/Applications/IntelliJ IDEA Ultimate.app
    • Flutter plugin version 77.2.2
    • Dart plugin version 232.10286

[✓] VS Code (version 1.87.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.86.0

[✓] Connected device (4 available)
    • Nexus (mobile)       • 00008020-001875E83A38002E • ios            • iOS 17.4.1 21E236
    • Dean’s iPad (mobile) • 00008103-000825C811E3401E • ios            • iOS 17.4.1 21E236
    • macOS (desktop)      • macos                     • darwin-arm64   • macOS 14.4.1 23E224 darwin-arm64
    • Chrome (web)         • chrome                    • web-javascript • Google Chrome 123.0.6312.107

[✓] Network resources
    • All expected network resources are available.

• No issues found!

[!] Flutter (Channel master, 3.22.0-8.0.pre.17, on macOS 14.4.1 23E224 darwin-arm64, locale en-GB)
    • Flutter version 3.22.0-8.0.pre.17 on channel master at /Users/nexus/dev/sdks/flutters
    ! Warning: `flutter` on your path resolves to /Users/nexus/dev/sdks/flutter/bin/flutter, which is not inside your current Flutter SDK checkout at /Users/nexus/dev/sdks/flutters. Consider adding /Users/nexus/dev/sdks/flutters/bin to the front of your path.
    ! Warning: `dart` on your path resolves to /Users/nexus/dev/sdks/flutter/bin/dart, which is not inside your current Flutter SDK checkout at /Users/nexus/dev/sdks/flutters. Consider adding /Users/nexus/dev/sdks/flutters/bin to the front of your path.
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 61cbe2f5b7 (6 hours ago), 2024-04-11 01:44:16 -0400
    • Engine revision fef8499fb9
    • Dart version 3.5.0 (build 3.5.0-47.0.dev)
    • DevTools version 2.34.1
    • If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly to perform update checks and upgrades.

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at /Users/nexus/Library/Android/sdk
    • Platform android-34, build-tools 34.0.0
    • Java binary at: /Users/nexus/Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 17.0.7+0-17.0.7b1000.6-10550314)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 15.3)
    • Xcode at /Applications/Xcode-15.3.0.app/Contents/Developer
    • Build 15E204a
    • CocoaPods version 1.14.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2023.1)
    • Android Studio at /Users/nexus/Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.7+0-17.0.7b1000.6-10550314)

[✓] IntelliJ IDEA Ultimate Edition (version 2023.2.5)
    • IntelliJ at /Users/nexus/Applications/IntelliJ IDEA Ultimate.app
    • Flutter plugin version 77.2.2
    • Dart plugin version 232.10286

[✓] VS Code (version 1.87.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.86.0

[✓] Connected device (5 available)
    • Nexus (mobile)                  • 00008020-001875E83A38002E • ios            • iOS 17.4.1 21E236
    • Dean’s iPad (mobile)            • 00008103-000825C811E3401E • ios            • iOS 17.4.1 21E236
    • macOS (desktop)                 • macos                     • darwin-arm64   • macOS 14.4.1 23E224 darwin-arm64
    • Mac Designed for iPad (desktop) • mac-designed-for-ipad     • darwin         • macOS 14.4.1 23E224 darwin-arm64
    • Chrome (web)                    • chrome                    • web-javascript • Google Chrome 123.0.6312.107

[✓] Network resources
    • All expected network resources are available.

! Doctor found issues in 1 category.

I've spent the last day or so looking at this. Here's what I can tell:

Skia is doing exactly what we ask it to - rendering an emoji from Apple Color Emoji at the exact font size we're asking for.
A simple program using CoreText shows the same behavior.
A simple program purely using Skia shows the same behavior.
On macOS, a SwiftUI "Text" widget shows the same behavior.
Safari and Chrome show the same behavior on macOS.

iOS seems to have some kind of special handling in Safari/Chrome/UIKit/SwiftUI, where AppleColorEmoji are scaled up a bit from what their normal size would be. I initially thought this was a multiplier of something like 125% (which is about what the scaling difference is when the font size is 16), but at larger sizes it goes away. I tried a test of rendering emoji at increasing sizes and the increase seems to be nearly linear for a 🤔 emoji:
Font Size	Glyph Height
11	28
12	30
13	32
14	34
15	38
16	40
17	40
18	40
19	41
20	42
21	43
22	44
23	46
24	48
25	50
26	52
27	54
28	56
29	56
30	58
You can see this in https://github.com/indjec/ui-label if you change the font size to something sufficiently large - there are still some spacing differences but the size difference is much less noticeable at larger font sizes.
I'm not quite sure what to do about this. One option would be to have some mode in Skia that lets us bump up the size a bit when the font is Apple Color Emoji and use that only on iOS. That's probably good enough to improve the fidelity. It's a bit disappointing that UIKit isn't open source to be able to see what it's doing with this or why it renders things any differently here. I tried finding relevant code in WebKit and couldn't.
Some sample programs:
A basic CoreText program that renders some Apple Color Emoji more-or-less the way Skia does to a png:
#include <Foundation/foundation.h>

#include <CoreFoundation/CoreFoundation.h>
#include <CoreGraphics/CoreGraphics.h>
#include <CoreText/CTFontManager.h>
#include <CoreText/CoreText.h>
#include <ImageIO/ImageIO.h>
#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h>

int main() {
  CTFontRef ct_font = CTFontCreateWithName((CFStringRef) @"Apple Color Emoji",
                                           (CGFloat)30, nullptr);

  if (!ct_font) {
    printf("Canot find Apple Color Emoji?\n");
    return -1;
  }

  CGGlyph glyphs[2];
  glyphs[0] = 1089;
  glyphs[1] = 2427;
  CGPoint positions[2];
  positions[0] = CGPointMake(50, 50);
  positions[1] = CGPointMake(90, 50);

  const CGBitmapInfo bitmapInfo =
      kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst;

  size_t width = 200;
  size_t height = 100;
  size_t row_bytes = width * sizeof(uint32_t);

  unsigned short image[height * row_bytes];

  CGContextRef context =
      CGBitmapContextCreate(image, width, height, 8, row_bytes,
                            CGColorSpaceCreateDeviceRGB(), bitmapInfo);

  CTFontDrawGlyphs(ct_font, glyphs, positions, 2, context);

  CGContextFlush(context);

  CGImageRef out_image = CGBitmapContextCreateImage(context);
  CFURLRef url =
      (__bridge CFURLRef)[[NSURL alloc]<EMAIL_ADDRESS>  CGImageDestinationRef destination = CGImageDestinationCreateWithURL(
      url, (__bridge CFStringRef)UTTypePNG.identifier, 1, nil);
  CGImageDestinationAddImage(destination, out_image, nil);
  CGImageDestinationFinalize(destination);
  CFRelease(destination);

  return 0;
}

A Skia test (only really makes sense on iOS/macOS):
    auto manager = SkFontMgr::RefDefault();
    auto aceType = manager->matchFamilyStyle("Apple Color Emoji", SkFontStyle());
    auto arialType = manager->matchFamilyStyle("Arial", SkFontStyle());

    auto fontSizes = {12, 16, 24, 30, 32, 64, 128};
    SkScalar y = 100;
    for (auto fontSize : fontSizes) {
        SkTextBlobBuilder textBlobBuilder;
        SkTextBlobBuilder textBlobBuilder2;
        SkFont aceFont(aceType, fontSize);
        SkFont arialFont(arialType, fontSize);

        const SkTextBlobBuilder::RunBuffer& run1 = textBlobBuilder.allocRun(arialFont, 4, 10, y);
        run1.glyphs[0] = 39;
        run1.glyphs[1] = 72;
        run1.glyphs[2] = 80;
        run1.glyphs[3] = 82;

        SkRect bounds[4];
        arialFont.getBounds(run1.glyphs, 4, bounds, nullptr);
        SkScalar advance = 0;
        for (size_t i = 0; i < 4; i++) {
            advance += bounds[i].width();
        }
        const SkTextBlobBuilder::RunBuffer& run2 =
                textBlobBuilder2.allocRun(aceFont, 2, advance + fontSize / 2, y);
        run2.glyphs[0] = 1089;
        run2.glyphs[1] = 2427;
        sk_sp<const SkTextBlob> blob = textBlobBuilder.make();
        sk_sp<const SkTextBlob> blob2 = textBlobBuilder2.make();

        SkPaint paint;
        paint.setColor(SK_ColorBLUE);
        canvas->drawTextBlob(blob.get(), 0, 0, paint);
        canvas->drawTextBlob(blob2.get(), 0, 0, paint);
        y += aceFont.getMetrics(nullptr) * 2;
    }

@dnfield why can't we adopt the approach you mentioned here with the discovery made in our repo ? https://github.com/delfme/text-widget
As mentioned, issue is size, kernel and offset (when text is multiline) but can't we just ask Skia to maniupalte those parameters as we do inside text-widget? In case, I can provide more precise numbers, we kept tuning things up.
I kindly disagree with those who mentioned they'd rather not fix issue this way coz it looks like a patch. I mean, meantime there is no perfect fix, it is way better to have something for the users. Also, several text-related issues are intertwined. For example, I lately filed this https://github.com/flutter/flutter/issues/151952 where I think textPainter might be returning width values that are not very precise when emojis are inside text (prolly due to emojis' wrong kernel and size).
One last consideration is about iphone users: they notice UI fidelity issue in one second, and are quick to mark an app like "cheap" or uninstall it.