diff --git a/Display/CAAnimationUtils.swift b/Display/CAAnimationUtils.swift index 0734a055a9..d28b1d5df9 100644 --- a/Display/CAAnimationUtils.swift +++ b/Display/CAAnimationUtils.swift @@ -17,6 +17,7 @@ import UIKit } private let completionKey = "CAAnimationUtils_completion" +private let springKey = "CAAnimationUtilsSpringCurve" public extension CAAnimation { public var completion: (Bool -> Void)? { @@ -38,27 +39,50 @@ public extension CAAnimation { public extension CALayer { public func animate(from from: NSValue, to: NSValue, keyPath: String, timingFunction: String, duration: NSTimeInterval, removeOnCompletion: Bool = true, completion: (Bool -> Void)? = nil) { - let k = Float(UIView.animationDurationFactor()) - var speed: Float = 1.0 - if k != 0 && k != 1 { - speed = Float(1.0) / k + if timingFunction == springKey { + let animation = CASpringAnimation(keyPath: keyPath) + animation.fromValue = from + animation.toValue = to + animation.damping = 500.0 + animation.stiffness = 1000.0 + animation.mass = 3.0 + animation.duration = animation.settlingDuration + animation.removedOnCompletion = removeOnCompletion + animation.fillMode = kCAFillModeForwards + if let completion = completion { + animation.delegate = CALayerAnimationDelegate(completion: completion) + } + + let k = Float(UIView.animationDurationFactor()) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + + animation.speed = speed * Float(animation.duration / duration) + + self.addAnimation(animation, forKey: keyPath) + } else { + let k = Float(UIView.animationDurationFactor()) + var speed: Float = 1.0 + if k != 0 && k != 1 { + speed = Float(1.0) / k + } + + let animation = CABasicAnimation(keyPath: keyPath) + animation.fromValue = from + animation.toValue = to + animation.duration = duration + animation.timingFunction = CAMediaTimingFunction(name: timingFunction) + animation.removedOnCompletion = removeOnCompletion + animation.fillMode = kCAFillModeForwards + animation.speed = speed + if let completion = completion { + animation.delegate = CALayerAnimationDelegate(completion: completion) + } + + self.addAnimation(animation, forKey: keyPath) } - - let animation = CABasicAnimation(keyPath: keyPath) - animation.fromValue = from - animation.toValue = to - animation.duration = duration - animation.timingFunction = CAMediaTimingFunction(name: timingFunction) - animation.removedOnCompletion = removeOnCompletion - animation.fillMode = kCAFillModeForwards - animation.speed = speed - if let completion = completion { - animation.delegate = CALayerAnimationDelegate(completion: completion) - } - - self.addAnimation(animation, forKey: keyPath) - - //self.setValue(to, forKey: keyPath) } public func animateAdditive(from from: NSValue, to: NSValue, keyPath: String, key: String, timingFunction: String, duration: NSTimeInterval, removeOnCompletion: Bool = true, completion: (Bool -> Void)? = nil) { @@ -93,10 +117,28 @@ public extension CALayer { } internal func animatePosition(from from: CGPoint, to: CGPoint, duration: NSTimeInterval, completion: (Bool -> Void)? = nil) { - self.animate(from: NSValue(CGPoint: from), to: NSValue(CGPoint: to), keyPath: "position", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: duration, removeOnCompletion: true, completion: completion) + if from == to { + return + } + self.animate(from: NSValue(CGPoint: from), to: NSValue(CGPoint: to), keyPath: "position", timingFunction: springKey, duration: duration, removeOnCompletion: true, completion: completion) + } + + internal func animateBounds(from from: CGRect, to: CGRect, duration: NSTimeInterval, completion: (Bool -> Void)? = nil) { + if from == to { + return + } + self.animate(from: NSValue(CGRect: from), to: NSValue(CGRect: to), keyPath: "bounds", timingFunction: springKey, duration: duration, removeOnCompletion: true, completion: completion) } public func animateBoundsOriginYAdditive(from from: CGFloat, to: CGFloat, duration: NSTimeInterval) { self.animateAdditive(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.y", key: "boundsOriginYAdditive", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: duration, removeOnCompletion: true) } + + public func animateFrame(from from: CGRect, to: CGRect, duration: NSTimeInterval, spring: Bool = false, completion: (Bool -> Void)? = nil) { + if from == to { + return + } + self.animatePosition(from: CGPoint(x: from.midX, y: from.midY), to: CGPoint(x: to.midX, y: to.midY), duration: duration, completion: nil) + self.animateBounds(from: CGRect(origin: self.bounds.origin, size: from.size), to: CGRect(origin: self.bounds.origin, size: to.size), duration: duration, completion: completion) + } } diff --git a/Display/ListView.swift b/Display/ListView.swift index 1f71c9a0a1..8889e79b70 100644 --- a/Display/ListView.swift +++ b/Display/ListView.swift @@ -1269,7 +1269,7 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { }) } - private func nodeForItem(synchronous: Bool, item: ListViewItem, previousNode: ListViewItemNode?, index: Int, previousItem: ListViewItem?, nextItem: ListViewItem?, width: CGFloat, completion: (ListViewItemNode, ListViewItemNodeLayout, () -> Void) -> Void) { + private func nodeForItem(synchronous: Bool, item: ListViewItem, previousNode: ListViewItemNode?, index: Int, previousItem: ListViewItem?, nextItem: ListViewItem?, width: CGFloat, updateAnimation: ListViewItemUpdateAnimation, completion: (ListViewItemNode, ListViewItemNodeLayout, () -> Void) -> Void) { if let previousNode = previousNode { item.updateNode({ f in if synchronous { @@ -1277,7 +1277,7 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { } else { self.async(f) } - }, node: previousNode, width: width, previousItem: previousItem, nextItem: nextItem, completion: { (layout, apply) in + }, node: previousNode, width: width, previousItem: previousItem, nextItem: nextItem, animation: updateAnimation, completion: { (layout, apply) in if NSThread.isMainThread() { if synchronous { completion(previousNode, layout, { @@ -1637,7 +1637,7 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { } else { self.async(f) } - }, node: referenceNode, width: state.visibleSize.width, previousItem: index == 0 ? nil : self.items[index - 1], nextItem: index == self.items.count - 1 ? nil : self.items[index + 1], completion: { layout, apply in + }, node: referenceNode, width: state.visibleSize.width, previousItem: index == 0 ? nil : self.items[index - 1], nextItem: index == self.items.count - 1 ? nil : self.items[index + 1], animation: .None, completion: { layout, apply in var updatedState = state var updatedOperations = operations @@ -1670,6 +1670,7 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { var previousNodes = inputPreviousNodes var operations = inputOperations let completion = inputCompletion + let updateAnimation: ListViewItemUpdateAnimation = animated ? .System(duration: insertionAnimationDuration) : .None while true { if self.items.count == 0 { @@ -1694,7 +1695,7 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { let index = insertionItemIndexAndDirection.0 let threadId = pthread_self() var tailRecurse = false - self.nodeForItem(synchronous, item: self.items[index], previousNode: previousNodes[index], index: index, previousItem: index == 0 ? nil : self.items[index - 1], nextItem: self.items.count == index + 1 ? nil : self.items[index + 1], width: state.visibleSize.width, completion: { (node, layout, apply) in + self.nodeForItem(synchronous, item: self.items[index], previousNode: previousNodes[index], index: index, previousItem: index == 0 ? nil : self.items[index - 1], nextItem: self.items.count == index + 1 ? nil : self.items[index + 1], width: state.visibleSize.width, updateAnimation: updateAnimation, completion: { (node, layout, apply) in if pthread_equal(pthread_self(), threadId) != 0 && !tailRecurse { tailRecurse = true @@ -1749,6 +1750,10 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { let previousApparentHeight = node.apparentHeight let previousInsets = node.insets + if node.wantsScrollDynamics && previousFrame != nil { + assert(true) + } + node.contentSize = layout.contentSize node.insets = layout.insets node.apparentHeight = animated ? 0.0 : layout.size.height @@ -1787,6 +1792,16 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { nextNode.removeApparentHeightAnimation() takenAnimation = true + + if abs(layout.size.height - previousApparentHeight) > CGFloat(FLT_EPSILON) { + node.addApparentHeightAnimation(layout.size.height, duration: insertionAnimationDuration, beginAt: timestamp, update: { [weak node] progress in + if let node = node { + node.animateFrameTransition(progress) + } + }) + node.transitionOffset = previousApparentHeight - layout.size.height + node.addTransitionOffsetAnimation(0.0, duration: insertionAnimationDuration, beginAt: timestamp) + } } } } @@ -2285,7 +2300,9 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate { updatedAccessoryItemNodeOrigin.y += updatedParentOrigin.y updatedAccessoryItemNodeOrigin.y -= itemNode.bounds.origin.y - nextAccessoryItemNode.animateTransitionOffset(CGPoint(x: 0.0, y: updatedAccessoryItemNodeOrigin.y - previousAccessoryItemNodeOrigin.y), beginAt: currentTimestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: listViewAnimationCurveSystem) + let deltaHeight = itemNode.frame.size.height - nextItemNode.frame.size.height + + nextAccessoryItemNode.animateTransitionOffset(CGPoint(x: 0.0, y: updatedAccessoryItemNodeOrigin.y - previousAccessoryItemNodeOrigin.y - deltaHeight), beginAt: currentTimestamp, duration: insertionAnimationDuration * UIView.animationDurationFactor(), curve: listViewAnimationCurveSystem) } } else { break diff --git a/Display/ListViewAccessoryItemNode.swift b/Display/ListViewAccessoryItemNode.swift index 28f365ddd1..b25d22757a 100644 --- a/Display/ListViewAccessoryItemNode.swift +++ b/Display/ListViewAccessoryItemNode.swift @@ -12,7 +12,7 @@ public class ListViewAccessoryItemNode: ASDisplayNode { final func animateTransitionOffset(from: CGPoint, beginAt: Double, duration: Double, curve: CGFloat -> CGFloat) { self.transitionOffset = from - self.transitionOffsetAnimation = ListViewAnimation(from: from, to: CGPoint(), duration: duration, curve: curve, beginAt: beginAt, update: { [weak self] currentValue in + self.transitionOffsetAnimation = ListViewAnimation(from: from, to: CGPoint(), duration: duration, curve: curve, beginAt: beginAt, update: { [weak self] _, currentValue in if let strongSelf = self { strongSelf.transitionOffset = currentValue } diff --git a/Display/ListViewAnimation.swift b/Display/ListViewAnimation.swift index 2eb56d288d..36d2a4167e 100644 --- a/Display/ListViewAnimation.swift +++ b/Display/ListViewAnimation.swift @@ -43,6 +43,16 @@ extension UIEdgeInsets: Interpolatable { } } +extension CGRect: Interpolatable { + public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable { + return { from, to, t -> Interpolatable in + let fromValue = from as! CGRect + let toValue = to as! CGRect + return floorToPixels(CGRect(x: toValue.origin.x * t + fromValue.origin.x * (1.0 - t), y: toValue.origin.y * t + fromValue.origin.y * (1.0 - t), width: toValue.size.width * t + fromValue.size.width * (1.0 - t), height: toValue.size.height * t + fromValue.size.height * (1.0 - t))) + } + } +} + extension CGPoint: Interpolatable { public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable { return { from, to, t -> Interpolatable in @@ -55,11 +65,10 @@ extension CGPoint: Interpolatable { private let springAnimationIn: CASpringAnimation = { let animation = CASpringAnimation() - animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) - animation.duration = 0.6 animation.damping = 500.0 animation.stiffness = 1000.0 animation.mass = 3.0 + animation.duration = animation.settlingDuration return animation }() @@ -87,18 +96,18 @@ public final class ListViewAnimation { let startTime: Double private let curve: CGFloat -> CGFloat private let interpolator: (Interpolatable, Interpolatable, CGFloat) -> Interpolatable - private let update: Interpolatable -> Void + private let update: (CGFloat, Interpolatable) -> Void private let completed: Bool -> Void - public init(from: T, to: T, duration: Double, curve: CGFloat -> CGFloat, beginAt: Double, update: T -> Void, completed: Bool -> Void = { _ in }) { + public init(from: T, to: T, duration: Double, curve: CGFloat -> CGFloat, beginAt: Double, update: (CGFloat, T) -> Void, completed: Bool -> Void = { _ in }) { self.from = from self.to = to self.duration = duration self.curve = curve self.startTime = beginAt self.interpolator = T.interpolator() - self.update = { value in - update(value as! T) + self.update = { progress, value in + update(progress, value as! T) } self.completed = completed } @@ -116,21 +125,28 @@ public final class ListViewAnimation { self.completed(false) } - private func valueAt(timestamp: Double) -> Interpolatable { - if timestamp < self.startTime { + private func valueAt(t: CGFloat) -> Interpolatable { + if t <= 0.0 { return self.from - } - - let t = CGFloat((timestamp - self.startTime) / self.duration) - - if t >= 1.0 { + } else if t >= 1.0 { return self.to } else { - return self.interpolator(self.from, self.to, self.curve(t)) + return self.interpolator(self.from, self.to, t) } } public func applyAt(timestamp: Double) { - self.update(self.valueAt(timestamp)) + var t = CGFloat((timestamp - self.startTime) / self.duration) + let ct: CGFloat + if t <= 0.0 + CGFloat(FLT_EPSILON) { + t = 0.0 + ct = 0.0 + } else if t >= 1.0 - CGFloat(FLT_EPSILON) { + t = 1.0 + ct = 1.0 + } else { + ct = self.curve(t) + } + self.update(ct, self.valueAt(ct)) } } diff --git a/Display/ListViewItem.swift b/Display/ListViewItem.swift index 494cad4e2e..5fff85d05b 100644 --- a/Display/ListViewItem.swift +++ b/Display/ListViewItem.swift @@ -1,9 +1,14 @@ import Foundation import SwiftSignalKit +public enum ListViewItemUpdateAnimation { + case None + case System(duration: Double) +} + public protocol ListViewItem { func nodeConfiguredForWidth(async: (() -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: (ListViewItemNode, () -> Void) -> Void) - func updateNode(async: (() -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: (ListViewItemNodeLayout, () -> Void) -> Void) + func updateNode(async: (() -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: (ListViewItemNodeLayout, () -> Void) -> Void) var accessoryItem: ListViewAccessoryItem? { get } var headerAccessoryItem: ListViewAccessoryItem? { get } @@ -28,7 +33,7 @@ public extension ListViewItem { func selected() { } - func updateNode(async: (() -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: (ListViewItemNodeLayout, () -> Void) -> Void) { + func updateNode(async: (() -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: (ListViewItemNodeLayout, () -> Void) -> Void) { completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), {}) } } diff --git a/Display/ListViewItemNode.swift b/Display/ListViewItemNode.swift index 6aaa392f43..d3c0e373ea 100644 --- a/Display/ListViewItemNode.swift +++ b/Display/ListViewItemNode.swift @@ -334,7 +334,7 @@ public class ListViewItemNode: ASDisplayNode { } public func addInsetsAnimationToValue(value: UIEdgeInsets, duration: Double, beginAt: Double) { - let animation = ListViewAnimation(from: self.insets, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] currentValue in + let animation = ListViewAnimation(from: self.insets, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] _, currentValue in if let strongSelf = self { strongSelf.insets = currentValue } @@ -342,10 +342,13 @@ public class ListViewItemNode: ASDisplayNode { self.setAnimationForKey("insets", animation: animation) } - public func addApparentHeightAnimation(value: CGFloat, duration: Double, beginAt: Double) { - let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] currentValue in + public func addApparentHeightAnimation(value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat) -> Void)? = nil) { + let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] progress, currentValue in if let strongSelf = self { strongSelf.apparentHeight = currentValue + if let update = update { + update(progress) + } } }) self.setAnimationForKey("apparentHeight", animation: animation) @@ -358,7 +361,7 @@ public class ListViewItemNode: ASDisplayNode { duration = 0.0 } - let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] currentValue in + let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] _, currentValue in if let strongSelf = self { strongSelf.apparentHeight = currentValue } @@ -373,7 +376,7 @@ public class ListViewItemNode: ASDisplayNode { } public func addTransitionOffsetAnimation(value: CGFloat, duration: Double, beginAt: Double) { - let animation = ListViewAnimation(from: self.transitionOffset, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] currentValue in + let animation = ListViewAnimation(from: self.transitionOffset, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] _, currentValue in if let strongSelf = self { strongSelf.transitionOffset = currentValue } @@ -392,4 +395,8 @@ public class ListViewItemNode: ASDisplayNode { public func setupGestures() { } + + public func animateFrameTransition(progress: CGFloat) { + + } }