Skip to content

Commit

Permalink
Add support for nested Group items (#1507)
Browse files Browse the repository at this point in the history
  • Loading branch information
calda authored Feb 1, 2022
1 parent 9ae7c84 commit c2c1558
Show file tree
Hide file tree
Showing 24 changed files with 132 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import QuartzCore

extension CAShapeLayer {
/// Adds animations for the given `[BezierPath]` keyframes to this `CALayer`
/// Adds animations for the given `CombinedShapeItem` to this `CALayer`
@nonobjc
func addAnimations(
for combinedShapes: CombinedShapeItem,
Expand Down
183 changes: 130 additions & 53 deletions Sources/Private/Experimental/Layers/ShapeLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,63 +13,42 @@ final class ShapeLayer: BaseCompositionLayer {
init(shapeLayer: ShapeLayerModel) {
self.shapeLayer = shapeLayer
super.init(layerModel: shapeLayer)
setupGroups(from: shapeLayer.items, parentGroup: nil)
}

// Each top-level `Group` item becomes its own `ShapeItemLayer` sublayer.
// Other top-level `ShapeItem`s are applied to all sublayers.
let groupItems = shapeLayer.items.compactMap { $0 as? Group }
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

let otherItems = shapeLayer.items
.filter { !($0 is Group) }
.map { ShapeItemLayer.Item(item: $0, parentGroup: nil) }
/// Called by CoreAnimation to create a shadow copy of this layer
/// More details: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
override init(layer: Any) {
guard let typedLayer = layer as? Self else {
fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))")
}

// Groups are listed from front to back,
// but `CALayer.sublayers` are listed from back to front.
let groupsInZAxisOrder = groupItems.reversed()
shapeLayer = typedLayer.shapeLayer
super.init(layer: typedLayer)
}

for group in groupsInZAxisOrder {
let itemsInGroup = group.items.map { ShapeItemLayer.Item(item: $0, parentGroup: group) }
+ otherItems

let pathDrawingItemsInGroup = itemsInGroup.filter { $0.item.drawsCGPath }
let otherItemsInGroup = itemsInGroup.filter { !$0.item.drawsCGPath }

// If all of the path-drawing `ShapeItem`s have keyframes with the same timing information,
// we can combine the `[KeyframeGroup<BezierPath>]` (which have to animate in separate layers)
// into a single `KeyframeGroup<[BezierPath]>`, which can be combined into a single CGPath animation.
//
// This is how Groups with multiple path-drawing items are supposed to be rendered,
// because combing multiple paths into a single `CGPath` (instead of render them in separate layers)
// allows `CAShapeLayerFillRule.evenOdd` to be applied if the paths overlap. We just can't do this
// in all cases, due to limitations of Core Animation.
if
pathDrawingItemsInGroup.count > 1,
let combinedShapeKeyframes = Keyframes.combinedIfPossible(pathDrawingItemsInGroup.map {
($0.item as? Shape)?.path
}),
// `Trim`s are currently only applied correctly using individual `ShapeItemLayer`s,
// because each path has to be trimmed separately.
!otherItemsInGroup.contains(where: { $0.item.type == .trim })
{
let combinedShape = CombinedShapeItem(
shapes: combinedShapeKeyframes,
name: group.name)

let sublayer = ShapeItemLayer(
shape: ShapeItemLayer.Item(item: combinedShape, parentGroup: group),
otherItems: otherItemsInGroup)
// MARK: Private

addSublayer(sublayer)
}
private let shapeLayer: ShapeLayerModel

// Otherwise, if each `ShapeItem` that draws a `GGPath` animates independently,
// we have to create a separate `ShapeItemLayer` for each one.
else {
for pathDrawingItem in pathDrawingItemsInGroup {
let sublayer = ShapeItemLayer(shape: pathDrawingItem, otherItems: otherItemsInGroup)
addSublayer(sublayer)
}
}
}
}

// MARK: - GroupLayer

/// The CALayer type responsible for rendering `Group`s
final class GroupLayer: BaseAnimationLayer {

// MARK: Lifecycle

init(group: Group, inheritedItems: [ShapeItemLayer.Item]) {
self.group = group
self.inheritedItems = inheritedItems
super.init()
setupLayerHierarchy()
}

required init?(coder _: NSCoder) {
Expand All @@ -83,14 +62,94 @@ final class ShapeLayer: BaseCompositionLayer {
fatalError("\(Self.self).init(layer:) incorrectly called with \(type(of: layer))")
}

shapeLayer = typedLayer.shapeLayer
group = typedLayer.group
inheritedItems = typedLayer.inheritedItems
super.init(layer: typedLayer)
}

// MARK: Private

private let shapeLayer: ShapeLayerModel
private let group: Group

/// `ShapeItem`s that were listed in the parent's `items: [ShapeItem]` array
/// - This layer's parent is either the root `ShapeLayerModel` or some other `Group`
private let inheritedItems: [ShapeItemLayer.Item]

private func setupLayerHierarchy() {
// Groups can contain other groups, so we may have to continue
// recursively creating more `GroupLayer`s
setupGroups(from: group.items, parentGroup: group)

let nonGroupItems = group.items
.filter { !($0 is Group) }
.map { ShapeItemLayer.Item(item: $0, parentGroup: group) }
+ inheritedItems

let (pathDrawingItemsInGroup, otherItemsInGroup) = nonGroupItems.grouped(by: \.item.drawsCGPath)

// If all of the path-drawing `ShapeItem`s have keyframes with the same timing information,
// we can combine the `[KeyframeGroup<BezierPath>]` (which have to animate in separate layers)
// into a single `KeyframeGroup<[BezierPath]>`, which can be combined into a single CGPath animation.
//
// This is how Groups with multiple path-drawing items are supposed to be rendered,
// because combining multiple paths into a single `CGPath` (instead of rendering them in separate layers)
// allows `CAShapeLayerFillRule.evenOdd` to be applied if the paths overlap. We just can't do this
// in all cases, due to limitations of Core Animation.
if
pathDrawingItemsInGroup.count > 1,
let combinedShapeKeyframes = Keyframes.combinedIfPossible(pathDrawingItemsInGroup.map {
($0.item as? Shape)?.path
}),
// `Trim`s are currently only applied correctly using individual `ShapeItemLayer`s,
// because each path has to be trimmed separately.
!otherItemsInGroup.contains(where: { $0.item.type == .trim })
{
let combinedShape = CombinedShapeItem(
shapes: combinedShapeKeyframes,
name: group.name)

let sublayer = ShapeItemLayer(
shape: ShapeItemLayer.Item(item: combinedShape, parentGroup: group),
otherItems: otherItemsInGroup)

addSublayer(sublayer)
}

// Otherwise, if each `ShapeItem` that draws a `GGPath` animates independently,
// we have to create a separate `ShapeItemLayer` for each one.
else {
for pathDrawingItem in pathDrawingItemsInGroup {
let sublayer = ShapeItemLayer(shape: pathDrawingItem, otherItems: otherItemsInGroup)
addSublayer(sublayer)
}
}
}

}

extension CALayer {
/// Sets up `GroupLayer`s for each `Group` in the given list of `ShapeItem`s
/// - Each `Group` item becomes its own `GroupLayer` sublayer.
/// - Other `ShapeItem` are applied to all sublayers
fileprivate func setupGroups(from items: [ShapeItem], parentGroup: Group?) {
let (groupItems, otherItems) = items.grouped(by: { $0 is Group })

// Groups are listed from front to back,
// but `CALayer.sublayers` are listed from back to front.
let groupsInZAxisOrder = groupItems.reversed()

for group in groupsInZAxisOrder {
guard let group = group as? Group else { continue }

let groupLayer = GroupLayer(
group: group,
inheritedItems: Array(otherItems.map {
ShapeItemLayer.Item(item: $0, parentGroup: parentGroup)
}))

addSublayer(groupLayer)
}
}
}

extension ShapeItem {
Expand All @@ -106,3 +165,21 @@ extension ShapeItem {
}
}
}

extension Collection {
/// Splits this collection into two groups, based on the given predicate
func grouped(by predicate: (Element) -> Bool) -> (trueElements: [Element], falseElements: [Element]) {
var trueElements = [Element]()
var falseElements = [Element]()

for element in self {
if predicate(element) {
trueElements.append(element)
} else {
falseElements.append(element)
}
}

return (trueElements, falseElements)
}
}
1 change: 1 addition & 0 deletions Tests/Samples/LottieFiles/settings_slider.json

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit c2c1558

Please sign in to comment.