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

Update text provider API to use full AnimationKeypath values #2183

Merged
merged 2 commits into from
Sep 14, 2023
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
4 changes: 4 additions & 0 deletions Lottie.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@
080DF07F2A95718200BE2D96 /* AnimationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9C95D22822F43100677516 /* AnimationContext.swift */; };
080DF0802A95718200BE2D96 /* AnyEquatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABF033B32A7B0ABA00F8C228 /* AnyEquatable.swift */; };
080DF0812A95718200BE2D96 /* LottieAnimationSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0819D2A02A718CAE00D7DE49 /* LottieAnimationSource.swift */; };
080F5FDC2AB1075000ADC32C /* TextProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F5FDB2AB1075000ADC32C /* TextProviderTests.swift */; };
0819D2A12A718CAE00D7DE49 /* LottieAnimationSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0819D2A02A718CAE00D7DE49 /* LottieAnimationSource.swift */; };
0819D2A22A718CAE00D7DE49 /* LottieAnimationSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0819D2A02A718CAE00D7DE49 /* LottieAnimationSource.swift */; };
0819D2A32A718CAE00D7DE49 /* LottieAnimationSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0819D2A02A718CAE00D7DE49 /* LottieAnimationSource.swift */; };
Expand Down Expand Up @@ -1159,6 +1160,7 @@

/* Begin PBXFileReference section */
080DEF622A95707B00BE2D96 /* Lottie.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Lottie.framework; sourceTree = BUILT_PRODUCTS_DIR; };
080F5FDB2AB1075000ADC32C /* TextProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextProviderTests.swift; sourceTree = "<group>"; };
0819D2A02A718CAE00D7DE49 /* LottieAnimationSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieAnimationSource.swift; sourceTree = "<group>"; };
0820D5922A8ACD67007D705C /* LottieButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieButton.swift; sourceTree = "<group>"; };
0820D5962A8ACDD7007D705C /* AnimatedButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedButton.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1752,6 +1754,7 @@
D453D8AE28FF9BC600D3F49C /* AnimationCacheProviderTests.swift */,
08CB2680291ED2B700B4F071 /* AnimationViewTests.swift */,
2E70F79E295BB6D30089A0EF /* CompatibleAnimationViewTests.swift */,
080F5FDB2AB1075000ADC32C /* TextProviderTests.swift */,
);
path = Tests;
sourceTree = "<group>";
Expand Down Expand Up @@ -3233,6 +3236,7 @@
36E57EAC28AF7ADF00B7EFDA /* HardcodedTextProvider.swift in Sources */,
2E72128527BB32DB0027BC56 /* PerformanceTests.swift in Sources */,
6DB3BDC328245AA2002A276D /* ParsingTests.swift in Sources */,
080F5FDC2AB1075000ADC32C /* TextProviderTests.swift in Sources */,
6DB3BDB628243FA5002A276D /* ValueProvidersTests.swift in Sources */,
2E72128327BB329C0027BC56 /* AnimationKeypathTests.swift in Sources */,
2E044E272820536800FA773B /* AutomaticEngineTests.swift in Sources */,
Expand Down
8 changes: 4 additions & 4 deletions Sources/Private/CoreAnimation/CoreAnimationLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ final class CoreAnimationLayer: BaseAnimationLayer {
init(
animation: LottieAnimation,
imageProvider: AnimationImageProvider,
textProvider: AnimationTextProvider,
textProvider: AnimationKeypathTextProvider,
fontProvider: AnimationFontProvider,
maskAnimationToBounds: Bool,
compatibilityTrackerMode: CompatibilityTracker.Mode,
Expand Down Expand Up @@ -108,9 +108,9 @@ final class CoreAnimationLayer: BaseAnimationLayer {
didSet { reloadImages() }
}

/// The `AnimationTextProvider` that `TextLayer`'s use to retrieve texts,
/// The `AnimationKeypathTextProvider` that `TextLayer`'s use to retrieve texts,
/// that they should use to render their text context
var textProvider: AnimationTextProvider {
var textProvider: AnimationKeypathTextProvider {
didSet {
// We need to rebuild the current animation after updating the text provider,
// since this is used in `TextLayer.setupAnimations(context:)`
Expand Down Expand Up @@ -449,7 +449,7 @@ extension CoreAnimationLayer: RootAnimationLayer {
}

func forceDisplayUpdate() {
// Unimplemented / unused
display()
}

func logHierarchyKeypaths() {
Expand Down
4 changes: 2 additions & 2 deletions Sources/Private/CoreAnimation/Layers/AnimationLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ struct LayerAnimationContext {
/// The AnimationKeypath represented by the current layer
var currentKeypath: AnimationKeypath

/// The `AnimationTextProvider`
var textProvider: AnimationTextProvider
/// The `AnimationKeypathTextProvider`
var textProvider: AnimationKeypathTextProvider

/// Records the given animation keypath so it can be logged or collected into a list
/// - Used for `CoreAnimationLayer.logHierarchyKeypaths()` and `allHierarchyKeypaths()`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import QuartzCore
struct LayerContext {
let animation: LottieAnimation
let imageProvider: AnimationImageProvider
let textProvider: AnimationTextProvider
let textProvider: AnimationKeypathTextProvider
let fontProvider: AnimationFontProvider
let compatibilityTracker: CompatibilityTracker
var layerName: String
Expand Down
16 changes: 13 additions & 3 deletions Sources/Private/CoreAnimation/Layers/TextLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,19 @@ final class TextLayer: BaseCompositionLayer {
context: textAnimationContext,
description: "text layer text")

renderLayer.text = context.textProvider.textFor(
keypathName: textAnimationContext.currentKeypath.fullPath,
sourceText: sourceText.text)
// Prior to Lottie 4.3.0 the Core Animation rendering engine always just used `LegacyAnimationTextProvider`
// but incorrectly called it with the full keypath string, unlike the Main Thread rendering engine
// which only used the last component of the keypath. Starting in Lottie 4.3.0 we use `AnimationKeypathTextProvider`
// instead if implemented.
if let keypathTextValue = context.textProvider.text(for: textAnimationContext.currentKeypath, sourceText: sourceText.text) {
renderLayer.text = keypathTextValue
} else if let legacyTextProvider = context.textProvider as? LegacyAnimationTextProvider {
renderLayer.text = legacyTextProvider.textFor(
keypathName: textAnimationContext.currentKeypath.fullPath,
sourceText: sourceText.text)
} else {
renderLayer.text = sourceText.text
}

renderLayer.sizeToFit()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ final class PreCompositionLayer: CompositionLayer {
precomp: PreCompLayerModel,
asset: PrecompAsset,
layerImageProvider: LayerImageProvider,
textProvider: AnimationTextProvider,
layerTextProvider: LayerTextProvider,
textProvider: AnimationKeypathTextProvider,
fontProvider: AnimationFontProvider,
assetLibrary: AssetLibrary?,
frameRate: CGFloat)
frameRate: CGFloat,
rootAnimationLayer: MainThreadAnimationLayer?)
{
animationLayers = []
if let keyframes = precomp.timeRemapping?.keyframes {
Expand All @@ -36,11 +38,14 @@ final class PreCompositionLayer: CompositionLayer {
let layers = asset.layers.initializeCompositionLayers(
assetLibrary: assetLibrary,
layerImageProvider: layerImageProvider,
layerTextProvider: layerTextProvider,
textProvider: textProvider,
fontProvider: fontProvider,
frameRate: frameRate)
frameRate: frameRate,
rootAnimationLayer: rootAnimationLayer)

var imageLayers = [ImageCompositionLayer]()
var textLayers = [TextCompositionLayer]()

var mattedLayer: CompositionLayer? = nil

Expand All @@ -50,6 +55,9 @@ final class PreCompositionLayer: CompositionLayer {
if let imageLayer = layer as? ImageCompositionLayer {
imageLayers.append(imageLayer)
}
if let textLayer = layer as? TextCompositionLayer {
textLayers.append(textLayer)
}
if let matte = mattedLayer {
/// The previous layer requires this layer to be its matte
matte.matteLayer = layer
Expand All @@ -69,6 +77,7 @@ final class PreCompositionLayer: CompositionLayer {
childKeypaths.append(contentsOf: layers)

layerImageProvider.addImageLayers(imageLayers)
layerTextProvider.addTextLayers(textLayers)
Copy link
Member Author

Choose a reason for hiding this comment

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

Found and fixed an unrelated bug where the Main Thread rendering engine didn't correctly track and update text layers included in PreCompositionLayers.

}

override init(layer: Any) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ final class TextCompositionLayer: CompositionLayer {

// MARK: Lifecycle

init(textLayer: TextLayerModel, textProvider: AnimationTextProvider, fontProvider: AnimationFontProvider) {
init(
textLayer: TextLayerModel,
textProvider: AnimationKeypathTextProvider,
fontProvider: AnimationFontProvider,
rootAnimationLayer: MainThreadAnimationLayer?)
{
var rootNode: TextAnimatorNode?
for animator in textLayer.animators {
rootNode = TextAnimatorNode(parentNode: rootNode, textAnimator: animator)
Expand All @@ -57,6 +62,7 @@ final class TextCompositionLayer: CompositionLayer {

self.textProvider = textProvider
self.fontProvider = fontProvider
self.rootAnimationLayer = rootAnimationLayer

super.init(layer: textLayer, size: .zero)
contentsLayer.addSublayer(self.textLayer)
Expand Down Expand Up @@ -92,8 +98,18 @@ final class TextCompositionLayer: CompositionLayer {
let textDocument: KeyframeInterpolator<TextDocument>?

let textLayer = CoreTextRenderLayer()
var textProvider: AnimationTextProvider
var textProvider: AnimationKeypathTextProvider
var fontProvider: AnimationFontProvider
weak var rootAnimationLayer: MainThreadAnimationLayer?

lazy var fullAnimationKeypath: AnimationKeypath = {
// Individual layers don't know their full keypaths, so we have to delegate
// to the `MainThreadAnimationLayer` to search the layer hierarchy and find
// the full keypath (which includes this layer's parent layers)
rootAnimationLayer?.keypath(for: self)
Copy link
Member Author

Choose a reason for hiding this comment

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

This was tricky -- I suppose this is why the Main Thread rendering engine didn't previously work like this

// If that failed for some reason, just use the last path component (which we do have here)
?? AnimationKeypath(keypath: keypathName)
}()

override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
guard let textDocument = textDocument else { return }
Expand All @@ -108,11 +124,23 @@ final class TextCompositionLayer: CompositionLayer {

// Get Text Attributes
let text = textDocument.value(frame: frame) as! TextDocument

// Prior to Lottie 4.3.0 the Main Thread rendering engine always just used `LegacyAnimationTextProvider`
// and called it with the `keypathName` (only the last path component of the full keypath).
// Starting in Lottie 4.3.0 we use `AnimationKeypathTextProvider` instead if implemented.
let textString: String
if let keypathTextValue = textProvider.text(for: fullAnimationKeypath, sourceText: text.text) {
textString = keypathTextValue
} else if let legacyTextProvider = textProvider as? LegacyAnimationTextProvider {
textString = legacyTextProvider.textFor(keypathName: keypathName, sourceText: text.text)
} else {
textString = text.text
}

let strokeColor = rootNode?.textOutputNode.strokeColor ?? text.strokeColorData?.cgColorValue
let strokeWidth = rootNode?.textOutputNode.strokeWidth ?? CGFloat(text.strokeWidth ?? 0)
let tracking = (CGFloat(text.fontSize) * (rootNode?.textOutputNode.tracking ?? CGFloat(text.tracking))) / 1000.0
let matrix = rootNode?.textOutputNode.xform ?? CATransform3DIdentity
let textString = textProvider.textFor(keypathName: keypathName, sourceText: text.text)
let ctFont = fontProvider.fontFor(family: text.fontFamily, size: CGFloat(text.fontSize))

// Set all of the text layer options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer {
init(
animation: LottieAnimation,
imageProvider: AnimationImageProvider,
textProvider: AnimationTextProvider,
textProvider: AnimationKeypathTextProvider,
fontProvider: AnimationFontProvider,
maskAnimationToBounds: Bool,
logger: LottieLogger)
Expand All @@ -37,9 +37,11 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer {
let layers = animation.layers.initializeCompositionLayers(
assetLibrary: animation.assetLibrary,
layerImageProvider: layerImageProvider,
layerTextProvider: layerTextProvider,
textProvider: textProvider,
fontProvider: fontProvider,
frameRate: CGFloat(animation.framerate))
frameRate: CGFloat(animation.framerate),
rootAnimationLayer: self)

var imageLayers = [ImageCompositionLayer]()
var textLayers = [TextCompositionLayer]()
Expand Down Expand Up @@ -191,7 +193,7 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer {
}
}

var textProvider: AnimationTextProvider {
var textProvider: AnimationKeypathTextProvider {
get { layerTextProvider.textProvider }
set { layerTextProvider.textProvider = newValue }
}
Expand Down Expand Up @@ -272,6 +274,15 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer {
return nil
}

func keypath(for layerToFind: CALayer) -> AnimationKeypath? {
for layer in animationLayers {
if let foundKeypath = layer.keypath(for: layerToFind) {
return foundKeypath
}
}
return nil
}

func animatorNodes(for keypath: AnimationKeypath) -> [AnimatorNode]? {
var results = [AnimatorNode]()
for layer in animationLayers {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ extension Array where Element == LayerModel {
func initializeCompositionLayers(
assetLibrary: AssetLibrary?,
layerImageProvider: LayerImageProvider,
textProvider: AnimationTextProvider,
layerTextProvider: LayerTextProvider,
textProvider: AnimationKeypathTextProvider,
fontProvider: AnimationFontProvider,
frameRate: CGFloat) -> [CompositionLayer]
frameRate: CGFloat,
rootAnimationLayer: MainThreadAnimationLayer?)
-> [CompositionLayer]
{
var compositionLayers = [CompositionLayer]()
var layerMap = [Int : CompositionLayer]()
Expand Down Expand Up @@ -45,10 +48,12 @@ extension Array where Element == LayerModel {
precomp: precompLayer,
asset: precompAsset,
layerImageProvider: layerImageProvider,
layerTextProvider: layerTextProvider,
textProvider: textProvider,
fontProvider: fontProvider,
assetLibrary: assetLibrary,
frameRate: frameRate)
frameRate: frameRate,
rootAnimationLayer: rootAnimationLayer)
compositionLayers.append(precompContainer)
layerMap[layer.index] = precompContainer
} else if
Expand All @@ -62,7 +67,11 @@ extension Array where Element == LayerModel {
compositionLayers.append(imageContainer)
layerMap[layer.index] = imageContainer
} else if let textLayer = layer as? TextLayerModel {
let textContainer = TextCompositionLayer(textLayer: textLayer, textProvider: textProvider, fontProvider: fontProvider)
let textContainer = TextCompositionLayer(
textLayer: textLayer,
textProvider: textProvider,
fontProvider: fontProvider,
rootAnimationLayer: rootAnimationLayer)
compositionLayers.append(textContainer)
layerMap[layer.index] = textContainer
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ final class LayerTextProvider {

// MARK: Lifecycle

init(textProvider: AnimationTextProvider) {
init(textProvider: AnimationKeypathTextProvider) {
self.textProvider = textProvider
textLayers = []
reloadTexts()
Expand All @@ -22,7 +22,7 @@ final class LayerTextProvider {

private(set) var textLayers: [TextCompositionLayer]

var textProvider: AnimationTextProvider {
var textProvider: AnimationKeypathTextProvider {
didSet {
reloadTexts()
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Private/RootAnimationLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ protocol RootAnimationLayer: CALayer {

var _animationLayers: [CALayer] { get }
var imageProvider: AnimationImageProvider { get set }
var textProvider: AnimationTextProvider { get set }
var textProvider: AnimationKeypathTextProvider { get set }
var fontProvider: AnimationFontProvider { get set }

/// The `CAAnimation` key corresponding to the primary animation.
Expand Down
30 changes: 30 additions & 0 deletions Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ extension KeypathSearchable {
return nil
}

/// Searches this layer's keypaths to find the keypath for the given layer
func keypath(for layer: CALayer) -> AnimationKeypath? {
let allKeypaths = layerKeypaths()
return allKeypaths[layer]
}

/// Computes the list of animation keypaths that descend from this layer
func allKeypaths(for keyPath: AnimationKeypath? = nil) -> [String] {
var allKeypaths: [String] = []
Expand All @@ -138,6 +144,30 @@ extension KeypathSearchable {

return allKeypaths
}

/// Computes the list of animation keypaths that descend from this layer
func layerKeypaths(for keyPath: AnimationKeypath? = nil) -> [CALayer: AnimationKeypath] {
Copy link
Member Author

Choose a reason for hiding this comment

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

Adapted from the allKeypaths method above, but just different enough that it didn't seam feasible to consolidate the logic into a single helper

var allKeypaths: [CALayer: AnimationKeypath] = [:]

let newKeypath: AnimationKeypath
if let previousKeypath = keyPath {
newKeypath = previousKeypath.appendingKey(keypathName)
} else {
newKeypath = AnimationKeypath(keys: [keypathName])
}

if let layer = self as? CALayer {
allKeypaths[layer] = newKeypath
}

for child in childKeypaths {
for (layer, keypath) in child.layerKeypaths(for: newKeypath) {
allKeypaths[layer] = keypath
}
}

return allKeypaths
}
}

extension AnimationKeypath {
Expand Down
Loading