Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for text range selectors #2518

Merged
merged 2 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@

public class AnimatableTextProperties {

@Nullable public final AnimatableColorValue color;
@Nullable public final AnimatableColorValue stroke;
@Nullable public final AnimatableFloatValue strokeWidth;
@Nullable public final AnimatableFloatValue tracking;
@Nullable public final AnimatableTextStyle textStyle;
@Nullable public final AnimatableTextRangeSelector rangeSelector;

public AnimatableTextProperties(@Nullable AnimatableColorValue color,
@Nullable AnimatableColorValue stroke, @Nullable AnimatableFloatValue strokeWidth,
@Nullable AnimatableFloatValue tracking) {
this.color = color;
this.stroke = stroke;
this.strokeWidth = strokeWidth;
this.tracking = tracking;
public AnimatableTextProperties(
@Nullable AnimatableTextStyle textStyle,
@Nullable AnimatableTextRangeSelector rangeSelector) {
this.textStyle = textStyle;
this.rangeSelector = rangeSelector;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.airbnb.lottie.model.animatable;

import androidx.annotation.Nullable;
import com.airbnb.lottie.model.content.TextRangeUnits;

/**
* Defines an animated range of text that should have an [AnimatableTextProperties] applied to it.
*/
public class AnimatableTextRangeSelector {
@Nullable public final AnimatableIntegerValue start;
@Nullable public final AnimatableIntegerValue end;
@Nullable public final AnimatableIntegerValue offset;
public final TextRangeUnits units;

public AnimatableTextRangeSelector(
@Nullable AnimatableIntegerValue start,
@Nullable AnimatableIntegerValue end,
@Nullable AnimatableIntegerValue offset,
TextRangeUnits units) {
this.start = start;
this.end = end;
this.offset = offset;
this.units = units;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.airbnb.lottie.model.animatable;

import androidx.annotation.Nullable;

public class AnimatableTextStyle {

@Nullable public final AnimatableColorValue color;
@Nullable public final AnimatableColorValue stroke;
@Nullable public final AnimatableFloatValue strokeWidth;
@Nullable public final AnimatableFloatValue tracking;
@Nullable public final AnimatableIntegerValue opacity;

public AnimatableTextStyle(
@Nullable AnimatableColorValue color,
@Nullable AnimatableColorValue stroke,
@Nullable AnimatableFloatValue strokeWidth,
@Nullable AnimatableFloatValue tracking,
@Nullable AnimatableIntegerValue opacity) {
this.color = color;
this.stroke = stroke;
this.strokeWidth = strokeWidth;
this.tracking = tracking;
this.opacity = opacity;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.airbnb.lottie.model.content;

public enum TextRangeUnits {
PERCENT,
INDEX
}
151 changes: 121 additions & 30 deletions lottie/src/main/java/com/airbnb/lottie/model/layer/TextLayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
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.Utils;
import com.airbnb.lottie.value.LottieValueCallback;

Expand Down Expand Up @@ -56,6 +57,7 @@ public class TextLayer extends BaseLayer {
private final TextKeyframeAnimation textAnimation;
private final LottieDrawable lottieDrawable;
private final LottieComposition composition;
private TextRangeUnits textRangeUnits = TextRangeUnits.INDEX;
@Nullable
private BaseKeyframeAnimation<Integer, Integer> colorAnimation;
@Nullable
Expand All @@ -73,9 +75,17 @@ public class TextLayer extends BaseLayer {
@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);
Expand All @@ -87,29 +97,57 @@ public class TextLayer extends BaseLayer {
addAnimation(textAnimation);

AnimatableTextProperties textProperties = layerModel.getTextProperties();
if (textProperties != null && textProperties.color != null) {
colorAnimation = textProperties.color.createAnimation();
if (textProperties != null && textProperties.textStyle != null && textProperties.textStyle.color != null) {
colorAnimation = textProperties.textStyle.color.createAnimation();
colorAnimation.addUpdateListener(this);
addAnimation(colorAnimation);
}

if (textProperties != null && textProperties.stroke != null) {
strokeColorAnimation = textProperties.stroke.createAnimation();
if (textProperties != null && textProperties.textStyle != null && textProperties.textStyle.stroke != null) {
strokeColorAnimation = textProperties.textStyle.stroke.createAnimation();
strokeColorAnimation.addUpdateListener(this);
addAnimation(strokeColorAnimation);
}

if (textProperties != null && textProperties.strokeWidth != null) {
strokeWidthAnimation = textProperties.strokeWidth.createAnimation();
if (textProperties != null && textProperties.textStyle != null && textProperties.textStyle.strokeWidth != null) {
strokeWidthAnimation = textProperties.textStyle.strokeWidth.createAnimation();
strokeWidthAnimation.addUpdateListener(this);
addAnimation(strokeWidthAnimation);
}

if (textProperties != null && textProperties.tracking != null) {
trackingAnimation = textProperties.tracking.createAnimation();
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
Expand All @@ -129,49 +167,86 @@ void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
canvas.save();
canvas.concat(parentMatrix);

configurePaint(documentData, parentAlpha);
configurePaint(documentData, parentAlpha, 0);

if (lottieDrawable.useTextGlyphs()) {
drawTextWithGlyphs(documentData, parentMatrix, font, canvas);
drawTextWithGlyphs(documentData, parentMatrix, font, canvas, parentAlpha);
} else {
drawTextWithFont(documentData, font, canvas);
drawTextWithFont(documentData, font, canvas, parentAlpha);
}

canvas.restore();
}

private void configurePaint(DocumentData documentData, int parentAlpha) {
if (colorCallbackAnimation != null) {
/**
* 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) {
} else if (colorAnimation != null && isIndexInRangeSelection(indexInDocument)) {
fillPaint.setColor(colorAnimation.getValue());
} else {
} else { // fall back to the document color
fillPaint.setColor(documentData.color);
}

if (strokeColorCallbackAnimation != null) {
strokePaint.setColor(strokeColorCallbackAnimation.getValue());
} else if (strokeColorAnimation != null) {
} else if (strokeColorAnimation != null && isIndexInRangeSelection(indexInDocument)) {
strokePaint.setColor(strokeColorAnimation.getValue());
} else {
strokePaint.setColor(documentData.strokeColor);
}
int opacity = transform.getOpacity() == null ? 100 : transform.getOpacity().getValue();
int alpha = opacity * 255 / 100 * parentAlpha / 255;

// 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) {
} 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) {
DocumentData documentData, Matrix parentMatrix, Font font, Canvas canvas, int parentAlpha) {
float textSize;
if (textSizeCallbackAnimation != null) {
textSize = textSizeCallbackAnimation.getValue();
Expand Down Expand Up @@ -205,7 +280,7 @@ private void drawTextWithGlyphs(
canvas.save();

if (offsetCanvas(canvas, documentData, lineIndex, line.width)) {
drawGlyphTextLine(line.text, documentData, font, canvas, parentScale, fontScale, tracking);
drawGlyphTextLine(line.text, documentData, font, canvas, parentScale, fontScale, tracking, parentAlpha);
}

canvas.restore();
Expand All @@ -214,7 +289,7 @@ private void drawTextWithGlyphs(
}

private void drawGlyphTextLine(String text, DocumentData documentData,
Font font, Canvas canvas, float parentScale, float fontScale, float tracking) {
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());
Expand All @@ -223,13 +298,13 @@ private void drawGlyphTextLine(String text, DocumentData documentData,
// Something is wrong. Potentially, they didn't export the text as a glyph.
continue;
}
drawCharacterAsGlyph(character, fontScale, documentData, canvas);
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) {
private void drawTextWithFont(DocumentData documentData, Font font, Canvas canvas, int parentAlpha) {
Typeface typeface = getTypeface(font);
if (typeface == null) {
return;
Expand Down Expand Up @@ -263,6 +338,7 @@ private void drawTextWithFont(DocumentData documentData, Font font, Canvas canva
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;
Expand All @@ -274,9 +350,11 @@ private void drawTextWithFont(DocumentData documentData, Font font, Canvas canva
canvas.save();

if (offsetCanvas(canvas, documentData, lineIndex, line.width)) {
drawFontTextLine(line.text, documentData, canvas, tracking);
drawFontTextLine(line.text, documentData, canvas, tracking, characterIndexAtStartOfLine, parentAlpha);
}

characterIndexAtStartOfLine += line.text.length();

canvas.restore();
}
}
Expand Down Expand Up @@ -331,14 +409,23 @@ private List<String> getTextLines(String text) {
return Arrays.asList(textLinesArray);
}

private void drawFontTextLine(String text, DocumentData documentData, Canvas canvas, float tracking) {
/**
* @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) {
for (int i = 0; i < text.length(); ) {
String charString = codePointToString(text, i);
i += charString.length();
drawCharacterFromFont(charString, documentData, canvas);
drawCharacterFromFont(charString, documentData, canvas, characterIndexAtStartOfLine + i, parentAlpha);
float charWidth = fillPaint.measureText(charString);
float tx = charWidth + tracking;
canvas.translate(tx, 0);
i += charString.length();
}
}

Expand Down Expand Up @@ -430,7 +517,10 @@ private void drawCharacterAsGlyph(
FontCharacter character,
float fontScale,
DocumentData documentData,
Canvas canvas) {
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();
Expand Down Expand Up @@ -459,7 +549,8 @@ private void drawGlyph(Path path, Paint paint, Canvas canvas) {
canvas.drawPath(path, paint);
}

private void drawCharacterFromFont(String character, DocumentData documentData, Canvas canvas) {
private void drawCharacterFromFont(String character, DocumentData documentData, Canvas canvas, int indexInDocument, int parentAlpha) {
configurePaint(documentData, parentAlpha, indexInDocument);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means we need to configure the paint on every character. I haven't looked into the performance implication or further optimization.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably okay as long as we're not allocating new paint objects (which we're not)

if (documentData.strokeOverFill) {
drawCharacter(character, fillPaint, canvas);
drawCharacter(character, strokePaint, canvas);
Expand Down
Loading
Loading