16

Canvas.drawText() doesn't render emojis above a certain font size on Android.

Correct render at somewhere below 256 px: emoji correctly rendered

Incorrect render at above 256 px: enter image description here

(There is a similar question about Google Chrome, and just like Android, Chrome also uses the Skia graphics library, so this seems like a bug in Skia.)

Apparently emojis fail to render above 256 px font size on my devices. But I'm not sure if this is the limit everywhere.

Is there a way to learn the font size at which emojis disappear? Or is there a workaround for this?

Attila Tanyi
  • 4,904
  • 5
  • 27
  • 34
  • 4
    Not only `Canvas.drawText()` but also `TextView` can't draw large emojis. I also tested myself and found out 256 was the max size but didn't know about the bug in Skia. Now it makes sense, thanks! – Jenix Nov 26 '18 at 20:52
  • Also fails when rendering a text via the StaticLayout and DynamicLayout classes. For my own purposes, in case is of any use to you, as workaround instead of rendering the text directly, I draw it first into a smaller bitmap, and then render the smaller bitmap into the final one scaling as required. – PerracoLabs Nov 19 '20 at 14:34

1 Answers1

10

I've come up with a test (an empirical estimation) for the maximum font size at which emojis can still be rendered.

The way this function works is it creates a 1x1 bitmap, and tries to draw the Earth Globe emoji () in the center of that. Then it checks that single pixel, whether it's still transparent, or colored.

I've chosen the Earth Globe emoji because I assume we can be fairly certain that no artist would ever draw the Earth with a hole in the middle. (Or we're in huge trouble anyway.)

The tests are done in a binary search - fashion, so the runtime should be logarithmic.

(Fun fact: the max font size on both my test phones turned out to be 256.)

public static float getMaxEmojiFontSize() {
    return getMaxEmojiFontSize(new Paint(), 8, 999999, 1);
}

/**
 * Emojis cannot be renderered above a certain font size due to a bug.
 * This function tries to estimate what the maximum font size is where emojis can still
 * be rendered.
 * @param   p           A Paint object to do the testing with.
 *                      A simple `new Paint()` should do.
 * @param   minTestSize From what size should we test if the emojis can be rendered.
 *                      We're assuming that at this size, emojis can be rendered.
 *                      A good value for this is 8.
 * @param   maxTestSize Until what size should we test if the emojis can be rendered.
 *                      This can be the max font size you're planning to use.
 * @param   maxError    How close should we be to the actual number with our estimation.
 *                      For example, if this is 10, and the result from this function is
 *                      240, then the maximum font size that still renders is guaranteed
 *                      to be under 250. (If maxTestSize is above 250.)
 */
public static float getMaxEmojiFontSize(Paint p, float minTestSize, float maxTestSize, float maxError) {
    Bitmap b = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
    float sizeLowerLimit = minTestSize; // Start testing from this size
    float sizeUpperLimit = maxTestSize;
    Canvas c = new Canvas(b);
    float size;
    for (size = sizeLowerLimit; size < maxTestSize; size *= 2) {
        if (!canRenderEmoji(b, c, p, size)) {
            sizeUpperLimit = size;
            break;
        }
        sizeLowerLimit = size;
    }
    // We now have a lower and upper limit for the maximum emoji size.
    // Let's proceed with a binary search.
    while (sizeUpperLimit - sizeLowerLimit > maxError) {
        float middleSize = (sizeUpperLimit + sizeLowerLimit) / 2f;
        if (!canRenderEmoji(b, c, p, middleSize)) {
            sizeUpperLimit = middleSize;
        } else {
            sizeLowerLimit = middleSize;
        }
    }
    return sizeLowerLimit;
}

private static boolean canRenderEmoji(Bitmap b, Canvas can, Paint p, float size) {
    final String EMOJI = "\uD83C\uDF0D"; // the Earth Globe (Europe, Africa) emoji - should never be transparent in the center.
    can.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); // clear the canvas with transparent
    p.setTextSize(size);
    { // draw the emoji in the center
        float ascent = Math.abs(p.ascent());
        float descent = Math.abs(p.descent());
        float halfHeight = (ascent + descent) / 2.0f;
        p.setTextAlign(Paint.Align.CENTER);
        can.drawText(EMOJI, 0.5f, 0.5f + halfHeight - descent, p);
    }
    return b.getPixel(0, 0) != 0;
}
Attila Tanyi
  • 4,904
  • 5
  • 27
  • 34
  • 1
    Update: the max font size seems to be 256 consistently all the way up to Android 10. From Android 11, it seems to jump to thousands (6000+ in some of my tests, but not the same value on different devices - maybe related to the device screen size?). The emoji is rendered properly (although it's quite blurry at that size). The above algorithm still works, but it may take a few seconds to run, especially with a low maxError like in the example. – Attila Tanyi Sep 01 '21 at 22:37