Commit 55e5e287 by Daniel Dahan

progress commit for MotionCoreAnimationViewContext

parent 7b809595
......@@ -13,7 +13,7 @@
965FE9671FDD99800098BDD0 /* Motion+Start.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965FE9661FDD99800098BDD0 /* Motion+Start.swift */; };
965FE9691FDDA1F20098BDD0 /* MotionViewOrderStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965FE9681FDDA1F20098BDD0 /* MotionViewOrderStrategy.swift */; };
965FE96B1FDDA4EA0098BDD0 /* BaseMotionPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965FE96A1FDDA4EA0098BDD0 /* BaseMotionPreprocessor.swift */; };
965FE96D1FDDA6400098BDD0 /* BaseMotionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965FE96C1FDDA6400098BDD0 /* BaseMotionAnimator.swift */; };
965FE96D1FDDA6400098BDD0 /* MotionCoreAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965FE96C1FDDA6400098BDD0 /* MotionCoreAnimator.swift */; };
965FE96F1FDEFA8B0098BDD0 /* Motion+Animate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965FE96E1FDEFA8B0098BDD0 /* Motion+Animate.swift */; };
96E409651F24F7370015A2B5 /* MotionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E4093D1F24F7370015A2B5 /* MotionAnimator.swift */; };
96E409661F24F7370015A2B5 /* MotionAnimatorViewContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E4093E1F24F7370015A2B5 /* MotionAnimatorViewContext.swift */; };
......@@ -91,7 +91,7 @@
965FE9661FDD99800098BDD0 /* Motion+Start.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Motion+Start.swift"; sourceTree = "<group>"; };
965FE9681FDDA1F20098BDD0 /* MotionViewOrderStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionViewOrderStrategy.swift; sourceTree = "<group>"; };
965FE96A1FDDA4EA0098BDD0 /* BaseMotionPreprocessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMotionPreprocessor.swift; sourceTree = "<group>"; };
965FE96C1FDDA6400098BDD0 /* BaseMotionAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMotionAnimator.swift; sourceTree = "<group>"; };
965FE96C1FDDA6400098BDD0 /* MotionCoreAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionCoreAnimator.swift; sourceTree = "<group>"; };
965FE96E1FDEFA8B0098BDD0 /* Motion+Animate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Motion+Animate.swift"; sourceTree = "<group>"; };
96C98DD11E424AB000B22906 /* Motion.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Motion.framework; sourceTree = BUILT_PRODUCTS_DIR; };
96E4093D1F24F7370015A2B5 /* MotionAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionAnimator.swift; sourceTree = "<group>"; };
......@@ -183,7 +183,7 @@
isa = PBXGroup;
children = (
96E4093D1F24F7370015A2B5 /* MotionAnimator.swift */,
965FE96C1FDDA6400098BDD0 /* BaseMotionAnimator.swift */,
965FE96C1FDDA6400098BDD0 /* MotionCoreAnimator.swift */,
96E4093E1F24F7370015A2B5 /* MotionAnimatorViewContext.swift */,
96E4093F1F24F7370015A2B5 /* MotionCoreAnimationViewContext.swift */,
96E409401F24F7370015A2B5 /* MotionHasInsertOrder.swift */,
......@@ -332,7 +332,7 @@
965FE9631FDCCE030098BDD0 /* Motion+Complete.swift in Sources */,
96E4097D1F24F7370015A2B5 /* MotionPlugin.swift in Sources */,
96E409681F24F7370015A2B5 /* MotionHasInsertOrder.swift in Sources */,
965FE96D1FDDA6400098BDD0 /* BaseMotionAnimator.swift in Sources */,
965FE96D1FDDA6400098BDD0 /* MotionCoreAnimator.swift in Sources */,
96E4096E1F24F7370015A2B5 /* Motion+CG.swift in Sources */,
96E409851F24F7370015A2B5 /* MatchPreprocessor.swift in Sources */,
96E409861F24F7370015A2B5 /* MotionPreprocessor.swift in Sources */,
......
......@@ -30,17 +30,20 @@ import UIKit
internal class MotionAnimatorViewContext {
/// An optional reference to a MotionAnimator.
var animator: BaseMotionAnimator?
var animator: MotionCoreAnimator?
/// A reference to the snapshot UIView.
var snapshot: UIView
/// Animation duration time.
var duration: TimeInterval = 0
/// The animation target state.
var targetState: MotionTransitionState
/// A boolean indicating if the view is appearing.
var isAppearing: Bool
/// Animation duration time.
var duration: TimeInterval = 0
/// The computed current time of the snapshot layer.
var currentTime: TimeInterval {
return snapshot.layer.convertTime(CACurrentMediaTime(), from: nil)
......@@ -56,11 +59,13 @@ internal class MotionAnimatorViewContext {
- Parameter animator: A MotionAnimator.
- Parameter snapshot: A UIView.
- Parameter targetState: A MotionTransitionState.
- Parameter isAppearing: A Boolean.
*/
required init(animator: BaseMotionAnimator, snapshot: UIView, targetState: MotionTransitionState) {
required init(animator: MotionCoreAnimator, snapshot: UIView, targetState: MotionTransitionState, isAppearing: Bool) {
self.animator = animator
self.snapshot = snapshot
self.targetState = targetState
self.isAppearing = isAppearing
}
/// Cleans the context.
......@@ -86,8 +91,11 @@ internal class MotionAnimatorViewContext {
- Parameter at elapsedTime: A TimeInterval.
- Parameter isReversed: A boolean to reverse the animation
or not.
- Returns: A TimeInterval.
*/
func resume(at elapsedTime: TimeInterval, isReversed: Bool) {}
func resume(at elapsedTime: TimeInterval, isReversed: Bool) -> TimeInterval {
return 0
}
/**
Moves the animation to the given elapsed time.
......@@ -106,5 +114,8 @@ internal class MotionAnimatorViewContext {
- Parameter isAppearing: A boolean value whether the view
is appearing or not.
*/
func startAnimations(isAppearing: Bool) {}
@discardableResult
func startAnimations() -> TimeInterval {
return 0
}
}
......@@ -35,6 +35,9 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext {
/// A reference to the animation timing function.
fileprivate var timingFunction = CAMediaTimingFunction.standard
/// Current animations.
var animations = [(CALayer, String, CAAnimation)]()
/// Layer which holds the content.
fileprivate var contentLayer: CALayer? {
return snapshot.layer.sublayers?.get(0)
......@@ -68,40 +71,60 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext {
}
override func apply(state: MotionTransitionState) {
let ts = viewState(targetState: state)
let targetState = viewState(targetState: state)
for (key, targetValue) in ts {
for (key, targetValue) in targetState {
if nil == transitionStates[key] {
let current = currentValue(for: key)
transitionStates[key] = (current, current)
}
animate(key: key, beginTime: 0, fromValue: targetValue, toValue: targetValue)
let oldAnimations = animations
animations = []
animate(key: key, beginTime: 0, duration: 100, fromValue: targetValue, toValue: targetValue)
animations = oldAnimations
}
}
override func resume(at elapsedTime: TimeInterval, isReversed: Bool) {
override func resume(at elapsedTime: TimeInterval, isReversed: Bool) -> TimeInterval {
for (key, (fromValue, toValue)) in transitionStates {
transitionStates[key] = (currentValue(for: key), !isReversed ? toValue : fromValue)
transitionStates[key] = (currentValue(for: key), isReversed ? fromValue : toValue)
}
targetState.duration = isReversed ? elapsedTime - targetState.delay : duration - elapsedTime
animate(delay: max(0, targetState.delay - elapsedTime))
if isReversed {
if elapsedTime > targetState.delay + duration {
let backDelay = elapsedTime - (targetState.delay + duration)
return animate(delay: backDelay, duration: duration)
} else if elapsedTime > targetState.delay {
return animate(delay: 0, duration: duration - elapsedTime - targetState.delay)
}
} else {
if elapsedTime <= targetState.delay {
return animate(delay: targetState.delay - elapsedTime, duration: duration)
} else if elapsedTime <= targetState.delay + duration {
let timePassedDelay = elapsedTime - targetState.delay
return animate(delay: 0, duration: duration - timePassedDelay)
}
}
return 0
}
override func seek(to elapsedTime: TimeInterval) {
seek(layer: snapshot.layer, elapsedTime: elapsedTime)
let timeOffset = CGFloat(elapsedTime - targetState.delay)
if let v = contentLayer {
seek(layer: v, elapsedTime: elapsedTime)
}
if let v = overlayLayer {
seek(layer: v, elapsedTime: elapsedTime)
for (layer, key, anim) in animations {
anim.speed = 0
anim.timeOffset = CFTimeInterval(timeOffset.clamp(0, CGFloat(anim.duration) - 0.001))
layer.removeAnimation(forKey: key)
layer.add(anim, forKey: key)
}
}
override func startAnimations(isAppearing: Bool) {
override func startAnimations() -> TimeInterval {
if let beginState = targetState.beginState?.state {
let appeared = viewState(targetState: beginState)
for (k, v) in appeared {
......@@ -124,16 +147,16 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext {
transitionStates[k] = (fromValue, toValue)
}
animate(delay: targetState.delay)
return animate(delay: targetState.delay, duration: duration)
}
}
extension MotionCoreAnimationViewContext {
fileprivate extension MotionCoreAnimationViewContext {
/**
Lazy loads the overlay layer.
- Returns: A CALayer.
*/
fileprivate func getOverlayLayer() -> CALayer {
func getOverlayLayer() -> CALayer {
if nil == overlayLayer {
overlayLayer = CALayer()
overlayLayer!.frame = snapshot.bounds
......@@ -149,7 +172,7 @@ extension MotionCoreAnimationViewContext {
- Parameter for key: A String.
- Returns: An optional String.
*/
fileprivate func overlayKey(for key: String) -> String? {
func overlayKey(for key: String) -> String? {
guard key.hasPrefix("overlay.") else {
return nil
}
......@@ -164,9 +187,9 @@ extension MotionCoreAnimationViewContext {
- Parameter for key: A String.
- Returns: An optional Any value.
*/
fileprivate func currentValue(for key: String) -> Any? {
func currentValue(for key: String) -> Any? {
if let key = overlayKey(for: key) {
return overlayLayer?.value(forKeyPath: key)
return (overlayLayer?.presentation() ?? overlayLayer)?.value(forKeyPath: key)
}
if false != snapshot.layer.animationKeys()?.isEmpty {
......@@ -180,11 +203,12 @@ extension MotionCoreAnimationViewContext {
Retrieves the animation for a given key.
- Parameter key: String.
- Parameter beginTime: A TimeInterval.
- Parameter duration: A TimeInterval.
- Parameter fromValue: An optional Any value.
- Parameter toValue: An optional Any value.
- Parameter ignoreArc: A Boolean value to ignore an arc position.
*/
fileprivate func getAnimation(key: String, beginTime: TimeInterval, fromValue: Any?, toValue: Any?, ignoreArc: Bool = false) -> CAPropertyAnimation {
func getAnimation(key: String, beginTime: TimeInterval, duration: TimeInterval, fromValue: Any?, toValue: Any?, ignoreArc: Bool = false) -> CAPropertyAnimation {
let key = overlayKey(for: key) ?? key
let anim: CAPropertyAnimation
......@@ -211,7 +235,7 @@ extension MotionCoreAnimationViewContext {
let a = CASpringAnimation(keyPath: key)
a.stiffness = stiffness
a.damping = damping
a.duration = a.settlingDuration * 0.9
a.duration = a.settlingDuration
a.fromValue = fromValue
a.toValue = toValue
......@@ -234,28 +258,118 @@ extension MotionCoreAnimationViewContext {
}
/**
Sets a new size for the given view.
- Parameter view: A UIView.
- Parameter newSize: A CGSize.
*/
func setSize(view: UIView, newSize: CGSize) {
let oldSize = view.bounds.size
if .noSnapshot != targetState.snapshotType {
if 0 == oldSize.width || 0 == oldSize.height || 0 == newSize.width || 0 == newSize.height {
for v in view.subviews {
v.center = newSize.center
v.bounds.size = newSize
setSize(view: v, newSize: newSize)
}
} else {
let sizeRatio = oldSize / newSize
for v in view.subviews {
let center = v.center
let size = v.bounds.size
v.center = center / sizeRatio
v.bounds.size = size / sizeRatio
setSize(view: v, newSize: size / sizeRatio)
}
}
view.bounds.size = newSize
} else {
view.bounds.size = newSize
view.layoutSubviews()
}
}
/**
Executes a UIView based animation.
- Parameter duration: A TimeInterval.
- Parameter delay: A TimeInterval.
- Parameter _ animations: An animation block.
*/
func uiViewBasedAnimate(duration: TimeInterval, delay: TimeInterval, _ animations: @escaping () -> Void) {
CALayer.motionAddedAnimations = []
if let (stiffness, damping) = targetState.spring {
UIView.animate(withDuration: duration, delay: delay, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: [], animations: animations, completion: nil)
let addedAnimations = CALayer.motionAddedAnimations!
CALayer.motionAddedAnimations = nil
for (layer, key, anim) in addedAnimations {
if #available(iOS 9.0, *), let anim = anim as? CASpringAnimation {
anim.stiffness = stiffness
anim.damping = damping
self.addAnimation(anim, for: key, to: layer)
} else {
self.animations.append((layer, key, anim))
}
}
} else {
CATransaction.begin()
CATransaction.setAnimationTimingFunction(timingFunction)
UIView.animate(withDuration: duration, delay: delay, options: [], animations: animations, completion: nil)
CATransaction.commit()
let addedAnimations = CALayer.motionAddedAnimations!
CALayer.motionAddedAnimations = nil
self.animations.append(contentsOf: addedAnimations)
}
}
/**
Adds an animation to a given layer.
- Parameter _ animation: A CAAnimation.
- Parameter for key: A String.
- Parameter to layer: A CALayer.
*/
func addAnimation(_ animation: CAAnimation, for key: String, to layer: CALayer) {
animations.append((layer, key, animation))
layer.add(animation, forKey: key)
}
/**
Retrieves the duration of an animation, including the
duration of the animation and the initial delay.
- Parameter key: A String.
- Parameter beginTime: A TimeInterval.
- Parameter duration: A TimeInterval.
- Parameter fromValue: A optional Any value.
- Parameter toValue: A optional Any value.
- Returns: A TimeInterval.
*/
@discardableResult
fileprivate func animate(key: String, beginTime: TimeInterval, fromValue: Any?, toValue: Any?) -> TimeInterval {
let anim = getAnimation(key: key, beginTime:beginTime, fromValue: fromValue, toValue: toValue)
func animate(key: String, beginTime: TimeInterval, duration: TimeInterval, fromValue: Any?, toValue: Any?) -> TimeInterval {
let anim = getAnimation(key: key, beginTime: beginTime, duration: duration, fromValue: fromValue, toValue: toValue)
if let overlayKey = overlayKey(for: key) {
getOverlayLayer().add(anim, forKey: overlayKey)
addAnimation(anim, for: overlayKey, to: getOverlayLayer())
} else {
snapshot.layer.add(anim, forKey: key)
switch key {
case "cornerRadius", "contentsRect", "contentsScale":
contentLayer?.add(anim, forKey: key)
overlayLayer?.add(anim, forKey: key)
addAnimation(anim, for: key, to: snapshot.layer)
if let v = contentLayer {
addAnimation(anim.copy() as! CAAnimation, for: key, to: v)
}
if let v = overlayLayer {
addAnimation(anim.copy() as! CAAnimation, for: key, to: v)
}
case "bounds.size":
guard let fs = (fromValue as? NSValue)?.cgSizeValue else {
......@@ -266,26 +380,17 @@ extension MotionCoreAnimationViewContext {
return 0
}
// for the snapshotView(UIReplicantView): there is a
// subview(UIReplicantContentView) that is hosting the real snapshot image.
// because we are using CAAnimations and not UIView animations,
// The snapshotView will not layout during animations.
// we have to add two more animations to manually layout the content view.
let fpn = NSValue(cgPoint: fs.center)
let tpn = NSValue(cgPoint: ts.center)
let a = getAnimation(key: "position", beginTime: 0, fromValue: fpn, toValue: tpn, ignoreArc: true)
a.beginTime = anim.beginTime
a.timingFunction = anim.timingFunction
a.duration = anim.duration
contentLayer?.add(a, forKey: "position")
contentLayer?.add(anim, forKey: key)
overlayLayer?.add(a, forKey: "position")
overlayLayer?.add(anim, forKey: key)
setSize(view: snapshot, newSize: fs)
uiViewBasedAnimate(duration: anim.duration, delay: beginTime - currentTime) { [weak self] in
guard let `self` = self else {
return
}
self.setSize(view: self.snapshot, newSize: ts)
}
default: break
default:
addAnimation(anim, for: key, to: snapshot.layer)
}
}
......@@ -295,26 +400,28 @@ extension MotionCoreAnimationViewContext {
/**
Animates the contentLayer and overlayLayer with a given delay.
- Parameter delay: A TimeInterval.
- Parameter duration: A TimeInterval.
- Returns: A TimeInterval.
*/
fileprivate func animate(delay: TimeInterval) {
if let v = targetState.timingFunction {
timingFunction = v
func animate(delay: TimeInterval, duration: TimeInterval) -> TimeInterval {
for (layer, key, _) in animations {
layer.removeAnimation(forKey: key)
}
if let v = targetState.duration {
duration = v
if let tf = targetState.timingFunction {
timingFunction = tf
}
let beginTime = currentTime + delay
var timeUntilStop = duration
var finalDuration: TimeInterval = duration
animations = []
for (key, (fromValue, toValue)) in transitionStates {
let neededTime = animate(key: key, beginTime: beginTime, fromValue: fromValue, toValue: toValue)
finalDuration = max(finalDuration, neededTime + delay)
let neededTime = animate(key: key, beginTime: currentTime + delay, duration: duration, fromValue: fromValue, toValue: toValue)
timeUntilStop = max(timeUntilStop, neededTime)
}
duration = finalDuration
return timeUntilStop + delay
}
/**
......@@ -322,7 +429,7 @@ extension MotionCoreAnimationViewContext {
- Parameter targetState state: A MotionTransitionState.
- Returns: A map of key paths to animation values.
*/
fileprivate func viewState(targetState ts: MotionTransitionState) -> [String: Any?] {
func viewState(targetState ts: MotionTransitionState) -> [String: Any?] {
var ts = ts
var values = [String: Any?]()
......@@ -409,19 +516,4 @@ extension MotionCoreAnimationViewContext {
return values
}
/**
Moves a layer's animation to a given elapsed time.
- Parameter layer: A CALayer.
- Parameter elapsedTime: A TimeInterval.
*/
fileprivate func seek(layer: CALayer, elapsedTime: TimeInterval) {
let timeOffset = elapsedTime - targetState.delay
for (key, anim) in layer.animations {
anim.speed = 0
anim.timeOffset = max(0, min(anim.duration - 0.01, timeOffset))
layer.removeAnimation(forKey: key)
layer.add(anim, forKey: key)
}
}
}
......@@ -28,7 +28,7 @@
import UIKit
class BaseMotionAnimator: MotionAnimator {
class MotionCoreAnimator: MotionAnimator {
weak public var motion: Motion!
/// A reference to the MotionContext.
......
......@@ -28,7 +28,7 @@
import UIKit
internal class MotionTransitionAnimator<T: MotionAnimatorViewContext>: BaseMotionAnimator, MotionHasInsertOrder {
internal class MotionTransitionAnimator<T: MotionAnimatorViewContext>: MotionCoreAnimator, MotionHasInsertOrder {
/// An index of views to their corresponding animator context.
var viewToContexts = [UIView: T]()
......@@ -114,8 +114,10 @@ internal class MotionTransitionAnimator<T: MotionAnimatorViewContext>: BaseMotio
var duration: TimeInterval = 0
for (_, v) in viewToContexts {
v.resume(at: elapsedTime, isReversed: isReversed)
duration = max(duration, v.duration)
if v.targetState.duration == nil {
v.duration = max(v.duration, v.snapshot.optimizedDuration(targetState: v.targetState) + elapsedTime)
}
duration = max(duration, v.resume(at: elapsedTime, isReversed: isReversed))
}
return duration
......@@ -144,11 +146,11 @@ extension MotionTransitionAnimator {
*/
fileprivate func animate(view: UIView, isAppearing: Bool) {
let s = context.snapshotView(for: view)
let v = T(animator: self, snapshot: s, targetState: context[view]!)
let v = T(animator: self, snapshot: s, targetState: context[view]!, isAppearing: isAppearing)
viewToContexts[view] = v
v.startAnimations(isAppearing: isAppearing)
v.startAnimations()
}
}
......@@ -29,6 +29,26 @@ import UIKit
extension CALayer: CAAnimationDelegate {}
internal extension CALayer {
internal static var motionAddedAnimations: [(CALayer, String, CAAnimation)]? = {
let swizzling: (AnyClass, Selector, Selector) -> Void = { forClass, originalSelector, swizzledSelector in
if let originalMethod = class_getInstanceMethod(forClass, originalSelector), let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
swizzling(CALayer.self, #selector(add(_:forKey:)), #selector(motionAdd(anim:forKey:)))
return nil
}()
@objc
dynamic func motionAdd(anim: CAAnimation, forKey: String?) {
let copiedAnim = anim.copy() as! CAAnimation
copiedAnim.delegate = nil // having delegate resulted some weird animation behavior
CALayer.motionAddedAnimations?.append((self, forKey!, copiedAnim))
motionAdd(anim: anim, forKey: forKey)
}
/// Retrieves all currently running animations for the layer.
var animations: [(String, CAAnimation)] {
guard let keys = animationKeys() else {
......
......@@ -187,6 +187,16 @@ public func /(left: CGPoint, right: CGPoint) -> CGPoint {
/**
A handler for the (/) operator.
- Parameter left: A CGPoint.
- Parameter right: A CGSize.
- Returns: A CGPoint.
*/
public func /(left: CGPoint, right: CGSize) -> CGPoint {
return CGPoint(x: left.x / right.width, y: left.y / right.height)
}
/**
A handler for the (/) operator.
- Parameter left: A CGSize.
- Parameter right: A CGSize.
- Returns: A CGSize.
......
......@@ -284,6 +284,17 @@ internal extension UIView {
}
/**
Calculates the optimized duration for a view.
- Parameter targetState: A MotionTransitionState.
- Returns: A TimeInterval.
*/
func optimizedDuration(targetState: MotionTransitionState) -> TimeInterval {
return optimizedDuration(position: targetState.position,
size: targetState.size,
transform: targetState.transform)
}
/**
Takes a snapshot of a view usinag the UI graphics context.
- Returns: A UIView with an embedded UIImageView.
*/
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment