Emoji size is off on iOS #28894
Emoji size is off on iOS #28894
Comments
|
/cc @GaryQian @goderbauer |
|
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;
}
}
|
|
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 You will need to be careful with forced strut-line heights however, as this will allow glyphs to overlap if pushed to the extreme. |
|
@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 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
forceStrutHeight solves The EmojiTextSpan solves I believe something framework level will be required for |
|
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. |
|
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. |
|
@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. |
|
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>
<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> |
|
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 emojis&text rendered in Safari and the same 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 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
}
}); |
|
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),
],
),
),
],
);
}
} |
|
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. |
|
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? 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. |
|
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 // 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 |
|
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 regex and make it match everything both in emoji and what apple's emoji font covers. |
|
@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)? There should be 3241 elements that should be inflated (so far, looking on my macbook) |
|
@miyoyo this
|
|
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),
),
);
}
}(if the regex complains, maybe add a |
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 all happen to be 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 tested EmojiTextSpan and think it works quite well.
|
Previewcomplete code sampleimport '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! |











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:

Native Android:

Flutter
(Bottom is Flutter out-of-the-box, top is my massaged Flutter RichText version.)
The baseline is still not correct but its much better.
This was achieved like so:
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
The text was updated successfully, but these errors were encountered: