让你的 Lottie 支持文字区域内自动换行

让你的 Lottie 支持文字区域内自动换行

老规矩,先说背景和结果,再说解决过程。如果和你遇到的问题一样,再看后边的解决过程吧

前言

最近遇到一个棘手的问题,设计同学给了一个lottie 动画,动画内有三个字段可以动态替换,如图:
在这里插入图片描述
但是其中的 inputTag在替换时,出现了一些问题

  1. 问题一 :长文本不会自动换行
    长文本错误

  2. 问题二:多行文本未在区域内展示
    多行文本错误

发现其实设计已经设置了 input 图层的文字区域范围,理论上来讲应该可以自动换行才对,于是我在lottie 官网预览了一下,发现却是正常的
官网预览

经过一番苦战,最后解决了这个问题,如果和你遇到的问题,就接着往后看吧、如果你需要直接的解决方案,可以直接看二、解决问题

一、分析问题

根据上边看到的问题一二,不难看出

  1. 没有解析到 textLayer 的正确显示范围
  2. 同时也没有再显示范围内自动换行
分析 json 源数据

json字段解释

可以看到,设计妹子给的json是没问题的,的确有包含图层尺寸以及偏移量等数据,那么真相只有一个 - lottie 解析库并没有解析处理 sz 、 ps 值

查看源码

直接定位到 TextLayer 对文字的绘制部分

private void drawTextWithFont(
        DocumentData documentData, Font font, Matrix parentMatrix, Canvas canvas) {
  ... // 上边是一些值的读取处理,我们不管他

  // 处理多行及绘制
  // Split full text in multiple lines
  List<String> textLines = getTextLines(text);
  int textLineCount = textLines.size();
  for (int l = 0; l < textLineCount; l++) {

    String textLine = textLines.get(l);
    float textLineWidth = strokePaint.measureText(textLine);

    // Apply horizontal justification
    applyJustification(documentData.justification, canvas, textLineWidth);

    // Center text vertically
    float multilineTranslateY = (textLineCount - 1) * lineHeight / 2;
    float translateY = l * lineHeight - multilineTranslateY;
    canvas.translate(0, translateY);

    // Draw each line
    drawFontTextLine(textLine, documentData, canvas, parentScale);

    // Reset canvas
    canvas.setMatrix(parentMatrix);
  }
}

getTextLines() ?

private List<String> getTextLines(String text) {
  // Split full text by carriage return character
  String formattedText = text.replaceAll("\r\n", "\r")
          .replaceAll("\n", "\r");
  String[] textLinesArray = formattedText.split("\r");
  return Arrays.asList(textLinesArray);
}

这样也叫 获取多行文本嘛?怪不得我的长文本只有一行,原来是看不到换行符不回头啊。等等,再看看 json 的解析,会不会压根没有解析sz 、 ps两个字段的值

查看 document 生成源码 DocumentDataParser

public class DocumentDataParser implements ValueParser<DocumentData> {
  public static final DocumentDataParser INSTANCE = new DocumentDataParser();
  // 解析的字段名
  private static final JsonReader.Options NAMES = JsonReader.Options.of(
          "t",
          "f",
          "s",
          "j",
          "tr",
          "lh",
          "ls",
          "fc",
          "sc",
          "sw",
          "of"
  );

  private DocumentDataParser() {
  }

  @Override
  public DocumentData parse(JsonReader reader, float scale) throws IOException {
    String text = null;
    String fontName = null;
    float size = 0f;
    Justification justification = Justification.CENTER;
    int tracking = 0;
    float lineHeight = 0f;
    float baselineShift = 0f;
    int fillColor = 0;
    int strokeColor = 0;
    float strokeWidth = 0f;
    boolean strokeOverFill = true;

    reader.beginObject();
    while (reader.hasNext()) {
      switch (reader.selectName(NAMES)) {
          ... // 都是已有的跟多行无关的
        default:
          reader.skipName();
          reader.skipValue();
      }
    }
    reader.endObject();

    return new DocumentData(text, fontName, size, justification, tracking, lineHeight,
            baselineShift, fillColor, strokeColor, strokeWidth, strokeOverFill);
  }
}

可以看到,果然不出我们所料,压根没有解析 sz、ps 字段值,更无需谈处理的事儿了

二、解决问题

经过我们缜密的分析,那么我们需要做的事就比较明确了,大概包括以下几个步骤:

  1. 补充 sz、ps 字段的解析
  2. 重写 getTextLines() 方法,正确获取多行文本
  3. 重写 drawTextWithFont() 方法,正确计算 align 偏移

OK,那么逐个模块来解决问题:

1. 补充 sz、ps 字段的解析
public class DocumentDataParser implements ValueParser<DocumentData> {
  public static final DocumentDataParser INSTANCE = new DocumentDataParser();
  // 解析的字段名
  private static final JsonReader.Options NAMES = JsonReader.Options.of(
          "t",
          "f",
          "s",
          "j",
          "tr",
          "lh",
          "ls",
          "fc",
          "sc",
          "sw",
          "of",
          // 补充两个字段名
          "sz",
          "ps"
  );

  private DocumentDataParser() {
  }

  @Override
  public DocumentData parse(JsonReader reader, float scale) throws IOException {
    String text = null;
    String fontName = null;
    float size = 0f;
    Justification justification = Justification.CENTER;
    int tracking = 0;
    float lineHeight = 0f;
    float baselineShift = 0f;
    int fillColor = 0;
    int strokeColor = 0;
    float strokeWidth = 0f;
    boolean strokeOverFill = true;
    // 补充 尺寸
    double[] viewSize = new double[] {-1, -1};
    // 补充 偏移
    double[] offset = new double[2];

    reader.beginObject();
    while (reader.hasNext()) {
      switch (reader.selectName(NAMES)) {
          ... // 都是已有的跟多行无关的
        // 补充对这两个字段的解析
        case 11:
          JsonUtils.jsonToArray(reader, viewSize);
          break;
        case 12:
          JsonUtils.jsonToArray(reader, offset);
          break;
        default:
          reader.skipName();
          reader.skipValue();
      }
    }
    reader.endObject();

    // 加入构造方法
    return new DocumentData(text, fontName, size, justification, tracking, lineHeight,
            baselineShift, fillColor, strokeColor, strokeWidth, strokeOverFill, viewSize, offset);
  }
}
  • 其中 JsonUtils 中的方法为
  static void jsonToArray(JsonReader reader, double[] array) {
    try {
      reader.beginArray();
      for (int i = 0; i < array.length; i++) {
        array[i] = reader.nextDouble();
      }
      while (reader.hasNext()) {
        reader.skipValue();
      }
      reader.endArray();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
  • 在 DocumentData 中补充这两个字段
public class DocumentData {

  ... 其他属性
  public final float[] viewSize;
  public final float[] offset;

  public DocumentData(String text, String fontName, float size, Justification justification, int tracking,
                      float lineHeight, float baselineShift, @ColorInt int color, @ColorInt int strokeColor,
                      float strokeWidth, boolean strokeOverFill, double[] viewSize, double[] offset) {
    ... 其他属性
    // 存储 viewSize,因为 json 中的 为 dp 值,这里我们要转为 像素值
    this.viewSize = new float[]{(float) (viewSize[0] * Utils.dpScale()), (float) (viewSize[1] * Utils.dpScale())};
    // 存储 偏移
    this.offset = new float[]{(float) (offset[0]), (float) (offset[1])};
  }
  ...
}
2. 重写 getTextLines() 方法,正确获取多行文本

为了方便我们实现长文本的多行实现,这里使用 StaticLayout 来做文本多行测量

  • 在 TextLayer中补充方法 obtainStaticLayout()
private StaticLayout obtainStaticLayout(DocumentData documentData, String text) {
    double maxWidth = documentData.viewSize[0];
    TextPaint paint = new TextPaint(fillPaint);
    return Utils.getStaticLayout(text, paint, ((int) maxWidth), documentData);
}
  • 其中 Utils.getStaticLayout 为
public static StaticLayout getStaticLayout(String text, TextPaint paint, int width, DocumentData documentData) {
    if (width < 0) {
      return null;
    }
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      return StaticLayout.Builder.obtain(text, 0, text.length(), paint, width)
              .setLineSpacing(0, documentData.lineHeight / documentData.size)
              .build();
    } else {
      return new StaticLayout(
              text, paint, width,
              Layout.Alignment.ALIGN_NORMAL,
              documentData.lineHeight / documentData.size,
              0f,
              true);
    }
}

然后重写 getTextLines方法

private List<String> getTextLines(String text, DocumentData documentData, StaticLayout sl) {
  if (documentData.viewSize[0] < 0) {
    // Split full text by carriage return character
    // 未设置尺寸的还是沿用以前的实现方案
    String formattedText = text.replaceAll("\r\n", "\r")
            .replaceAll("\n", "\r");
    String[] textLinesArray = formattedText.split("\r");
    return Arrays.asList(textLinesArray);
  }
  double maxHeight = documentData.viewSize[1];
  List<String> lines = new LinkedList<>();
  int line = 0;
  // 把每行可显示的文字逐行获取出来
  while (line < sl.getLineCount() && sl.getLineTop(line) < maxHeight) {
    int lineStart = sl.getLineStart(line);
    int lineEnd = sl.getLineEnd(line);
    lines.add(text.substring(lineStart, lineEnd));
    line++;
  }
  return lines;
}
3. 重写 drawTextWithFont() 方法,正确计算 align 偏移

最后,重写 drawTextWithFont 中的部分代码,来使我们的改动生效

private void drawTextWithFont(
        DocumentData documentData, Font font, Matrix parentMatrix, Canvas canvas) {
  ... // 上边是一些值的读取处理,我们不管他

  // 处理多行及绘制
  // Split full text in multiple lines
    StaticLayout sl = obtainStaticLayout(documentData, text);
    List<String> textLines = getTextLines(text, documentData, sl);
    int textLineCount = textLines.size();
    // 计算实际的首行位置
    float textLayerHeight = getRealTextLayerHeight(sl, documentData, lineHeight, textLineCount);
    for (int l = 0; l < textLineCount; l++) {

      String textLine = textLines.get(l);
      float textLineWidth = strokePaint.measureText(textLine);

      canvas.save();
      // Apply horizontal justification
      applyJustification(documentData, canvas, textLineWidth);

      // 计算本行实际的位置
      float translateY = l * lineHeight - textLayerHeight / 2;
      // 让 offset 也加入计算
      canvas.translate(-documentData.offset[0], translateY - documentData.offset[1]);

      // Draw each line
      drawFontTextLine(textLine, documentData, canvas, parentScale);

      // Reset canvas
      canvas.restore();
    }
}

/**
 * 计算 TextLayer 实际的显示高度
 */
private float getRealTextLayerHeight(StaticLayout sl, DocumentData documentData,
                                     float lineHeight, int textLineCount) {
  if (sl == null || documentData.viewSize[1] <= 0) {
    return (textLineCount - 1) * lineHeight;
  } else {
    int line = 1;
    float height;
    while ((height = line * lineHeight) < documentData.viewSize[1]) {
      line++;
    }
    return height;
  }
}

drawTextGlyphs 方法也同样调用了 getTextLines 方法,也参考 drawTextWithFont 做同样的改动即可

三、解决结果

到此为止就完成了对换行逻辑的补充,运行起来查看一下改动结果如何
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可以看到与官网对 这个 lottie 的预览效果是一致的。OK 大功告成~

PS: 这个问题真的是足足困扰了我前后有三天!!!一开始不知道 lottie 可以动态替换文本,尝试自己做这个复杂动画。后来发现可以用lottie,结果显示有问题。我以为是 设计那边的问题,后来发现 iOS 可以。定位到是 lottie 解析库的锅,才开始自己改,又改了好久……

结果是好的! 加油!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

俺不理解

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值