Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emoji size is off on iOS #28894

Open
lukepighetti opened this issue Mar 5, 2019 · 58 comments
Open

Emoji size is off on iOS #28894

lukepighetti opened this issue Mar 5, 2019 · 58 comments

Comments

@lukepighetti
Copy link

@lukepighetti lukepighetti commented Mar 5, 2019

Emojis still do not render properly on Flutter. They are not the correct height or baseline. This has been an issue since the beginning and it makes any kind of social text experience look low quality and poor in comparison to alternatives.

Native iOS:
img_6efd59636937-1

Native Android:
screenshot_2019-03-05-17-49-45-591_com google android keep

Flutter

(Bottom is Flutter out-of-the-box, top is my massaged Flutter RichText version.)

image-1

The baseline is still not correct but its much better.

This was achieved like so:

  TextStyle get textStyle => TextStyle(
        fontSize: 16,
        height: 1.2,
        color: Colors.black,
      );

  TextStyle get emojiStyle => TextStyle(
        fontSize: 20,
        height: 0.5,
      );

RichText(
      textAlign: TextAlign.left,
      text: TextSpan(
        style: textStyle,
        text: "I am ",
        children: <TextSpan>[
          TextSpan(text: "I am fascinated by how Flutter renders emojis. "),
          TextSpan(text: "🤣🤣🤣", style: emojiStyle),
          TextSpan(
              text: " It seems to make them smaller than the text"
                  " and cause lines to shift up/down."),
        ],
      ),
    )

The bigger issue is that the user has to maintain massive Regex blobs and parse out strings into RichText when the framework should just render these properly from the beginning.

Any insight is appreciated.

@willlarche

@dnfield
Copy link
Member

@dnfield dnfield commented Mar 5, 2019

/cc @GaryQian @goderbauer

@dnfield dnfield added this to the Goals milestone Mar 5, 2019
@dnfield dnfield added the a: fidelity label Mar 5, 2019
@lukepighetti lukepighetti changed the title Flutter still doesn't render Emoji's properly Flutter still doesn't render Emojis properly Mar 6, 2019
@lukepighetti
Copy link
Author

@lukepighetti lukepighetti commented Mar 6, 2019

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;
  }
}
@GaryQian
Copy link
Contributor

@GaryQian GaryQian commented Mar 6, 2019

You can try to use the new feature of 'Strut' to lock line heights. Android/iOS 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.

You will need to be careful with forced strut-line heights however, as this will allow glyphs to overlap if pushed to the extreme.

@lukepighetti
Copy link
Author

@lukepighetti lukepighetti commented Mar 6, 2019

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

@GaryQian
Copy link
Contributor

@GaryQian GaryQian commented Mar 6, 2019

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
  ),
)
@lukepighetti
Copy link
Author

@lukepighetti lukepighetti commented Mar 6, 2019

@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 components as a reference which render emojis approximately 25% larger fontSize than the surrounding text while lowering the vertical alignment.

There are three attributes to Emojis that are not handled by Flutter currently

  1. emoji size 25% greater than text size
  2. line heights locked
  3. vertical align below center

forceStrutHeight solves #2,

The EmojiTextSpan solves #1 and #2.

I believe something framework level will be required for #1 ,#2 and #3.

@GaryQian
Copy link
Contributor

@GaryQian GaryQian commented Mar 6, 2019

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.

@lukepighetti
Copy link
Author

@lukepighetti lukepighetti commented Mar 6, 2019

I think they handle them as a specific case because its better for user experience and arguably worth the effort. If you pull open your Twitter timeline you'll notice this immediately, especially when compared to Flutter text widgets. Thanks for all the work you do.

@lukepighetti
Copy link
Author

@lukepighetti lukepighetti commented Mar 6, 2019

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

Also, upon further inspection it looks like iOS is more like 30% instead of 25%. I haven't played around on Android enough to know what they do.

@GaryQian
Copy link
Contributor

@GaryQian GaryQian commented Mar 6, 2019

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.

@GaryQian
Copy link
Contributor

@GaryQian GaryQian commented Mar 6, 2019

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.

@lukepighetti
Copy link
Author

@lukepighetti lukepighetti commented Mar 6, 2019

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

@dnfield
Copy link
Member

@dnfield dnfield commented Mar 6, 2019

@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?

@lukepighetti
Copy link
Author

@lukepighetti lukepighetti commented Mar 6, 2019

I used this HTML in Safari to get Native text rendering on iOS.

<!DOCTYPE html>
<html lang="en">
  <head>
    <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>
  </head>
  <body>
    <div>
      <span style="font-size: 8px">8px jjJJ</span>
      <span style="font-size: 16px">🤔jjJJ 16px</span>
    </div>
    <div>
      <span style="font-size: 16px">16px jjJJ🤔</span>
      <span style="font-size: 8px">jjJJ 8px</span>
    </div>
    <div>
      <span style="font-size: 8px">8px j</span>
      <span style="font-size: 12px">j</span>
      <span style="font-size: 16px">j</span>
      <span style="font-size: 20px">j</span>
      <span style="font-size: 24px">j 24px</span>
    </div>
    <div>
      <span style="font-size: 8px">8px 🤔</span>
      <span style="font-size: 12px">🤔</span>
      <span style="font-size: 16px">🤔</span>
      <span style="font-size: 20px">🤔</span>
      <span style="font-size: 24px">🤔 24px</span>
    </div>
    <div>
      <span style="font-size: 8px">8px jJ🤔</span>
      <span style="font-size: 12px">jJ🤔</span>
      <span style="font-size: 16px">jJ🤔</span>
      <span style="font-size: 20px">jJ🤔</span>
      <span style="font-size: 24px">jJ🤔 24px</span>
    </div>
  </body>
</html>

img_981cf0371cf4-1

@GaryQian
Copy link
Contributor

@GaryQian GaryQian commented Mar 7, 2019

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.

@lukepighetti
Copy link
Author

@lukepighetti lukepighetti commented Mar 7, 2019

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 emojis&text rendered in Safari and the same rendered elsewhere on iOS.

@jonahwilliams
Copy link
Member

@jonahwilliams jonahwilliams commented Mar 7, 2019

@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.

@lukepighetti
Copy link
Author

@lukepighetti lukepighetti commented Mar 7, 2019

Here it is in React Native on iOS 12 (XS Max simulator) and Android 8.1 (Pixel 2 simulator)

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.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
  }
});

simulator screen shot - iphone xs - 2019-03-07 at 07 05 09

screenshot_1551960508

@lukepighetti
Copy link
Author

@lukepighetti lukepighetti commented Mar 7, 2019

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),
            ],
          ),
        ),
      ],
    );
  }
}

simulator screen shot - iphone xs - 2019-03-07 at 07 28 35

@lukepighetti
Copy link
Author

@lukepighetti lukepighetti commented Mar 7, 2019

Side by side comparison (iOS). No resizing.

screen shot 2019-03-07 at 7 33 51 am

@miyoyo
Copy link

@miyoyo miyoyo commented Mar 7, 2019

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

emojisize

@GaryQian
Copy link
Contributor

@GaryQian GaryQian commented Mar 8, 2019

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.

@dnfield dnfield removed the framework label Mar 8, 2019
@Hixie
Copy link
Member

@Hixie Hixie commented Feb 12, 2020

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.

@lukef
Copy link
Contributor

@lukef lukef commented Feb 12, 2020

I definitely know we won't have time to make a font to demonstrate this issue. That's a pretty tall ask, no? I don't know, I've never made one. 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. You can attach whatever priority you want.

@Hixie
Copy link
Member

@Hixie Hixie commented Feb 12, 2020

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.

@nicogranuja
Copy link

@nicogranuja nicogranuja commented Feb 21, 2020

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),
      ),
    );
  }
}
@jmagman jmagman added this to Engineer Reviewed in Mobile - iOS fidelity review Feb 25, 2020
@jmagman jmagman moved this from Engineer Reviewed to Awaiting Triage in Mobile - iOS fidelity review Feb 25, 2020
@EliasDeuss
Copy link

@EliasDeuss EliasDeuss commented Apr 12, 2020

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. @nicogranuja

@miyoyo
Copy link

@miyoyo miyoyo commented Apr 12, 2020

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.

Edit: I think this is indeed the case, as EmojiOne in TextEdit on MacOS does seem to render at different sizes.
In red, Apple Color Emoji
In green, EmojiOne
In blue, no Emoji.
Screenshot 2020-04-13 at 01 23 42

And here's an example of chrome (word online) vs textedit
Screenshot 2020-04-13 at 01 33 24

@droidluv
Copy link

@droidluv droidluv commented Apr 22, 2020

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!

@miyoyo
Copy link

@miyoyo miyoyo commented Apr 22, 2020

Add a plus on the regex and make it match everything both in emoji and what apple's emoji font covers.

@droidluv
Copy link

@droidluv droidluv commented Apr 22, 2020

@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])

@miyoyo
Copy link

@miyoyo miyoyo commented Apr 22, 2020

((?:\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)?

There should be 3241 elements that should be inflated (so far, looking on my macbook)

@miyoyo
Copy link

@miyoyo miyoyo commented Apr 22, 2020

((?:\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, \ufe0f , which the previous regex does not include.

Reproducible example: ☁️

This is a cloud, with an alternative variation (variationless is colorless)

On regex101 without the variation selector
image

On regex101 with the variation selector

image

@droidluv
Copy link

@droidluv droidluv commented Apr 22, 2020

@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

@miyoyo
Copy link

@miyoyo miyoyo commented Apr 22, 2020

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

@lukepighetti
Copy link
Author

@lukepighetti lukepighetti commented Apr 23, 2020

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

@miyoyo
Copy link

@miyoyo miyoyo commented Apr 23, 2020

@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),
      ),
    );
  }
}

(if the regex complains, maybe add a r in front of "((?:\u00a9|\u00ae|[\u2000-\u3300]|\ufe0f|[\ud83c-\ud83e][\udc00-\udfff]|\udb40[\udc61-\udc7f])+)")

@droidluv
Copy link

@droidluv droidluv commented Apr 23, 2020

@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

@lukepighetti
Copy link
Author

@lukepighetti lukepighetti commented Apr 24, 2020

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 all happen to be on Android haha.

@miguelpruivo
Copy link

@miguelpruivo miguelpruivo commented Jun 1, 2020

@lukaspili did you find any workaround for this?

@ventr1x
Copy link

@ventr1x ventr1x commented Aug 12, 2020

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
      )
    ];
  }
}

@TahaTesser
Copy link
Member

@TahaTesser TahaTesser commented Oct 22, 2020

Preview

Screenshot 2020-10-22 at 5 45 03 PM

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!
@gaaclarke gaaclarke moved this from Awaiting Triage to Engineer Reviewed in Mobile - iOS fidelity review Oct 26, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Mobile - iOS fidelity review
  
Engineer Reviewed
Linked pull requests

Successfully merging a pull request may close this issue.

None yet