• 让你的 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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这样也叫 获取多行文本嘛?怪不得我的长文本只有一行,原来是看不到换行符不回头啊。等等,再看看 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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    可以看到,果然不出我们所料,压根没有解析 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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 其中 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();
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 在 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])};
      }
      ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 其中 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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    然后重写 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    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;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

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

    三、解决结果

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

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

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

    结果是好的! 加油!

  • 相关阅读:
    前端--第一个前端程序
    JavaEE 网络原理——TCP的工作机制(末篇 其余TCP特点)
    ClickHouse(04)如何搭建ClickHouse集群
    通过python管理MySQL
    目标检测 YOLO5部署安装测试
    问题 B: Ella的密码——map
    vue-自适应滑动条overflow: auto
    Java配置38-配置Nexus
    kubesphere安装harbor
    使用yolov5训练自己的目标检测模型
  • 原文地址:https://blog.csdn.net/weixin_41957078/article/details/125995291