Added support for emoji in TextDelegates (#1150)

diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt b/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt
index d4775ba..842b6e3 100644
--- a/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt
@@ -104,6 +104,7 @@
             testDynamicProperties()
             testMarkers()
             snapshotAssets()
+            testText()
             snapshotProdAnimations()
             snapshotter.finalizeReportAndUpload()
         }
@@ -171,6 +172,7 @@
         bitmapPool.release(bitmap)
     }
 
+    @ObsoleteCoroutinesApi
     private suspend fun snapshotAssets() = coroutineScope {
         val assetsChannel = listAssets()
         val compositionsChannel = parseCompositionsFromAssets(assetsChannel)
@@ -587,6 +589,116 @@
         }
     }
 
+    private suspend fun testText() {
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "Hello World") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "Hello World")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "Emoji") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "🔥💪💯")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "Taiwanese") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "我的密碼")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "Fire Taiwanese") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "🔥的A")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "Family man man girl boy") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC67\u200D\uD83D\uDC66")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "Family woman woman girl girl") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "Brown Police Man") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "\uD83D\uDC6E\uD83C\uDFFF\u200D♀️")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "Family and Brown Police Man") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC67\u200D\uD83D\uDC67\uD83D\uDC6E\uD83C\uDFFF\u200D♀️")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "Family, Brown Police Man, emoji and chars") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "🔥\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC67\u200D\uD83D\uDC67\uD83D\uDC6E\uD83C\uDFFF\u200D♀的Aabc️")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "Fire English Fire Brown Police Man Fire") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "🔥c️🔥\uD83D\uDC6E\uD83C\uDFFF\u200D♀️\uD83D\uDD25")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "American Flag") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "\uD83C\uDDFA\uD83C\uDDF8")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "Checkered Flag") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "\uD83C\uDFC1")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "Pirate Flag") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "\uD83C\uDFF4\u200D☠️")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "3 Oclock") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "\uD83D\uDD52")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "Woman frowning") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "\uD83D\uDE4D\u200D♀️")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "Gay couple") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "\uD83D\uDC68\u200D❤️\u200D\uD83D\uDC68️")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "Lesbian couple") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "\uD83D\uDC69\u200D❤️\u200D\uD83D\uDC69️")
+        }
+
+        withAnimationView("Tests/DynamicText.json", "Dynamic Text", "Straight couple") { animationView ->
+            val textDelegate = TextDelegate(animationView)
+            animationView.setTextDelegate(textDelegate)
+            textDelegate.setText("NAME", "\uD83D\uDC91")
+        }
+    }
+
     private suspend fun withDrawable(assetName: String, snapshotName: String, snapshotVariant: String, callback: (LottieDrawable) -> Unit) {
         val result = LottieCompositionFactory.fromAssetSync(activity, assetName)
         val composition = result.value
diff --git a/LottieSample/src/main/assets/Tests/DynamicText.json b/LottieSample/src/main/assets/Tests/DynamicText.json
new file mode 100644
index 0000000..fad053c
--- /dev/null
+++ b/LottieSample/src/main/assets/Tests/DynamicText.json
@@ -0,0 +1,2 @@
+{"v":"4.8.0","fr":29.9700012207031,"ip":0,"op":61.0000024845809,"w":150,"h":150,"nm":"Name",
+  "ddd":0,"assets":[],"fonts":{"list":[{"origin":0,"fPath":"","fClass":"","fFamily":"Comic Neue","fWeight":"","fStyle":"Regular","fName":"ComicNeue","ascent":69.6990966796875}]},"layers":[{"ddd":0,"ind":1,"ty":5,"nm":"NAME","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[10.5,15,0],"e":[10.5,147,0],"to":[0,22,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":15,"s":[10.5,147,0],"e":[10.5,15,0],"to":[0,0,0],"ti":[0,22,0]},{"t":30.0000012219251}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[100,100,100],"e":[196,196,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":15,"s":[196,196,100],"e":[100,100,100]},{"t":30.0000012219251}]}},"ao":0,"t":{"d":{"k":[{"s":{"s":14,"f":"ComicNeue","t":"NAME","j":0,"tr":0,"lh":16.8,"ls":0,"fc":[0.92,0,0]},"t":0}]},"p":{},"m":{"g":1,"a":{"a":0,"k":[0,0]}},"a":[{"s":{"t":0,"xe":{"a":0,"k":0},"ne":{"a":0,"k":0},"a":{"a":0,"k":100},"b":1,"rn":0,"sh":1,"r":1},"a":{"fc":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[1,0,0,1],"e":[0,1,0,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":15,"s":[0,1,0,1],"e":[0,0,1,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":30,"s":[0,0,1,1],"e":[0,1,0,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":45,"s":[0,1,0,1],"e":[1,0,0,1]},{"t":60.0000024438501}]},"t":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":30,"s":[0],"e":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":45,"s":[20],"e":[0]},{"t":60.0000024438501}]}}}]},"ip":0,"op":61.0000024845809,"st":0,"bm":0,"sr":1}]}
\ No newline at end of file
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/layer/TextLayer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/TextLayer.java
index 44df146..ab45531 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/layer/TextLayer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/TextLayer.java
@@ -8,6 +8,7 @@
 import android.graphics.RectF;
 import android.graphics.Typeface;
 import androidx.annotation.Nullable;
+import androidx.collection.LongSparseArray;
 import com.airbnb.lottie.LottieComposition;
 import com.airbnb.lottie.LottieDrawable;
 import com.airbnb.lottie.LottieProperty;
@@ -23,6 +24,7 @@
 import com.airbnb.lottie.model.content.ShapeGroup;
 import com.airbnb.lottie.utils.Utils;
 import com.airbnb.lottie.value.LottieValueCallback;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -30,7 +32,9 @@
 import java.util.Map;
 
 public class TextLayer extends BaseLayer {
-  private final char[] tempCharArray = new char[1];
+  // Capacity is 2 because emojis are 2 characters. Some are longer in which case, the capacity will
+  // be expanded but that should be pretty rare.
+  private final StringBuilder stringBuilder = new StringBuilder(2);
   private final RectF rectF = new RectF();
   private final Matrix matrix = new Matrix();
   private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG) {{
@@ -40,13 +44,18 @@
     setStyle(Style.STROKE);
   }};
   private final Map<FontCharacter, List<ContentGroup>> contentsForCharacter = new HashMap<>();
+  private final LongSparseArray<String> codePointCache = new LongSparseArray<String>();
   private final TextKeyframeAnimation textAnimation;
   private final LottieDrawable lottieDrawable;
   private final LottieComposition composition;
-  @Nullable private BaseKeyframeAnimation<Integer, Integer> colorAnimation;
-  @Nullable private BaseKeyframeAnimation<Integer, Integer> strokeColorAnimation;
-  @Nullable private BaseKeyframeAnimation<Float, Float> strokeWidthAnimation;
-  @Nullable private BaseKeyframeAnimation<Float, Float> trackingAnimation;
+  @Nullable
+  private BaseKeyframeAnimation<Integer, Integer> colorAnimation;
+  @Nullable
+  private BaseKeyframeAnimation<Integer, Integer> strokeColorAnimation;
+  @Nullable
+  private BaseKeyframeAnimation<Float, Float> strokeWidthAnimation;
+  @Nullable
+  private BaseKeyframeAnimation<Float, Float> trackingAnimation;
 
   TextLayer(LottieDrawable lottieDrawable, Layer layerModel) {
     super(lottieDrawable, layerModel);
@@ -83,13 +92,15 @@
     }
   }
 
-  @Override public void getBounds(RectF outBounds, Matrix parentMatrix, boolean applyParents) {
+  @Override
+  public void getBounds(RectF outBounds, Matrix parentMatrix, boolean applyParents) {
     super.getBounds(outBounds, parentMatrix, applyParents);
     // TODO: use the correct text bounds.
     outBounds.set(0, 0, composition.getBounds().width(), composition.getBounds().height());
   }
 
-  @Override void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
+  @Override
+  void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
     canvas.save();
     if (!lottieDrawable.useTextGlyphs()) {
       canvas.setMatrix(parentMatrix);
@@ -171,7 +182,7 @@
   }
 
   private void drawGlyphTextLine(String text, DocumentData documentData, Matrix parentMatrix,
-                                 Font font, Canvas canvas, float parentScale, float fontScale) {
+      Font font, Canvas canvas, float parentScale, float fontScale) {
     for (int i = 0; i < text.length(); i++) {
       char c = text.charAt(i);
       int characterHash = FontCharacter.hashFor(c, font.getFamily(), font.getStyle());
@@ -245,11 +256,11 @@
   }
 
   private void drawFontTextLine(String text, DocumentData documentData, Canvas canvas, float parentScale) {
-    for (int i = 0; i < text.length(); i++) {
-      char character = text.charAt(i);
-      drawCharacterFromFont(character, documentData, canvas);
-      tempCharArray[0] = character;
-      float charWidth = fillPaint.measureText(tempCharArray, 0, 1);
+    for (int i = 0; i < text.length(); ) {
+      String charString = codePointToString(text, i);
+      i += charString.length();
+      drawCharacterFromFont(charString, documentData, canvas);
+      float charWidth = fillPaint.measureText(charString, 0, 1);
       // Add tracking
       float tracking = documentData.tracking / 10f;
       if (trackingAnimation != null) {
@@ -323,25 +334,24 @@
     canvas.drawPath(path, paint);
   }
 
-  private void drawCharacterFromFont(char c, DocumentData documentData, Canvas canvas) {
-    tempCharArray[0] = c;
+  private void drawCharacterFromFont(String character, DocumentData documentData, Canvas canvas) {
     if (documentData.strokeOverFill) {
-      drawCharacter(tempCharArray, fillPaint, canvas);
-      drawCharacter(tempCharArray, strokePaint, canvas);
+      drawCharacter(character, fillPaint, canvas);
+      drawCharacter(character, strokePaint, canvas);
     } else {
-      drawCharacter(tempCharArray, strokePaint, canvas);
-      drawCharacter(tempCharArray, fillPaint, canvas);
+      drawCharacter(character, strokePaint, canvas);
+      drawCharacter(character, fillPaint, canvas);
     }
   }
 
-  private void drawCharacter(char[] character, Paint paint, Canvas canvas) {
+  private void drawCharacter(String character, Paint paint, Canvas canvas) {
     if (paint.getColor() == Color.TRANSPARENT) {
       return;
     }
     if (paint.getStyle() == Paint.Style.STROKE && paint.getStrokeWidth() == 0) {
       return;
     }
-    canvas.drawText(character, 0, 1, 0, 0, paint);
+    canvas.drawText(character, 0, character.length(), 0, 0, paint);
   }
 
   private List<ContentGroup> getContentsForCharacter(FontCharacter character) {
@@ -359,6 +369,44 @@
     return contents;
   }
 
+  private String codePointToString(String text, int startIndex) {
+    int firstCodePoint = text.codePointAt(startIndex);
+    int firstCodePointLength = Character.charCount(firstCodePoint);
+    int key = firstCodePoint;
+    int index = startIndex + firstCodePointLength;
+    while (index < text.length()) {
+      int nextCodePoint = text.codePointAt(index);
+      if (!isModifier(nextCodePoint)) {
+        break;
+      }
+      int nextCodePointLength = Character.charCount(nextCodePoint);
+      index += nextCodePointLength;
+      key = key * 31 + nextCodePoint;
+    }
+
+    if (codePointCache.containsKey(key)) {
+      return codePointCache.get(key);
+    }
+
+    stringBuilder.setLength(0);
+    for (int i = startIndex; i < index; ) {
+      int codePoint = text.codePointAt(i);
+      stringBuilder.appendCodePoint(codePoint);
+      i += Character.charCount(codePoint);
+    }
+    String str = stringBuilder.toString();
+    codePointCache.put(key, str);
+    return str;
+  }
+
+  private boolean isModifier(int codePoint) {
+    return Character.getType(codePoint) == Character.FORMAT ||
+        Character.getType(codePoint) == Character.MODIFIER_SYMBOL ||
+        Character.getType(codePoint) == Character.NON_SPACING_MARK ||
+        Character.getType(codePoint) == Character.OTHER_SYMBOL ||
+        Character.getType(codePoint) == Character.SURROGATE;
+  }
+
   @SuppressWarnings("unchecked")
   @Override
   public <T> void addValueCallback(T property, @Nullable LottieValueCallback<T> callback) {