老规矩,先说背景和结果,再说解决过程。如果和你遇到的问题一样,再看后边的解决过程吧
最近遇到一个棘手的问题,设计同学给了一个lottie 动画,动画内有三个字段可以动态替换,如图:
但是其中的 inputTag
在替换时,出现了一些问题
问题一 :长文本不会自动换行
问题二:多行文本未在区域内展示
发现其实设计已经设置了 input 图层的文字区域范围,理论上来讲应该可以自动换行才对,于是我在lottie 官网预览了一下,发现却是正常的
经过一番苦战,最后解决了这个问题,如果和你遇到的问题,就接着往后看吧、如果你需要直接的解决方案,可以直接看二、解决问题
根据上边看到的问题一二,不难看出
可以看到,设计妹子给的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 字段值,更无需谈处理的事儿了
经过我们缜密的分析,那么我们需要做的事就比较明确了,大概包括以下几个步骤:
OK,那么逐个模块来解决问题:
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);
}
}
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();
}
}
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])};
}
...
}
为了方便我们实现长文本的多行实现,这里使用 StaticLayout 来做文本多行测量
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);
}
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;
}
最后,重写 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 解析库的锅,才开始自己改,又改了好久……
结果是好的! 加油!