content: package com.airbnb.lottie.model.layer;
  
  import android.graphics.Canvas;
  import android.graphics.Color;
  import android.graphics.Matrix;
  import android.graphics.Paint;
  import android.graphics.Path;
  import android.graphics.PointF;
  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;
  import com.airbnb.lottie.TextDelegate;
  import com.airbnb.lottie.animation.content.ContentGroup;
  import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
  import com.airbnb.lottie.animation.keyframe.TextKeyframeAnimation;
  import com.airbnb.lottie.animation.keyframe.ValueCallbackKeyframeAnimation;
  import com.airbnb.lottie.model.DocumentData;
  import com.airbnb.lottie.model.Font;
  import com.airbnb.lottie.model.FontCharacter;
  import com.airbnb.lottie.model.animatable.AnimatableTextProperties;
  import com.airbnb.lottie.model.content.ShapeGroup;
  import com.airbnb.lottie.model.content.TextRangeUnits;
  import com.airbnb.lottie.utils.DropShadow;
  import com.airbnb.lottie.utils.Utils;
  import com.airbnb.lottie.value.LottieValueCallback;
  
  import java.text.Bidi;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  public class TextLayer extends BaseLayer {
  
    // 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 StringBuilder charStringBuilder = new StringBuilder(0);
    private final StringBuilder reorderingStringBuilder = new StringBuilder(0);
    private final StringBuilder reversingStringBuilder = new StringBuilder(0);
    private final RectF rectF = new RectF();
    private final Matrix matrix = new Matrix();
    private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG) {{
      setStyle(Style.FILL);
    }};
    private final Paint strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG) {{
      setStyle(Style.STROKE);
    }};
    private final Map<FontCharacter, List<ContentGroup>> contentsForCharacter = new HashMap<>();
    private final LongSparseArray<String> codePointCache = new LongSparseArray<>();
    private final List<String> charStrings = new ArrayList<>();
    /**
     * If this is paragraph text, one line may wrap depending on the size of the document data box.
     */
    private final List<TextSubLine> textSubLines = new ArrayList<>();
    private final TextKeyframeAnimation textAnimation;
    private final LottieDrawable lottieDrawable;
    private final LottieComposition composition;
    private TextRangeUnits textRangeUnits = TextRangeUnits.INDEX;
    @Nullable
    private BaseKeyframeAnimation<Integer, Integer> colorAnimation;
    @Nullable
    private BaseKeyframeAnimation<Integer, Integer> colorCallbackAnimation;
    @Nullable
    private BaseKeyframeAnimation<Integer, Integer> strokeColorAnimation;
    @Nullable
    private BaseKeyframeAnimation<Integer, Integer> strokeColorCallbackAnimation;
    @Nullable
    private BaseKeyframeAnimation<Float, Float> strokeWidthAnimation;
    @Nullable
    private BaseKeyframeAnimation<Float, Float> strokeWidthCallbackAnimation;
    @Nullable
    private BaseKeyframeAnimation<Float, Float> trackingAnimation;
    @Nullable
    private BaseKeyframeAnimation<Float, Float> trackingCallbackAnimation;
    @Nullable
    private BaseKeyframeAnimation<Integer, Integer> opacityAnimation;
    @Nullable
    private BaseKeyframeAnimation<Float, Float> textSizeCallbackAnimation;
    @Nullable
    private BaseKeyframeAnimation<Typeface, Typeface> typefaceCallbackAnimation;
    @Nullable
    private BaseKeyframeAnimation<Integer, Integer> textRangeStartAnimation;
    @Nullable
    private BaseKeyframeAnimation<Integer, Integer> textRangeEndAnimation;
    @Nullable
    private BaseKeyframeAnimation<Integer, Integer> textRangeOffsetAnimation;
  
    TextLayer(LottieDrawable lottieDrawable, Layer layerModel) {
      super(lottieDrawable, layerModel);
      this.lottieDrawable = lottieDrawable;
      composition = layerModel.getComposition();
      //noinspection ConstantConditions
      textAnimation = layerModel.getText().createAnimation();
      textAnimation.addUpdateListener(this);
      addAnimation(textAnimation);
  
      AnimatableTextProperties textProperties = layerModel.getTextProperties();
      if (textProperties != null && textProperties.textStyle != null && textProperties.textStyle.color != null) {
        colorAnimation = textProperties.textStyle.color.createAnimation();
        colorAnimation.addUpdateListener(this);
        addAnimation(colorAnimation);
      }
  
      if (textProperties != null && textProperties.textStyle != null && textProperties.textStyle.stroke != null) {
        strokeColorAnimation = textProperties.textStyle.stroke.createAnimation();
        strokeColorAnimation.addUpdateListener(this);
        addAnimation(strokeColorAnimation);
      }
  
      if (textProperties != null && textProperties.textStyle != null && textProperties.textStyle.strokeWidth != null) {
        strokeWidthAnimation = textProperties.textStyle.strokeWidth.createAnimation();
        strokeWidthAnimation.addUpdateListener(this);
        addAnimation(strokeWidthAnimation);
      }
  
      if (textProperties != null && textProperties.textStyle != null && textProperties.textStyle.tracking != null) {
        trackingAnimation = textProperties.textStyle.tracking.createAnimation();
        trackingAnimation.addUpdateListener(this);
        addAnimation(trackingAnimation);
      }
  
      if (textProperties != null && textProperties.textStyle != null && textProperties.textStyle.opacity != null) {
        opacityAnimation = textProperties.textStyle.opacity.createAnimation();
        opacityAnimation.addUpdateListener(this);
        addAnimation(opacityAnimation);
      }
  
      if (textProperties != null && textProperties.rangeSelector != null && textProperties.rangeSelector.start != null) {
        textRangeStartAnimation = textProperties.rangeSelector.start.createAnimation();
        textRangeStartAnimation.addUpdateListener(this);
        addAnimation(textRangeStartAnimation);
      }
  
      if (textProperties != null && textProperties.rangeSelector != null && textProperties.rangeSelector.end != null) {
        textRangeEndAnimation = textProperties.rangeSelector.end.createAnimation();
        textRangeEndAnimation.addUpdateListener(this);
        addAnimation(textRangeEndAnimation);
      }
  
      if (textProperties != null && textProperties.rangeSelector != null && textProperties.rangeSelector.offset != null) {
        textRangeOffsetAnimation = textProperties.rangeSelector.offset.createAnimation();
        textRangeOffsetAnimation.addUpdateListener(this);
        addAnimation(textRangeOffsetAnimation);
      }
  
      if (textProperties != null && textProperties.rangeSelector != null) {
        textRangeUnits = textProperties.rangeSelector.units;
      }
    }
  
    @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, @Nullable DropShadow shadowToApply /* ignored for now */) {
      DocumentData documentData = textAnimation.getValue();
      Font font = composition.getFonts().get(documentData.fontName);
      if (font == null) {
        return;
      }
      canvas.save();
      canvas.concat(parentMatrix);
  
      configurePaint(documentData, parentAlpha, 0);
  
      if (lottieDrawable.useTextGlyphs()) {
        drawTextWithGlyphs(documentData, parentMatrix, font, canvas, parentAlpha);
      } else {
        drawTextWithFont(documentData, font, canvas, parentAlpha);
      }
  
      canvas.restore();
    }
  
    /**
     * Configures the [fillPaint] and [strokePaint] used for drawing based on currently active text ranges.
     *
     * @param parentAlpha A value from 0 to 255 indicating the alpha of the parented layer.
     */
    private void configurePaint(DocumentData documentData, int parentAlpha, int indexInDocument) {
      if (colorCallbackAnimation != null) { // dynamic property takes priority
        fillPaint.setColor(colorCallbackAnimation.getValue());
      } else if (colorAnimation != null && isIndexInRangeSelection(indexInDocument)) {
        fillPaint.setColor(colorAnimation.getValue());
      } else { // fall back to the document color
        fillPaint.setColor(documentData.color);
      }
  
      if (strokeColorCallbackAnimation != null) {
        strokePaint.setColor(strokeColorCallbackAnimation.getValue());
      } else if (strokeColorAnimation != null && isIndexInRangeSelection(indexInDocument)) {
        strokePaint.setColor(strokeColorAnimation.getValue());
      } else {
        strokePaint.setColor(documentData.strokeColor);
      }
  
      // These opacity values are in the range 0 to 100
      int transformOpacity = transform.getOpacity() == null ? 100 : transform.getOpacity().getValue();
      int textRangeOpacity = opacityAnimation != null && isIndexInRangeSelection(indexInDocument) ? opacityAnimation.getValue() : 100;
  
      // This alpha value needs to be in the range 0 to 255 to be applied to the Paint instances.
      // We map the layer transform's opacity into that range and multiply it by the fractional opacity of the text range and the parent.
      int alpha = Math.round((transformOpacity * 255f / 100f)
          * (textRangeOpacity / 100f)
          * parentAlpha / 255f);
      fillPaint.setAlpha(alpha);
      strokePaint.setAlpha(alpha);
  
      if (strokeWidthCallbackAnimation != null) {
        strokePaint.setStrokeWidth(strokeWidthCallbackAnimation.getValue());
      } else if (strokeWidthAnimation != null && isIndexInRangeSelection(indexInDocument)) {
        strokePaint.setStrokeWidth(strokeWidthAnimation.getValue());
      } else {
        strokePaint.setStrokeWidth(documentData.strokeWidth * Utils.dpScale());
      }
    }
  
    private boolean isIndexInRangeSelection(int indexInDocument) {
      int textLength = textAnimation.getValue().text.length();
      if (textRangeStartAnimation != null && textRangeEndAnimation != null) {
        // After effects supports reversed text ranges where the start index is greater than the end index.
        // For the purposes of determining if the given index is inside of the range, we take the start as the smaller value.
        int rangeStart = Math.min(textRangeStartAnimation.getValue(), textRangeEndAnimation.getValue());
        int rangeEnd = Math.max(textRangeStartAnimation.getValue(), textRangeEndAnimation.getValue());
  
        if (textRangeOffsetAnimation != null) {
          int offset = textRangeOffsetAnimation.getValue();
          rangeStart += offset;
          rangeEnd += offset;
        }
  
        if (textRangeUnits == TextRangeUnits.INDEX) {
          return indexInDocument >= rangeStart && indexInDocument < rangeEnd;
        } else {
          float currentIndexAsPercent = indexInDocument / (float) textLength * 100;
          return currentIndexAsPercent >= rangeStart && currentIndexAsPercent < rangeEnd;
        }
      }
      return true;
    }
  
    private void drawTextWithGlyphs(
        DocumentData documentData, Matrix parentMatrix, Font font, Canvas canvas, int parentAlpha) {
      float textSize;
      if (textSizeCallbackAnimation != null) {
        textSize = textSizeCallbackAnimation.getValue();
      } else {
        textSize = documentData.size;
      }
      float fontScale = textSize / 100f;
      float parentScale = Utils.getScale(parentMatrix);
  
      String text = documentData.text;
  
      // Split full text in multiple lines
      List<String> textLines = getTextLines(text);
      int textLineCount = textLines.size();
      // Add tracking
      float tracking = documentData.tracking / 10f;
      if (trackingCallbackAnimation != null) {
        tracking += trackingCallbackAnimation.getValue();
      } else if (trackingAnimation != null) {
        tracking += trackingAnimation.getValue();
      }
      int lineIndex = -1;
      for (int i = 0; i < textLineCount; i++) {
        String textLine = textLines.get(i);
        float boxWidth = documentData.boxSize == null ? 0f : documentData.boxSize.x;
        List<TextSubLine> lines = splitGlyphTextIntoLines(textLine, boxWidth, font, fontScale, tracking, true);
        for (int j = 0; j < lines.size(); j++) {
          TextSubLine line = lines.get(j);
          lineIndex++;
  
          canvas.save();
  
          if (offsetCanvas(canvas, documentData, lineIndex, line.width)) {
            drawGlyphTextLine(line.text, documentData, font, canvas, parentScale, fontScale, tracking, parentAlpha);
          }
  
          canvas.restore();
        }
      }
    }
  
    private void drawGlyphTextLine(String text, DocumentData documentData,
        Font font, Canvas canvas, float parentScale, float fontScale, float tracking, int parentAlpha) {
      for (int i = 0; i < text.length(); i++) {
        char c = text.charAt(i);
        int characterHash = FontCharacter.hashFor(c, font.getFamily(), font.getStyle());
        FontCharacter character = composition.getCharacters().get(characterHash);
        if (character == null) {
          // Something is wrong. Potentially, they didn't export the text as a glyph.
          continue;
        }
        drawCharacterAsGlyph(character, fontScale, documentData, canvas, i, parentAlpha);
        float tx = (float) character.getWidth() * fontScale * Utils.dpScale() + tracking;
        canvas.translate(tx, 0);
      }
    }
  
    private void drawTextWithFont(DocumentData documentData, Font font, Canvas canvas, int parentAlpha) {
      Typeface typeface = getTypeface(font);
      if (typeface == null) {
        return;
      }
      String text = documentData.text;
      TextDelegate textDelegate = lottieDrawable.getTextDelegate();
      if (textDelegate != null) {
        text = textDelegate.getTextInternal(getName(), text);
      }
      fillPaint.setTypeface(typeface);
      float textSize;
      if (textSizeCallbackAnimation != null) {
        textSize = textSizeCallbackAnimation.getValue();
      } else {
        textSize = documentData.size;
      }
      fillPaint.setTextSize(textSize * Utils.dpScale());
      strokePaint.setTypeface(fillPaint.getTypeface());
      strokePaint.setTextSize(fillPaint.getTextSize());
  
      // Calculate tracking
      float tracking = documentData.tracking / 10f;
      if (trackingCallbackAnimation != null) {
        tracking += trackingCallbackAnimation.getValue();
      } else if (trackingAnimation != null) {
        tracking += trackingAnimation.getValue();
      }
      tracking = tracking * Utils.dpScale() * textSize / 100.0f;
  
      // Split full text in multiple lines
      List<String> textLines = getTextLines(text);
      int textLineCount = textLines.size();
      int lineIndex = -1;
      int characterIndexAtStartOfLine = 0;
      for (int i = 0; i < textLineCount; i++) {
        String textLine = textLines.get(i);
        float boxWidth = documentData.boxSize == null ? 0f : documentData.boxSize.x;
        List<TextSubLine> lines = splitGlyphTextIntoLines(textLine, boxWidth, font, 0f, tracking, false);
        for (int j = 0; j < lines.size(); j++) {
          TextSubLine line = lines.get(j);
          lineIndex++;
  
          canvas.save();
  
          float lineWidth =
              (textAnimation == null &&
                  textSizeCallbackAnimation == null &&
                  trackingCallbackAnimation == null) ?
                  line.width :
                  fillPaint.measureText(line.text);
          if (offsetCanvas(canvas, documentData, lineIndex, lineWidth)) {
            String currentLineText = line.text;
            if (Bidi.requiresBidi(currentLineText.toCharArray(), 0, currentLineText.length())) {
              currentLineText = reorderLineVisually(currentLineText);
            }
            drawFontTextLine(currentLineText, documentData, canvas, tracking, characterIndexAtStartOfLine, parentAlpha);
          }
  
          characterIndexAtStartOfLine += line.text.length();
  
          canvas.restore();
        }
      }
    }
  
    private boolean offsetCanvas(Canvas canvas, DocumentData documentData, int lineIndex, float lineWidth) {
      PointF position = documentData.boxPosition;
      PointF size = documentData.boxSize;
      float dpScale = Utils.dpScale();
      float lineStartY = position == null ? 0f : documentData.lineHeight * dpScale + position.y;
      float lineOffset = (lineIndex * documentData.lineHeight * dpScale) + lineStartY;
      if (lottieDrawable.getClipTextToBoundingBox() && size != null && position != null && lineOffset >= position.y + size.y + documentData.size) {
        return false;
      }
      float lineStart = position == null ? 0f : position.x;
      float boxWidth = size == null ? 0f : size.x;
      switch (documentData.justification) {
        case LEFT_ALIGN:
          canvas.translate(lineStart, lineOffset);
          break;
        case RIGHT_ALIGN:
          canvas.translate(lineStart + boxWidth - lineWidth, lineOffset);
          break;
        case CENTER:
          canvas.translate(lineStart + boxWidth / 2f - lineWidth / 2f, lineOffset);
          break;
      }
      return true;
    }
  
    @Nullable
    private Typeface getTypeface(Font font) {
      if (typefaceCallbackAnimation != null) {
        Typeface callbackTypeface = typefaceCallbackAnimation.getValue();
        if (callbackTypeface != null) {
          return callbackTypeface;
        }
      }
      Typeface drawableTypeface = lottieDrawable.getTypeface(font);
      if (drawableTypeface != null) {
        return drawableTypeface;
      }
      return font.getTypeface();
    }
  
    private List<String> getTextLines(String text) {
      // Split full text by carriage return character
      String formattedText = text.replaceAll("\r\n", "\r")
          .replaceAll("\u0003", "\r")
          .replaceAll("\n", "\r");
      String[] textLinesArray = formattedText.split("\r");
      return Arrays.asList(textLinesArray);
    }
  
    /**
     * @param characterIndexAtStartOfLine The index within the overall document of the character at the start of the line
     * @param parentAlpha
     */
    private void drawFontTextLine(String text,
        DocumentData documentData,
        Canvas canvas,
        float tracking,
        int characterIndexAtStartOfLine,
        int parentAlpha) {
      charStrings.clear();
      for (int i = 0; i < text.length(); ) {
        String charString = codePointToString(text, i);
        charStrings.add(charString);
        i += charString.length();
      }
  
      for (int i = 0; i < charStrings.size(); ) {
        charStringBuilder.setLength(0);
        charStringBuilder.append(charStrings.get(i));
        int nextIndex = i + 1;
        while (nextIndex < charStrings.size()) {
          String nextCharString = charStrings.get(nextIndex);
          if (isJoiningRightToLeft(nextCharString)) {
            // We insert at the front explicitly, string builder will actually add it to the end visually since it is rtl
            charStringBuilder.insert(0, nextCharString);
            nextIndex++;
          } else {
            break;
          }
        }
  
        String charString = charStringBuilder.toString();
        drawCharacterFromFont(charString, documentData, canvas, characterIndexAtStartOfLine + i, parentAlpha);
        float charWidth = fillPaint.measureText(charString);
        float tx = charWidth + tracking;
        canvas.translate(tx, 0);
  
        i = nextIndex;
      }
    }
  
    /**
     * Transform string stored in logical order to visual order.
     * For example, if an RTL language is stored as qwert in memory, it will become trewq,
     * so we can keep the logic of drawing each character from left to right afterward.
     *
     * @param text the original string
     * @return the text in visual order, written from left to right
     */
    private String reorderLineVisually(String text) {
      Bidi bidi = new Bidi(text, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT);
      int runCount = bidi.getRunCount();
      byte[] runLevels = new byte[runCount];
      Integer[] runs = new Integer[runCount];
      for (int i = 0; i < runCount; i++) {
        runLevels[i] = (byte) bidi.getRunLevel(i);
        runs[i] = i;
      }
      Bidi.reorderVisually(runLevels, 0, runs, 0, runCount);
  
      reorderingStringBuilder.setLength(0);
      for (int i = 0; i < runCount; i++) {
        int originalIndex = runs[i];
        int runStart = bidi.getRunStart(originalIndex);
        int runLimit = bidi.getRunLimit(originalIndex);
        // Even run level means LTR, odd means RTL
        int runLevel = bidi.getRunLevel(originalIndex);
  
        String word = text.substring(runStart, runLimit);
        if ((runLevel & 1) == 0) {
          reorderingStringBuilder.append(word);
        } else {
          reversingStringBuilder.setLength(0);
          for (int j = 0; j < word.length();) {
            String charString = codePointToString(word, j);
            reversingStringBuilder.insert(0, charString);
            j += charString.length();
          }
          reorderingStringBuilder.append(reversingStringBuilder);
        }
      }
  
      return reorderingStringBuilder.toString();
    }
  
    private List<TextSubLine> splitGlyphTextIntoLines(String textLine, float boxWidth, Font font, float fontScale, float tracking,
        boolean usingGlyphs) {
      int lineCount = 0;
  
      float currentLineWidth = 0;
      int currentLineStartIndex = 0;
  
      int currentWordStartIndex = 0;
      float currentWordWidth = 0f;
      boolean nextCharacterStartsWord = false;
  
      // The measured size of a space.
      float spaceWidth = 0f;
  
      for (int i = 0; i < textLine.length(); i++) {
        char c = textLine.charAt(i);
        float currentCharWidth;
        if (usingGlyphs) {
          int characterHash = FontCharacter.hashFor(c, font.getFamily(), font.getStyle());
          FontCharacter character = composition.getCharacters().get(characterHash);
          if (character == null) {
            continue;
          }
          currentCharWidth = (float) character.getWidth() * fontScale * Utils.dpScale() + tracking;
        } else {
          currentCharWidth = fillPaint.measureText(textLine.substring(i, i + 1)) + tracking;
        }
  
        if (c == ' ') {
          spaceWidth = currentCharWidth;
          nextCharacterStartsWord = true;
        } else if (nextCharacterStartsWord) {
          nextCharacterStartsWord = false;
          currentWordStartIndex = i;
          currentWordWidth = currentCharWidth;
        } else {
          currentWordWidth += currentCharWidth;
        }
        currentLineWidth += currentCharWidth;
  
        if (boxWidth > 0f && currentLineWidth >= boxWidth) {
          if (c == ' ') {
            // Spaces at the end of a line don't do anything. Ignore it.
            // The next non-space character will hit the conditions below.
            continue;
          }
          TextSubLine subLine = ensureEnoughSubLines(++lineCount);
          if (currentWordStartIndex == currentLineStartIndex) {
            // Only word on line is wider than box, start wrapping mid-word.
            String substr = textLine.substring(currentLineStartIndex, i);
            String trimmed = substr.trim();
            float trimmedSpace = (trimmed.length() - substr.length()) * spaceWidth;
            subLine.set(trimmed, currentLineWidth - currentCharWidth - trimmedSpace);
            currentLineStartIndex = i;
            currentLineWidth = currentCharWidth;
            currentWordStartIndex = currentLineStartIndex;
            currentWordWidth = currentCharWidth;
          } else {
            String substr = textLine.substring(currentLineStartIndex, currentWordStartIndex - 1);
            String trimmed = substr.trim();
            float trimmedSpace = (substr.length() - trimmed.length()) * spaceWidth;
            subLine.set(trimmed, currentLineWidth - currentWordWidth - trimmedSpace - spaceWidth);
            currentLineStartIndex = currentWordStartIndex;
            currentLineWidth = currentWordWidth;
          }
        }
      }
      if (currentLineWidth > 0f) {
        TextSubLine line = ensureEnoughSubLines(++lineCount);
        line.set(textLine.substring(currentLineStartIndex), currentLineWidth);
      }
      return textSubLines.subList(0, lineCount);
    }
  
    /**
     * Elements are reused and not deleted to save allocations.
     */
    private TextSubLine ensureEnoughSubLines(int numLines) {
      for (int i = textSubLines.size(); i < numLines; i++) {
        textSubLines.add(new TextSubLine());
      }
      return textSubLines.get(numLines - 1);
    }
  
    private void drawCharacterAsGlyph(
        FontCharacter character,
        float fontScale,
        DocumentData documentData,
        Canvas canvas,
        int indexInDocument,
        int parentAlpha) {
      configurePaint(documentData, parentAlpha, indexInDocument);
      List<ContentGroup> contentGroups = getContentsForCharacter(character);
      for (int j = 0; j < contentGroups.size(); j++) {
        Path path = contentGroups.get(j).getPath();
        path.computeBounds(rectF, false);
        matrix.reset();
        matrix.preTranslate(0, -documentData.baselineShift * Utils.dpScale());
        matrix.preScale(fontScale, fontScale);
        path.transform(matrix);
        if (documentData.strokeOverFill) {
          drawGlyph(path, fillPaint, canvas);
          drawGlyph(path, strokePaint, canvas);
        } else {
          drawGlyph(path, strokePaint, canvas);
          drawGlyph(path, fillPaint, canvas);
        }
      }
    }
  
    private void drawGlyph(Path path, Paint paint, Canvas canvas) {
      if (paint.getColor() == Color.TRANSPARENT) {
        return;
      }
      if (paint.getStyle() == Paint.Style.STROKE && paint.getStrokeWidth() == 0) {
        return;
      }
      canvas.drawPath(path, paint);
    }
  
    private void drawCharacterFromFont(String character, DocumentData documentData, Canvas canvas, int indexInDocument, int parentAlpha) {
      configurePaint(documentData, parentAlpha, indexInDocument);
      if (documentData.strokeOverFill) {
        drawCharacter(character, fillPaint, canvas);
        drawCharacter(character, strokePaint, canvas);
      } else {
        drawCharacter(character, strokePaint, canvas);
        drawCharacter(character, fillPaint, 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, character.length(), 0, 0, paint);
    }
  
    private List<ContentGroup> getContentsForCharacter(FontCharacter character) {
      if (contentsForCharacter.containsKey(character)) {
        return contentsForCharacter.get(character);
      }
      List<ShapeGroup> shapes = character.getShapes();
      int size = shapes.size();
      List<ContentGroup> contents = new ArrayList<>(size);
      for (int i = 0; i < size; i++) {
        ShapeGroup sg = shapes.get(i);
        contents.add(new ContentGroup(lottieDrawable, this, sg, composition));
      }
      contentsForCharacter.put(character, contents);
      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.DIRECTIONALITY_NONSPACING_MARK ||
          Character.getType(codePoint) == Character.SURROGATE;
    }
  
    /**
     * Whether a character is joining (connecting, such as ت + ت should become تت), and is written from right to left.
     * This is used for determining if we should render a group of characters together,
     * and we will pass them to the renderer to pick the correct joining glyph.
     *
     * @param stringChar a string that preferably contains only one visual character
     * @return true if any character in the input is joining, and is written from right to left
     */
    private boolean isJoiningRightToLeft(String stringChar) {
      for (int i = 0; i < stringChar.length(); i++) {
        int codePoint = stringChar.codePointAt(i);
        if (Character.getDirectionality(codePoint) == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) {
          return true;
        }
      }
      return false;
    }
  
    @SuppressWarnings("unchecked")
    @Override
    public <T> void addValueCallback(T property, @Nullable LottieValueCallback<T> callback) {
      super.addValueCallback(property, callback);
      if (property == LottieProperty.COLOR) {
        if (colorCallbackAnimation != null) {
          removeAnimation(colorCallbackAnimation);
        }
  
        if (callback == null) {
          colorCallbackAnimation = null;
        } else {
          colorCallbackAnimation = new ValueCallbackKeyframeAnimation<>((LottieValueCallback<Integer>) callback);
          colorCallbackAnimation.addUpdateListener(this);
          addAnimation(colorCallbackAnimation);
        }
      } else if (property == LottieProperty.STROKE_COLOR) {
        if (strokeColorCallbackAnimation != null) {
          removeAnimation(strokeColorCallbackAnimation);
        }
  
        if (callback == null) {
          strokeColorCallbackAnimation = null;
        } else {
          strokeColorCallbackAnimation = new ValueCallbackKeyframeAnimation<>((LottieValueCallback<Integer>) callback);
          strokeColorCallbackAnimation.addUpdateListener(this);
          addAnimation(strokeColorCallbackAnimation);
        }
      } else if (property == LottieProperty.STROKE_WIDTH) {
        if (strokeWidthCallbackAnimation != null) {
          removeAnimation(strokeWidthCallbackAnimation);
        }
  
        if (callback == null) {
          strokeWidthCallbackAnimation = null;
        } else {
          strokeWidthCallbackAnimation = new ValueCallbackKeyframeAnimation<>((LottieValueCallback<Float>) callback);
          strokeWidthCallbackAnimation.addUpdateListener(this);
          addAnimation(strokeWidthCallbackAnimation);
        }
      } else if (property == LottieProperty.TEXT_TRACKING) {
        if (trackingCallbackAnimation != null) {
          removeAnimation(trackingCallbackAnimation);
        }
  
        if (callback == null) {
          trackingCallbackAnimation = null;
        } else {
          trackingCallbackAnimation = new ValueCallbackKeyframeAnimation<>((LottieValueCallback<Float>) callback);
          trackingCallbackAnimation.addUpdateListener(this);
          addAnimation(trackingCallbackAnimation);
        }
      } else if (property == LottieProperty.TEXT_SIZE) {
        if (textSizeCallbackAnimation != null) {
          removeAnimation(textSizeCallbackAnimation);
        }
  
        if (callback == null) {
          textSizeCallbackAnimation = null;
        } else {
          textSizeCallbackAnimation = new ValueCallbackKeyframeAnimation<>((LottieValueCallback<Float>) callback);
          textSizeCallbackAnimation.addUpdateListener(this);
          addAnimation(textSizeCallbackAnimation);
        }
      } else if (property == LottieProperty.TYPEFACE) {
        if (typefaceCallbackAnimation != null) {
          removeAnimation(typefaceCallbackAnimation);
        }
  
        if (callback == null) {
          typefaceCallbackAnimation = null;
        } else {
          typefaceCallbackAnimation = new ValueCallbackKeyframeAnimation<>((LottieValueCallback<Typeface>) callback);
          typefaceCallbackAnimation.addUpdateListener(this);
          addAnimation(typefaceCallbackAnimation);
        }
      } else if (property == LottieProperty.TEXT) {
        textAnimation.setStringValueCallback((LottieValueCallback<String>) callback);
      }
    }
  
    private static class TextSubLine {
  
      private String text = "";
      private float width = 0f;
  
      void set(String text, float width) {
        this.text = text;
        this.width = width;
      }
    }
  }
  