-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
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 to TextProviders
for CoreAnimation
#1723
Conversation
/// The `AnimationTextProvider` that `TextLayer`'s use to retrieve texts, | ||
/// that they should use to render their text context | ||
var textProvider: AnimationTextProvider { | ||
didSet { reloadTexts() } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reloadFonts()
/ reloadTexts()
doesn't cause setupAnimations(context:)
to be called again. Instead I think we need to call rebuildCurrentAnimation()
, like we do in setValueProvider(_:keypath:)
below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got you! Thanks!
Fixed in b542f66 ✅
try super.setupAnimations(context: context) | ||
|
||
// Question 1: Which keypath to pass? | ||
_ = try context.currentKeypath.keys.map { key in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should pass the entire keypath here, e.g. context.currentKeypath.fullPath
|
||
// Question 1: Which keypath to pass? | ||
_ = try context.currentKeypath.keys.map { key in | ||
// Question 2: Where do we get the text layer text? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
try textLayerModel.text.exactlyOneKeyframe(context: context, description: "text layer text").text
is text value displayed in the text layer.
Due to limitations of Core Animation text rendering, this text layer only supports displaying a static text value over the entire animation. That's why we extract a single string from the keyframes using exaxtlyOneKeyframe()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the method in 5055f10 🟡
Due to limitations of Core Animation text rendering, this text layer only supports displaying a static text value over the entire animation. That's why we extract a single string from the keyframes using exaxtlyOneKeyframe().
Do you mean that text won't be able to be animated in the end anyway, or that just the text can't be changed during the animation? Like we can't start animation with text "Hell.." and finish it with "Hello!" 🤔
_ = try context.currentKeypath.keys.map { key in | ||
// Question 2: Where do we get the text layer text? | ||
let customText = context.textProvider.textFor(keypathName: key, sourceText: "text layer text") | ||
if customText.isEmpty { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need this isEmpty
check -- the Main Thread rendering engine doesn't have this check (
lottie-ios/Sources/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.swift
Line 115 in 167812e
let textString = textProvider.textFor(keypathName: keypathName, sourceText: text.text) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the method in 5055f10 🟡
Okay, I see. But then I just don't understand how do we actually decide when to set this:
renderLayer.text = customText
and when do this:
renderLayer.text = try textLayerModel.text.exactlyOneKeyframe(context: context, description: "text layer text").text
? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we always just do renderLayer.text = customText
😄 https://github.com/airbnb/lottie-ios/pull/1723/files#r950221577
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, then it is this way already in the code! 🎉
@@ -48,6 +64,9 @@ final class TextLayer: BaseCompositionLayer { | |||
// - We may be able to support animating `fillColor` by getting clever with layer blend modes | |||
// or masks (e.g. use `CoreTextRenderLayer` to draw black glyphs, and then fill them in | |||
// using a `CAShapeLayer`). | |||
|
|||
// Question 3: Should we somehow initialize LayerContextAnimations at this stage |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remove the renderLayer.text = text.text
call from this method -- it only needs to be done once, and setupAnimations
is fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right! Fixed in 01307aa ✅
Tests/SnapshotConfiguration.swift
Outdated
@@ -22,6 +22,9 @@ struct SnapshotConfiguration { | |||
/// A custom `AnimationImageProvider` to use when rendering this animation | |||
var customImageProvider: AnimationImageProvider? | |||
|
|||
/// A custom `AnimationTextProvider` to use when rendering this animatino | |||
var customTextProvider: AnimationTextProvider? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var customTextProvider: AnimationTextProvider? | |
var customTextProvider: AnimationTextProvider? |
btw our readme has instructions on how you can run our code formatter (which will be necessary for CI to pass): https://github.com/airbnb/lottie-ios#contributing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
31f3bf9 ✅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the way, I am experiencing some troubles with the ruby version and therefore can't run the linter.
So all the previous problems I found manually and fixed. But now I don't see any problem, and it still fails...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All good, I'll run it locally and push
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ruby can be a nightmare 🫠
Tests/SnapshotConfiguration.swift
Outdated
@@ -70,6 +73,9 @@ extension SnapshotConfiguration { | |||
// Test cases for `AnimatedImageProvider` | |||
"Nonanimating/_dog": .customImageProvider(HardcodedImageProvider(imageName: "Samples/Images/dog.png")), | |||
|
|||
// Test cases for `AnimatedTextProvider` | |||
"": .customTextProvider(HardcodedTextProvider()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be:
"": .customTextProvider(HardcodedTextProvider()), | |
"Issues/issue_1722": .customTextProvider(HardcodedTextProvider()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a053713 ✅
class HardcodedTextProvider: AnimationTextProvider { | ||
// Question 4: What should we actually return here? | ||
func textFor(keypathName: String, sourceText: String) -> String { | ||
#if os(iOS) | ||
return "text layer text" | ||
#else | ||
return nil | ||
#endif | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be:
class HardcodedTextProvider: AnimationTextProvider { | |
// Question 4: What should we actually return here? | |
func textFor(keypathName: String, sourceText: String) -> String { | |
#if os(iOS) | |
return "text layer text" | |
#else | |
return nil | |
#endif | |
} | |
class HardcodedTextProvider: AnimationTextProvider { | |
init(text: String) { | |
self.text = text | |
} | |
func textFor(keypathName _: String, sourceText _: String) -> String { | |
text | |
} | |
private let text: String | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alrighty! a053713 ✅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good!
// We need to rebuild the current animation after registering a value provider, | ||
// since any existing `CAAnimation`s could now be out of date. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// We need to rebuild the current animation after registering a value provider, | |
// since any existing `CAAnimation`s could now be out of date. | |
// We need to rebuild the current animation after updating the text provider, | |
// since this is used in `TextLayer.setupAnimations(context:)` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
7a71e9a ✅
@@ -26,10 +26,11 @@ final class TextLayer: BaseCompositionLayer { | |||
override func setupAnimations(context: LayerAnimationContext) throws { | |||
try super.setupAnimations(context: context) | |||
|
|||
// Question 1: Which keypath to pass? | |||
_ = try context.currentKeypath.keys.map { key in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need this context.currentKeypath.keys.map
-- we should be able to just remove it and just have the code on the inside
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got rid of it in c8a84c2 ✅
// Question 2.1: Should we get from somewhere source text? What if the user has custom name? | ||
let customText = context.textProvider.textFor(keypathName: context.currentKeypath.fullPath, sourceText: "text layer text") | ||
// Question 2.2: If we don't have this .isEmpty check, then how do we decide when to set our custom text, and when to | ||
// set the result of the `exactlyOneKeyFrame()`? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you look at the behavior of the main thread rendering engine, it just unconditionally sets renderLayer.text
to the result of textProvider.textFor(keypathName:sourceText:)
. Since that method doesn't return an optional, it looks like expected usage is to just return sourceText
if you don't want to customize the text for that specific keypath. That's how DefaultTextProvider
works, for example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the method in c8a84c2 🟡
Then we just set it like this? 🤔
// Question 2: Where do we get the text layer text? | ||
let customText = context.textProvider.textFor(keypathName: key, sourceText: "text layer text") | ||
// Question 2.1: Should we get from somewhere source text? What if the user has custom name? | ||
let customText = context.textProvider.textFor(keypathName: context.currentKeypath.fullPath, sourceText: "text layer text") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sourceText
is just the text value specified in the animation json for this layer -- so, try textLayerModel.text.exactlyOneKeyframe(context: context, description: "text layer text").text
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But what if the JSON will have a different name for this text layer?
Would there be a way to pass some specific value to configure animation for such behavior?
@@ -70,6 +73,9 @@ extension SnapshotConfiguration { | |||
// Test cases for `AnimatedImageProvider` | |||
"Nonanimating/_dog": .customImageProvider(HardcodedImageProvider(imageName: "Samples/Images/dog.png")), | |||
|
|||
// Test cases for `AnimatedTextProvider` | |||
"Issues/issue_1722": .customTextProvider(HardcodedTextProvider(text: "Bounce-bounce")), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you're done with the code changes you can generate the snapshot images for this by running the unit test target on an iPhone 8 simulator (or by running bundle exec rake test:package
on the command line).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fyi I think the snapshot tests are sensitive to specific Xcode versions -- right now I think they run correctly on Xcode 13.4.1 but not on Xcode 14. If you get a lot of spurious failures when you run the tests let me know and I can pull you branch and run them for you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm unfortunately it always return this message during the test run:
No reference was found on disk. Automatically recorded snapshot:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you get a lot of spurious failures when you run the tests let me know and I can pull you branch and run them for you.
Yeah, Cal, that would be helpful, thanks a bunch! 🙏🏼
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the way the name of the text layer in the provided animation is Animation text layer
🙏🏼
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm unfortunately it always return this message during the test run:
No reference was found on disk. Automatically recorded snapshot:
This is expected the first time you run the tests after you add a new sample JSON file. If you run the tests again this goes away, since now it compares the new snapshot with the previously-recorded one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm true. However, the snapshots anyway don't contain the text. Could it be because the text layer has a different name?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this was because we had to call textRenderLayer.sizeToFit()
after updating textRenderLayer.text
.
@@ -22,6 +22,9 @@ struct SnapshotConfiguration { | |||
/// A custom `AnimationImageProvider` to use when rendering this animation | |||
var customImageProvider: AnimationImageProvider? | |||
|
|||
/// A custom `AnimationTextProvider` to use when rendering this animatino | |||
var customTextProvider: AnimationTextProvider? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sure to actually apply this text provider to the AnimationView
in SnapshotConfiguration.makeAnimationView
: https://github.com/airbnb/lottie-ios/blob/master/Tests/SnapshotTests.swift#L255
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool to know, thanks!
Done in c7a21ce ✅
@@ -23,6 +23,13 @@ final class TextLayer: BaseCompositionLayer { | |||
fatalError("init(coder:) has not been implemented") | |||
} | |||
|
|||
override func setupAnimations(context: LayerAnimationContext) throws { | |||
try super.setupAnimations(context: context) | |||
let customText = context.textProvider.textFor(keypathName: context.currentKeypath.fullPath, sourceText: "text layer text") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to pass in the actual source text like here:
lottie-ios/Sources/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.swift
Line 115 in 167812e
let textString = textProvider.textFor(keypathName: keypathName, sourceText: text.text) |
let customText = context.textProvider.textFor(keypathName: context.currentKeypath.fullPath, sourceText: "text layer text") | |
let sourceText = try textLayerModel.text.exactlyOneKeyframe(context: context, description: "text layer text") | |
let customText = context.textProvider.textFor(keypathName: context.currentKeypath.fullPath, sourceText: sourceText ) |
I think the textLayerModel.text.exactlyOneKeyframe
code (currently on line 51) can be removed from configureRenderLayer
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I think it's possible.
Done in 8cb4ccd ✅
I think the textLayerModel.text.exactlyOneKeyframe code (currently on line 51) can be removed from configureRenderLayer.
But I am not sure about that. Since we use the text later on in the configureRenderLayer(:)
renderLayer.alignment = text.justification.textAlignment
renderLayer.lineHeight = CGFloat(text.lineHeight)
renderLayer.tracking = (CGFloat(text.fontSize) * CGFloat(text.tracking)) / 1000
renderLayer.fillColor = text.fillColorData?.cgColorValue
renderLayer.strokeColor = text.strokeColorData?.cgColorValue
renderLayer.strokeWidth = CGFloat(text.strokeWidth ?? 0)
renderLayer.strokeOnTop = text.strokeOverFill ?? false
renderLayer.preferredSize = text.textFrameSize?.sizeValue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, haven't expanded far enough downwards, nevermind.
import QuartzCore | ||
#if os(iOS) | ||
import UIKit | ||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these necessary?
import QuartzCore | |
#if os(iOS) | |
import UIKit | |
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
8bbf3c9 ✅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work, I pushed some commits to make the tests pass etc
Thanks a lot @calda! Superb experience to try out some code here together! |
Thanks for the contribution! I turned on auto-merge so this PR will merge automatically as soon as CI finishes, assuming the checks are all green. We just released 3.4.2 so I don't plan on releasing a new version immediately. We'd probably release 3.4.3 in a month or so. In the mean time your project could point directly to |
Co-authored-by: Cal Stephens <[email protected]>
Co-authored-by: Cal Stephens <[email protected]>
…#1723) Co-authored-by: Cal Stephens <[email protected]>
What 🙋🏼
This PR is supposed to provide support for text providers, so it would be possible to pass text to animations running on the
CoreAnimation
engine. 🚀This is my first attempt of doing the PR in this library and an open source project in general. So please bear with me a bit! 🤠
I have four questions/concerns that should be answered before this PR can be valid! (All questions are also presented in the code as comments).
Concerns 😬
setupAnimations
method?LayerContextAnimations
insideconfigureRenderLayer
to call theself.setupAnimations
method properly?HardcodedTextProvider
, what do we want to return?Related issue 🤖
Fixes #1722 ☝🏼