import Foundation import UIKit import UIKitRuntimeUtils public protocol Interpolatable { static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> (Interpolatable) } private func floorToPixels(_ value: CGFloat) -> CGFloat { return value } private func floorToPixels(_ value: CGPoint) -> CGPoint { return CGPoint(x: floorToPixels(value.x), y: floorToPixels(value.y)) } private func floorToPixels(_ value: CGSize) -> CGSize { return CGSize(width: floorToPixels(value.width), height: floorToPixels(value.height)) } private func floorToPixels(_ value: CGRect) -> CGRect { return CGRect(origin: floorToPixels(value.origin), size: floorToPixels(value.size)) } private func floorToPixels(_ value: UIEdgeInsets) -> UIEdgeInsets { return UIEdgeInsets(top: floorToPixels(value.top), left: floorToPixels(value.left), bottom: floorToPixels(value.bottom), right: floorToPixels(value.right)) } extension CGFloat: Interpolatable { public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable { return { from, to, t -> Interpolatable in let fromValue: CGFloat = from as! CGFloat let toValue: CGFloat = to as! CGFloat let invT: CGFloat = 1.0 - t let term: CGFloat = toValue * t + fromValue * invT return floorToPixels(term) } } static func interpolate(from fromValue: CGFloat, to toValue: CGFloat, at t: CGFloat) -> CGFloat { let invT: CGFloat = 1.0 - t let term: CGFloat = toValue * t + fromValue * invT return term } } extension UIEdgeInsets: Interpolatable { public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable { return { from, to, t -> Interpolatable in let fromValue = from as! UIEdgeInsets let toValue = to as! UIEdgeInsets return floorToPixels(UIEdgeInsets(top: toValue.top * t + fromValue.top * (1.0 - t), left: toValue.left * t + fromValue.left * (1.0 - t), bottom: toValue.bottom * t + fromValue.bottom * (1.0 - t), right: toValue.right * t + fromValue.right * (1.0 - t))) } } } 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))) } } static func interpolate(from fromValue: CGRect, to toValue: CGRect, at t: CGFloat) -> CGRect { return CGRect(origin: CGPoint.interpolate(from: fromValue.origin, to: toValue.origin, at: t), size: CGSize.interpolate(from: fromValue.size, to: toValue.size, at: t)) } } extension CGPoint: Interpolatable { public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable { return { from, to, t -> Interpolatable in let fromValue = from as! CGPoint let toValue = to as! CGPoint return floorToPixels(CGPoint(x: toValue.x * t + fromValue.x * (1.0 - t), y: toValue.y * t + fromValue.y * (1.0 - t))) } } static func interpolate(from fromValue: CGPoint, to toValue: CGPoint, at t: CGFloat) -> CGPoint { return CGPoint(x: toValue.x * t + fromValue.x * (1.0 - t), y: toValue.y * t + fromValue.y * (1.0 - t)) } } extension CGSize { static func interpolate(from fromValue: CGSize, to toValue: CGSize, at t: CGFloat) -> CGSize { return CGSize(width: toValue.width * t + fromValue.width * (1.0 - t), height: toValue.height * t + fromValue.height * (1.0 - t)) } } private let springAnimationIn: CABasicAnimation = { let animation = makeSpringAnimation("") return animation }() let springAnimationSolver: (CGFloat) -> CGFloat = { () -> (CGFloat) -> CGFloat in if #available(iOS 9.0, *) { return { t in return springAnimationValueAt(springAnimationIn, t) } } else { return { t in return bezierPoint(0.23, 1.0, 0.32, 1.0, t) } } }() public let listViewAnimationCurveSystem: (CGFloat) -> CGFloat = { t in return springAnimationSolver(t) } public let listViewAnimationCurveLinear: (CGFloat) -> CGFloat = { t in return t } public let listViewAnimationCurveEaseInOut: (CGFloat) -> CGFloat = { t in return bezierPoint(0.42, 0.0, 0.58, 1.0, t) } #if os(iOS) public func listViewAnimationCurveFromAnimationOptions(animationOptions: UIView.AnimationOptions) -> (CGFloat) -> CGFloat { if animationOptions.rawValue == UInt(7 << 16) { return listViewAnimationCurveSystem } else { return listViewAnimationCurveLinear } } #endif public final class ListViewAnimation { let from: Interpolatable let to: Interpolatable let duration: Double let startTime: Double let invertOffsetDirection: Bool private let curve: (CGFloat) -> CGFloat private let interpolator: (Interpolatable, Interpolatable, CGFloat) -> Interpolatable private let update: (CGFloat, Interpolatable) -> Void private let completed: (Bool) -> Void public init(from: T, to: T, duration: Double, invertOffsetDirection: Bool = false, curve: @escaping (CGFloat) -> CGFloat, beginAt: Double, update: @escaping (CGFloat, T) -> Void, completed: @escaping (Bool) -> Void = { _ in }) { self.from = from self.to = to self.duration = duration self.invertOffsetDirection = invertOffsetDirection self.curve = curve self.startTime = beginAt self.interpolator = T.interpolator() self.update = { progress, value in update(progress, value as! T) } self.completed = completed } init(copying: ListViewAnimation, update: @escaping (CGFloat, T) -> Void, completed: @escaping (Bool) -> Void = { _ in }) { self.from = copying.from self.to = copying.to self.duration = copying.duration self.curve = copying.curve self.startTime = copying.startTime self.interpolator = copying.interpolator self.invertOffsetDirection = copying.invertOffsetDirection self.update = { progress, value in update(progress, value as! T) } self.completed = completed } public func completeAt(_ timestamp: Double) -> Bool { if timestamp >= self.startTime + self.duration { self.completed(true) return true } else { return false } } public func cancel() { self.completed(false) } private func valueAt(_ t: CGFloat) -> Interpolatable { if t <= 0.0 { return self.from } else if t >= 1.0 { return self.to } else { return self.interpolator(self.from, self.to, t) } } public func applyAt(_ timestamp: Double) { var t = CGFloat((timestamp - self.startTime) / self.duration) let ct: CGFloat if t <= 0.0 + CGFloat.ulpOfOne { t = 0.0 ct = 0.0 } else if t >= 1.0 - CGFloat.ulpOfOne { t = 1.0 ct = 1.0 } else { ct = self.curve(t) } self.update(ct, self.valueAt(ct)) } } public func listViewAnimationDurationAndCurve(transition: ContainedViewLayoutTransition) -> (Double, ListViewAnimationCurve) { switch transition { case .immediate: return (0.0, .Default(duration: 0.0)) case let .animated(animationDuration, animationCurve): switch animationCurve { case .linear: return (animationDuration, .Default(duration: animationDuration)) case .easeInOut: return (animationDuration, .Default(duration: animationDuration)) case .spring, .customSpring: return (animationDuration, .Spring(duration: animationDuration)) case let .custom(c1x, c1y, c2x, c2y): return (animationDuration, .Custom(duration: animationDuration, c1x, c1y, c2x, c2y)) } } } public func scrollingRubberBandingOffset(offset: CGFloat, bandingStart: CGFloat, range: CGFloat, coefficient: CGFloat = 0.4) -> CGFloat { let bandedOffset = offset - bandingStart return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range }