25
25
import com .airbnb .lottie .model .FontCharacter ;
26
26
import com .airbnb .lottie .model .animatable .AnimatableTextProperties ;
27
27
import com .airbnb .lottie .model .content .ShapeGroup ;
28
+ import com .airbnb .lottie .model .content .TextRangeUnits ;
28
29
import com .airbnb .lottie .utils .Utils ;
29
30
import com .airbnb .lottie .value .LottieValueCallback ;
30
31
@@ -56,6 +57,7 @@ public class TextLayer extends BaseLayer {
56
57
private final TextKeyframeAnimation textAnimation ;
57
58
private final LottieDrawable lottieDrawable ;
58
59
private final LottieComposition composition ;
60
+ private TextRangeUnits textRangeUnits = TextRangeUnits .INDEX ;
59
61
@ Nullable
60
62
private BaseKeyframeAnimation <Integer , Integer > colorAnimation ;
61
63
@ Nullable
@@ -73,9 +75,17 @@ public class TextLayer extends BaseLayer {
73
75
@ Nullable
74
76
private BaseKeyframeAnimation <Float , Float > trackingCallbackAnimation ;
75
77
@ Nullable
78
+ private BaseKeyframeAnimation <Integer , Integer > opacityAnimation ;
79
+ @ Nullable
76
80
private BaseKeyframeAnimation <Float , Float > textSizeCallbackAnimation ;
77
81
@ Nullable
78
82
private BaseKeyframeAnimation <Typeface , Typeface > typefaceCallbackAnimation ;
83
+ @ Nullable
84
+ private BaseKeyframeAnimation <Integer , Integer > textRangeStartAnimation ;
85
+ @ Nullable
86
+ private BaseKeyframeAnimation <Integer , Integer > textRangeEndAnimation ;
87
+ @ Nullable
88
+ private BaseKeyframeAnimation <Integer , Integer > textRangeOffsetAnimation ;
79
89
80
90
TextLayer (LottieDrawable lottieDrawable , Layer layerModel ) {
81
91
super (lottieDrawable , layerModel );
@@ -87,29 +97,57 @@ public class TextLayer extends BaseLayer {
87
97
addAnimation (textAnimation );
88
98
89
99
AnimatableTextProperties textProperties = layerModel .getTextProperties ();
90
- if (textProperties != null && textProperties .color != null ) {
91
- colorAnimation = textProperties .color .createAnimation ();
100
+ if (textProperties != null && textProperties .textStyle != null && textProperties . textStyle . color != null ) {
101
+ colorAnimation = textProperties .textStyle . color .createAnimation ();
92
102
colorAnimation .addUpdateListener (this );
93
103
addAnimation (colorAnimation );
94
104
}
95
105
96
- if (textProperties != null && textProperties .stroke != null ) {
97
- strokeColorAnimation = textProperties .stroke .createAnimation ();
106
+ if (textProperties != null && textProperties .textStyle != null && textProperties . textStyle . stroke != null ) {
107
+ strokeColorAnimation = textProperties .textStyle . stroke .createAnimation ();
98
108
strokeColorAnimation .addUpdateListener (this );
99
109
addAnimation (strokeColorAnimation );
100
110
}
101
111
102
- if (textProperties != null && textProperties .strokeWidth != null ) {
103
- strokeWidthAnimation = textProperties .strokeWidth .createAnimation ();
112
+ if (textProperties != null && textProperties .textStyle != null && textProperties . textStyle . strokeWidth != null ) {
113
+ strokeWidthAnimation = textProperties .textStyle . strokeWidth .createAnimation ();
104
114
strokeWidthAnimation .addUpdateListener (this );
105
115
addAnimation (strokeWidthAnimation );
106
116
}
107
117
108
- if (textProperties != null && textProperties .tracking != null ) {
109
- trackingAnimation = textProperties .tracking .createAnimation ();
118
+ if (textProperties != null && textProperties .textStyle != null && textProperties . textStyle . tracking != null ) {
119
+ trackingAnimation = textProperties .textStyle . tracking .createAnimation ();
110
120
trackingAnimation .addUpdateListener (this );
111
121
addAnimation (trackingAnimation );
112
122
}
123
+
124
+ if (textProperties != null && textProperties .textStyle != null && textProperties .textStyle .opacity != null ) {
125
+ opacityAnimation = textProperties .textStyle .opacity .createAnimation ();
126
+ opacityAnimation .addUpdateListener (this );
127
+ addAnimation (opacityAnimation );
128
+ }
129
+
130
+ if (textProperties != null && textProperties .rangeSelector != null && textProperties .rangeSelector .start != null ) {
131
+ textRangeStartAnimation = textProperties .rangeSelector .start .createAnimation ();
132
+ textRangeStartAnimation .addUpdateListener (this );
133
+ addAnimation (textRangeStartAnimation );
134
+ }
135
+
136
+ if (textProperties != null && textProperties .rangeSelector != null && textProperties .rangeSelector .end != null ) {
137
+ textRangeEndAnimation = textProperties .rangeSelector .end .createAnimation ();
138
+ textRangeEndAnimation .addUpdateListener (this );
139
+ addAnimation (textRangeEndAnimation );
140
+ }
141
+
142
+ if (textProperties != null && textProperties .rangeSelector != null && textProperties .rangeSelector .offset != null ) {
143
+ textRangeOffsetAnimation = textProperties .rangeSelector .offset .createAnimation ();
144
+ textRangeOffsetAnimation .addUpdateListener (this );
145
+ addAnimation (textRangeOffsetAnimation );
146
+ }
147
+
148
+ if (textProperties != null && textProperties .rangeSelector != null ) {
149
+ textRangeUnits = textProperties .rangeSelector .units ;
150
+ }
113
151
}
114
152
115
153
@ Override
@@ -129,49 +167,86 @@ void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
129
167
canvas .save ();
130
168
canvas .concat (parentMatrix );
131
169
132
- configurePaint (documentData , parentAlpha );
170
+ configurePaint (documentData , parentAlpha , 0 );
133
171
134
172
if (lottieDrawable .useTextGlyphs ()) {
135
- drawTextWithGlyphs (documentData , parentMatrix , font , canvas );
173
+ drawTextWithGlyphs (documentData , parentMatrix , font , canvas , parentAlpha );
136
174
} else {
137
- drawTextWithFont (documentData , font , canvas );
175
+ drawTextWithFont (documentData , font , canvas , parentAlpha );
138
176
}
139
177
140
178
canvas .restore ();
141
179
}
142
180
143
- private void configurePaint (DocumentData documentData , int parentAlpha ) {
144
- if (colorCallbackAnimation != null ) {
181
+ /**
182
+ * Configures the [fillPaint] and [strokePaint] used for drawing based on currently active text ranges.
183
+ *
184
+ * @param parentAlpha A value from 0 to 255 indicating the alpha of the parented layer.
185
+ */
186
+ private void configurePaint (DocumentData documentData , int parentAlpha , int indexInDocument ) {
187
+ if (colorCallbackAnimation != null ) { // dynamic property takes priority
145
188
fillPaint .setColor (colorCallbackAnimation .getValue ());
146
- } else if (colorAnimation != null ) {
189
+ } else if (colorAnimation != null && isIndexInRangeSelection ( indexInDocument ) ) {
147
190
fillPaint .setColor (colorAnimation .getValue ());
148
- } else {
191
+ } else { // fall back to the document color
149
192
fillPaint .setColor (documentData .color );
150
193
}
151
194
152
195
if (strokeColorCallbackAnimation != null ) {
153
196
strokePaint .setColor (strokeColorCallbackAnimation .getValue ());
154
- } else if (strokeColorAnimation != null ) {
197
+ } else if (strokeColorAnimation != null && isIndexInRangeSelection ( indexInDocument ) ) {
155
198
strokePaint .setColor (strokeColorAnimation .getValue ());
156
199
} else {
157
200
strokePaint .setColor (documentData .strokeColor );
158
201
}
159
- int opacity = transform .getOpacity () == null ? 100 : transform .getOpacity ().getValue ();
160
- int alpha = opacity * 255 / 100 * parentAlpha / 255 ;
202
+
203
+ // These opacity values are in the range 0 to 100
204
+ int transformOpacity = transform .getOpacity () == null ? 100 : transform .getOpacity ().getValue ();
205
+ int textRangeOpacity = opacityAnimation != null && isIndexInRangeSelection (indexInDocument ) ? opacityAnimation .getValue () : 100 ;
206
+
207
+ // This alpha value needs to be in the range 0 to 255 to be applied to the Paint instances.
208
+ // We map the layer transform's opacity into that range and multiply it by the fractional opacity of the text range and the parent.
209
+ int alpha = Math .round ((transformOpacity * 255f / 100f )
210
+ * (textRangeOpacity / 100f )
211
+ * parentAlpha / 255f );
161
212
fillPaint .setAlpha (alpha );
162
213
strokePaint .setAlpha (alpha );
163
214
164
215
if (strokeWidthCallbackAnimation != null ) {
165
216
strokePaint .setStrokeWidth (strokeWidthCallbackAnimation .getValue ());
166
- } else if (strokeWidthAnimation != null ) {
217
+ } else if (strokeWidthAnimation != null && isIndexInRangeSelection ( indexInDocument ) ) {
167
218
strokePaint .setStrokeWidth (strokeWidthAnimation .getValue ());
168
219
} else {
169
220
strokePaint .setStrokeWidth (documentData .strokeWidth * Utils .dpScale ());
170
221
}
171
222
}
172
223
224
+ private boolean isIndexInRangeSelection (int indexInDocument ) {
225
+ int textLength = textAnimation .getValue ().text .length ();
226
+ if (textRangeStartAnimation != null && textRangeEndAnimation != null ) {
227
+ // After effects supports reversed text ranges where the start index is greater than the end index.
228
+ // For the purposes of determining if the given index is inside of the range, we take the start as the smaller value.
229
+ int rangeStart = Math .min (textRangeStartAnimation .getValue (), textRangeEndAnimation .getValue ());
230
+ int rangeEnd = Math .max (textRangeStartAnimation .getValue (), textRangeEndAnimation .getValue ());
231
+
232
+ if (textRangeOffsetAnimation != null ) {
233
+ int offset = textRangeOffsetAnimation .getValue ();
234
+ rangeStart += offset ;
235
+ rangeEnd += offset ;
236
+ }
237
+
238
+ if (textRangeUnits == TextRangeUnits .INDEX ) {
239
+ return indexInDocument >= rangeStart && indexInDocument < rangeEnd ;
240
+ } else {
241
+ float currentIndexAsPercent = indexInDocument / (float ) textLength * 100 ;
242
+ return currentIndexAsPercent >= rangeStart && currentIndexAsPercent < rangeEnd ;
243
+ }
244
+ }
245
+ return true ;
246
+ }
247
+
173
248
private void drawTextWithGlyphs (
174
- DocumentData documentData , Matrix parentMatrix , Font font , Canvas canvas ) {
249
+ DocumentData documentData , Matrix parentMatrix , Font font , Canvas canvas , int parentAlpha ) {
175
250
float textSize ;
176
251
if (textSizeCallbackAnimation != null ) {
177
252
textSize = textSizeCallbackAnimation .getValue ();
@@ -205,7 +280,7 @@ private void drawTextWithGlyphs(
205
280
canvas .save ();
206
281
207
282
if (offsetCanvas (canvas , documentData , lineIndex , line .width )) {
208
- drawGlyphTextLine (line .text , documentData , font , canvas , parentScale , fontScale , tracking );
283
+ drawGlyphTextLine (line .text , documentData , font , canvas , parentScale , fontScale , tracking , parentAlpha );
209
284
}
210
285
211
286
canvas .restore ();
@@ -214,7 +289,7 @@ private void drawTextWithGlyphs(
214
289
}
215
290
216
291
private void drawGlyphTextLine (String text , DocumentData documentData ,
217
- Font font , Canvas canvas , float parentScale , float fontScale , float tracking ) {
292
+ Font font , Canvas canvas , float parentScale , float fontScale , float tracking , int parentAlpha ) {
218
293
for (int i = 0 ; i < text .length (); i ++) {
219
294
char c = text .charAt (i );
220
295
int characterHash = FontCharacter .hashFor (c , font .getFamily (), font .getStyle ());
@@ -223,13 +298,13 @@ private void drawGlyphTextLine(String text, DocumentData documentData,
223
298
// Something is wrong. Potentially, they didn't export the text as a glyph.
224
299
continue ;
225
300
}
226
- drawCharacterAsGlyph (character , fontScale , documentData , canvas );
301
+ drawCharacterAsGlyph (character , fontScale , documentData , canvas , i , parentAlpha );
227
302
float tx = (float ) character .getWidth () * fontScale * Utils .dpScale () + tracking ;
228
303
canvas .translate (tx , 0 );
229
304
}
230
305
}
231
306
232
- private void drawTextWithFont (DocumentData documentData , Font font , Canvas canvas ) {
307
+ private void drawTextWithFont (DocumentData documentData , Font font , Canvas canvas , int parentAlpha ) {
233
308
Typeface typeface = getTypeface (font );
234
309
if (typeface == null ) {
235
310
return ;
@@ -263,6 +338,7 @@ private void drawTextWithFont(DocumentData documentData, Font font, Canvas canva
263
338
List <String > textLines = getTextLines (text );
264
339
int textLineCount = textLines .size ();
265
340
int lineIndex = -1 ;
341
+ int characterIndexAtStartOfLine = 0 ;
266
342
for (int i = 0 ; i < textLineCount ; i ++) {
267
343
String textLine = textLines .get (i );
268
344
float boxWidth = documentData .boxSize == null ? 0f : documentData .boxSize .x ;
@@ -274,9 +350,11 @@ private void drawTextWithFont(DocumentData documentData, Font font, Canvas canva
274
350
canvas .save ();
275
351
276
352
if (offsetCanvas (canvas , documentData , lineIndex , line .width )) {
277
- drawFontTextLine (line .text , documentData , canvas , tracking );
353
+ drawFontTextLine (line .text , documentData , canvas , tracking , characterIndexAtStartOfLine , parentAlpha );
278
354
}
279
355
356
+ characterIndexAtStartOfLine += line .text .length ();
357
+
280
358
canvas .restore ();
281
359
}
282
360
}
@@ -331,14 +409,23 @@ private List<String> getTextLines(String text) {
331
409
return Arrays .asList (textLinesArray );
332
410
}
333
411
334
- private void drawFontTextLine (String text , DocumentData documentData , Canvas canvas , float tracking ) {
412
+ /**
413
+ * @param characterIndexAtStartOfLine The index within the overall document of the character at the start of the line
414
+ * @param parentAlpha
415
+ */
416
+ private void drawFontTextLine (String text ,
417
+ DocumentData documentData ,
418
+ Canvas canvas ,
419
+ float tracking ,
420
+ int characterIndexAtStartOfLine ,
421
+ int parentAlpha ) {
335
422
for (int i = 0 ; i < text .length (); ) {
336
423
String charString = codePointToString (text , i );
337
- i += charString .length ();
338
- drawCharacterFromFont (charString , documentData , canvas );
424
+ drawCharacterFromFont (charString , documentData , canvas , characterIndexAtStartOfLine + i , parentAlpha );
339
425
float charWidth = fillPaint .measureText (charString );
340
426
float tx = charWidth + tracking ;
341
427
canvas .translate (tx , 0 );
428
+ i += charString .length ();
342
429
}
343
430
}
344
431
@@ -430,7 +517,10 @@ private void drawCharacterAsGlyph(
430
517
FontCharacter character ,
431
518
float fontScale ,
432
519
DocumentData documentData ,
433
- Canvas canvas ) {
520
+ Canvas canvas ,
521
+ int indexInDocument ,
522
+ int parentAlpha ) {
523
+ configurePaint (documentData , parentAlpha , indexInDocument );
434
524
List <ContentGroup > contentGroups = getContentsForCharacter (character );
435
525
for (int j = 0 ; j < contentGroups .size (); j ++) {
436
526
Path path = contentGroups .get (j ).getPath ();
@@ -459,7 +549,8 @@ private void drawGlyph(Path path, Paint paint, Canvas canvas) {
459
549
canvas .drawPath (path , paint );
460
550
}
461
551
462
- private void drawCharacterFromFont (String character , DocumentData documentData , Canvas canvas ) {
552
+ private void drawCharacterFromFont (String character , DocumentData documentData , Canvas canvas , int indexInDocument , int parentAlpha ) {
553
+ configurePaint (documentData , parentAlpha , indexInDocument );
463
554
if (documentData .strokeOverFill ) {
464
555
drawCharacter (character , fillPaint , canvas );
465
556
drawCharacter (character , strokePaint , canvas );
0 commit comments