Commit 2caa4eb1 by Daniel Dahan

initial clean up for MotionCoreAnimationViewContext completed

parent 97383774
...@@ -43,13 +43,11 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext { ...@@ -43,13 +43,11 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext {
/// Layer which holds the overlay. /// Layer which holds the overlay.
fileprivate var overlayLayer: CALayer? fileprivate var overlayLayer: CALayer?
/** override func clean() {
Determines whether a view can be animated. super.clean()
- Parameter view: A UIView. overlayLayer = nil
- Parameter state: A MotionTargetState. }
- Parameter isAppearing: A boolean indicating whether or not the
view is going to appear.
*/
override class func canAnimate(view: UIView, state: MotionTargetState, isAppearing: Bool) -> Bool { override class func canAnimate(view: UIView, state: MotionTargetState, isAppearing: Bool) -> Bool {
return nil != state.position || return nil != state.position ||
nil != state.size || nil != state.size ||
...@@ -69,11 +67,73 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext { ...@@ -69,11 +67,73 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext {
state.forceAnimate state.forceAnimate
} }
override func apply(state: MotionTargetState) {
let ts = viewState(targetState: state)
for (key, targetValue) in ts {
if nil == transitionStates[key] {
let current = currentValue(for: key)
transitionStates[key] = (current, current)
}
animate(key: key, beginTime: 0, fromValue: targetValue, toValue: targetValue)
}
}
override func resume(elapsedTime: TimeInterval, isReversed: Bool) {
for (key, (fromValue, toValue)) in transitionStates {
transitionStates[key] = (currentValue(for: key), !isReversed ? toValue : fromValue)
}
targetState.duration = isReversed ? elapsedTime - targetState.delay : duration - elapsedTime
animate(delay: max(0, targetState.delay - elapsedTime))
}
override func seek(to elapsedTime: TimeInterval) {
seek(layer: snapshot.layer, elapsedTime: elapsedTime)
if let v = contentLayer {
seek(layer: v, elapsedTime: elapsedTime)
}
if let v = overlayLayer {
seek(layer: v, elapsedTime: elapsedTime)
}
}
override func startAnimations(isAppearing: Bool) {
if let beginState = targetState.beginState?.state {
let appeared = viewState(targetState: beginState)
for (k, v) in appeared {
snapshot.layer.setValue(v, forKeyPath: k)
}
if let (k, v) = beginState.overlay {
let overlay = getOverlayLayer()
overlay.backgroundColor = k
overlay.opacity = Float(v)
}
}
let disappeared = viewState(targetState: targetState)
for (k, v) in disappeared {
let isAppearingState = currentValue(for: k)
let toValue = isAppearing ? isAppearingState : v
let fromValue = !isAppearing ? isAppearingState : v
transitionStates[k] = (fromValue, toValue)
}
animate(delay: targetState.delay)
}
}
extension MotionCoreAnimationViewContext {
/** /**
Lazy loads the overlay layer. Lazy loads the overlay layer.
- Returns: A CALayer. - Returns: A CALayer.
*/ */
func getOverlayLayer() -> CALayer { fileprivate func getOverlayLayer() -> CALayer {
if nil == overlayLayer { if nil == overlayLayer {
overlayLayer = CALayer() overlayLayer = CALayer()
overlayLayer!.frame = snapshot.bounds overlayLayer!.frame = snapshot.bounds
...@@ -89,7 +149,7 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext { ...@@ -89,7 +149,7 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext {
- Parameter for key: A String. - Parameter for key: A String.
- Returns: An optional String. - Returns: An optional String.
*/ */
func overlayKey(for key: String) -> String? { fileprivate func overlayKey(for key: String) -> String? {
guard key.hasPrefix("overlay.") else { guard key.hasPrefix("overlay.") else {
return nil return nil
} }
...@@ -100,11 +160,11 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext { ...@@ -100,11 +160,11 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext {
} }
/** /**
Retrieves the current value for a given key. Retrieves the current value for a given key.
- Parameter for key: A String. - Parameter for key: A String.
- Returns: An optional Any value. - Returns: An optional Any value.
*/ */
func currentValue(for key: String) -> Any? { fileprivate func currentValue(for key: String) -> Any? {
if let key = overlayKey(for: key) { if let key = overlayKey(for: key) {
return overlayLayer?.value(forKeyPath: key) return overlayLayer?.value(forKeyPath: key)
} }
...@@ -117,35 +177,35 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext { ...@@ -117,35 +177,35 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext {
} }
/** /**
Retrieves the animation for a given key. Retrieves the animation for a given key.
- Parameter key: String. - Parameter key: String.
- Parameter beginTime: A TimeInterval. - Parameter beginTime: A TimeInterval.
- Parameter fromValue: An optional Any value. - Parameter fromValue: An optional Any value.
- Parameter toValue: An optional Any value. - Parameter toValue: An optional Any value.
- Parameter ignoreArc: A Boolean value to ignore an arc position. - Parameter ignoreArc: A Boolean value to ignore an arc position.
*/ */
func getAnimation(key: String, beginTime: TimeInterval, fromValue: Any?, toValue: Any?, ignoreArc: Bool = false) -> CAPropertyAnimation { fileprivate func getAnimation(key: String, beginTime: TimeInterval, fromValue: Any?, toValue: Any?, ignoreArc: Bool = false) -> CAPropertyAnimation {
let key = overlayKey(for: key) ?? key let key = overlayKey(for: key) ?? key
let anim: CAPropertyAnimation let anim: CAPropertyAnimation
if !ignoreArc, "position" == key, let arcIntensity = targetState.arc, if !ignoreArc, "position" == key, let arcIntensity = targetState.arc,
let fromPos = (fromValue as? NSValue)?.cgPointValue, let fromPos = (fromValue as? NSValue)?.cgPointValue,
let toPos = (toValue as? NSValue)?.cgPointValue, let toPos = (toValue as? NSValue)?.cgPointValue,
abs(fromPos.x - toPos.x) >= 1, abs(fromPos.y - toPos.y) >= 1 { abs(fromPos.x - toPos.x) >= 1, abs(fromPos.y - toPos.y) >= 1 {
let a = CAKeyframeAnimation(keyPath: key) let a = CAKeyframeAnimation(keyPath: key)
let path = CGMutablePath() let path = CGMutablePath()
let maxControl = fromPos.y > toPos.y ? CGPoint(x: toPos.x, y: fromPos.y) : CGPoint(x: fromPos.x, y: toPos.y) let maxControl = fromPos.y > toPos.y ? CGPoint(x: toPos.x, y: fromPos.y) : CGPoint(x: fromPos.x, y: toPos.y)
let minControl = (toPos - fromPos) / 2 + fromPos let minControl = (toPos - fromPos) / 2 + fromPos
path.move(to: fromPos) path.move(to: fromPos)
path.addQuadCurve(to: toPos, control: minControl + (maxControl - minControl) * arcIntensity) path.addQuadCurve(to: toPos, control: minControl + (maxControl - minControl) * arcIntensity)
a.values = [fromValue!, toValue!] a.values = [fromValue!, toValue!]
a.path = path a.path = path
a.duration = duration a.duration = duration
a.timingFunctions = [timingFunction] a.timingFunctions = [timingFunction]
anim = a anim = a
} else if #available(iOS 9.0, *), "cornerRadius" != key, let (stiffness, damping) = targetState.spring { } else if #available(iOS 9.0, *), "cornerRadius" != key, let (stiffness, damping) = targetState.spring {
let a = CASpringAnimation(keyPath: key) let a = CASpringAnimation(keyPath: key)
...@@ -165,14 +225,14 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext { ...@@ -165,14 +225,14 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext {
anim = a anim = a
} }
anim.fillMode = kCAFillModeBoth anim.fillMode = kCAFillModeBoth
anim.isRemovedOnCompletion = false anim.isRemovedOnCompletion = false
anim.beginTime = beginTime anim.beginTime = beginTime
return anim return anim
} }
/** /**
Retrieves the duration of an animation, including the Retrieves the duration of an animation, including the
duration of the animation and the initial delay. duration of the animation and the initial delay.
...@@ -182,214 +242,181 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext { ...@@ -182,214 +242,181 @@ internal class MotionCoreAnimationViewContext: MotionAnimatorViewContext {
- Parameter toValue: A optional Any value. - Parameter toValue: A optional Any value.
- Returns: A TimeInterval. - Returns: A TimeInterval.
*/ */
func animate(key: String, beginTime: TimeInterval, fromValue: Any?, toValue: Any?) -> 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) let anim = getAnimation(key: key, beginTime:beginTime, fromValue: fromValue, toValue: toValue)
if let overlayKey = overlayKey(for: key) { if let overlayKey = overlayKey(for: key) {
getOverlayLayer().add(anim, forKey: overlayKey) getOverlayLayer().add(anim, forKey: overlayKey)
} else { } else {
snapshot.layer.add(anim, forKey: key) snapshot.layer.add(anim, forKey: key)
switch key { switch key {
case "cornerRadius", "contentsRect", "contentsScale": case "cornerRadius", "contentsRect", "contentsScale":
contentLayer?.add(anim, forKey: key) contentLayer?.add(anim, forKey: key)
overlayLayer?.add(anim, forKey: key) overlayLayer?.add(anim, forKey: key)
case "bounds.size": case "bounds.size":
let fromSize = (fromValue as? NSValue)!.cgSizeValue guard let fs = (fromValue as? NSValue)?.cgSizeValue else {
let toSize = (toValue as? NSValue)!.cgSizeValue return 0
}
guard let ts = (toValue as? NSValue)?.cgSizeValue else {
return 0
}
// for the snapshotView(UIReplicantView): there is a // for the snapshotView(UIReplicantView): there is a
// subview(UIReplicantContentView) that is hosting the real snapshot image. // subview(UIReplicantContentView) that is hosting the real snapshot image.
// because we are using CAAnimations and not UIView animations, // because we are using CAAnimations and not UIView animations,
// The snapshotView will not layout during animations. // The snapshotView will not layout during animations.
// we have to add two more animations to manually layout the content view. // we have to add two more animations to manually layout the content view.
let fromPosn = NSValue(cgPoint:fromSize.center) let fpn = NSValue(cgPoint: fs.center)
let toPosn = NSValue(cgPoint:toSize.center) let tpn = NSValue(cgPoint: ts.center)
let positionAnim = getAnimation(key: "position", beginTime:0, fromValue: fromPosn, toValue: toPosn, ignoreArc: true) let a = getAnimation(key: "position", beginTime: 0, fromValue: fpn, toValue: tpn, ignoreArc: true)
positionAnim.beginTime = anim.beginTime a.beginTime = anim.beginTime
positionAnim.timingFunction = anim.timingFunction a.timingFunction = anim.timingFunction
positionAnim.duration = anim.duration a.duration = anim.duration
contentLayer?.add(positionAnim, forKey: "position") contentLayer?.add(a, forKey: "position")
contentLayer?.add(anim, forKey: key) contentLayer?.add(anim, forKey: key)
overlayLayer?.add(positionAnim, forKey: "position") overlayLayer?.add(a, forKey: "position")
overlayLayer?.add(anim, forKey: key) overlayLayer?.add(anim, forKey: key)
default: break default: break
} }
} }
return anim.duration + anim.beginTime - beginTime return anim.duration + anim.beginTime - beginTime
} }
func animate(delay: TimeInterval) { /**
if let tf = targetState.timingFunction { Animates the contentLayer and overlayLayer with a given delay.
timingFunction = tf - Parameter delay: A TimeInterval.
} */
fileprivate func animate(delay: TimeInterval) {
duration = targetState.duration! if let v = targetState.timingFunction {
timingFunction = v
let beginTime = currentTime + delay }
var finalDuration: TimeInterval = duration
for (key, (fromValue, toValue)) in transitionStates { if let v = targetState.duration {
let neededTime = animate(key: key, beginTime: beginTime, fromValue: fromValue, toValue: toValue) duration = v
finalDuration = max(finalDuration, neededTime + delay) }
}
let beginTime = currentTime + delay
duration = finalDuration
} var finalDuration: TimeInterval = duration
/** for (key, (fromValue, toValue)) in transitionStates {
- Returns: a CALayer [keyPath:value] map for animation let neededTime = animate(key: key, beginTime: beginTime, fromValue: fromValue, toValue: toValue)
*/ finalDuration = max(finalDuration, neededTime + delay)
func viewState(targetState: MotionTargetState) -> [String: Any?] { }
var targetState = targetState
var rtn = [String: Any?]() duration = finalDuration
if let size = targetState.size {
if targetState.useScaleBasedSizeChange ?? self.targetState.useScaleBasedSizeChange ?? false {
let currentSize = snapshot.bounds.size
targetState.append(.scale(x:size.width / currentSize.width,
y:size.height / currentSize.height))
} else {
rtn["bounds.size"] = NSValue(cgSize:size)
}
}
if let position = targetState.position {
rtn["position"] = NSValue(cgPoint:position)
}
if let opacity = targetState.opacity, !(snapshot is UIVisualEffectView) {
rtn["opacity"] = NSNumber(value: opacity)
}
if let cornerRadius = targetState.cornerRadius {
rtn["cornerRadius"] = NSNumber(value: cornerRadius.native)
}
if let backgroundColor = targetState.backgroundColor {
rtn["backgroundColor"] = backgroundColor
}
if let zPosition = targetState.zPosition {
rtn["zPosition"] = NSNumber(value: zPosition.native)
}
if let borderWidth = targetState.borderWidth {
rtn["borderWidth"] = NSNumber(value: borderWidth.native)
}
if let borderColor = targetState.borderColor {
rtn["borderColor"] = borderColor
}
if let masksToBounds = targetState.masksToBounds {
rtn["masksToBounds"] = masksToBounds
}
if targetState.displayShadow {
if let shadowColor = targetState.shadowColor {
rtn["shadowColor"] = shadowColor
}
if let shadowRadius = targetState.shadowRadius {
rtn["shadowRadius"] = NSNumber(value: shadowRadius.native)
}
if let shadowOpacity = targetState.shadowOpacity {
rtn["shadowOpacity"] = NSNumber(value: shadowOpacity)
}
if let shadowPath = targetState.shadowPath {
rtn["shadowPath"] = shadowPath
}
if let shadowOffset = targetState.shadowOffset {
rtn["shadowOffset"] = NSValue(cgSize: shadowOffset)
}
}
if let contentsRect = targetState.contentsRect {
rtn["contentsRect"] = NSValue(cgRect: contentsRect)
}
if let contentsScale = targetState.contentsScale {
rtn["contentsScale"] = NSNumber(value: contentsScale.native)
}
if let transform = targetState.transform {
rtn["transform"] = NSValue(caTransform3D: transform)
}
if let (color, opacity) = targetState.overlay {
rtn["overlay.backgroundColor"] = color
rtn["overlay.opacity"] = NSNumber(value: opacity.native)
}
return rtn
}
override func apply(state: MotionTargetState) {
let targetState = viewState(targetState: state)
for (key, targetValue) in targetState {
if self.transitionStates[key] == nil {
let current = currentValue(for: key)
self.transitionStates[key] = (current, current)
}
_ = animate(key: key, beginTime: 0, fromValue: targetValue, toValue: targetValue)
}
}
override func resume(elapsedTime: TimeInterval, isReversed: Bool) {
for (key, (fromValue, toValue)) in transitionStates {
let realToValue = !isReversed ? toValue : fromValue
let realFromValue = currentValue(for: key)
transitionStates[key] = (realFromValue, realToValue)
}
// we need to update the duration to reflect current state
targetState.duration = isReversed ? elapsedTime - targetState.delay : duration - elapsedTime
let realDelay = max(0, targetState.delay - elapsedTime)
animate(delay: realDelay)
}
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)
}
}
override func seek(to elapsedTime: TimeInterval) {
seek(layer:snapshot.layer, elapsedTime:elapsedTime)
if let contentLayer = contentLayer {
seek(layer:contentLayer, elapsedTime:elapsedTime)
}
if let overlayLayer = overlayLayer {
seek(layer: overlayLayer, elapsedTime: elapsedTime)
} }
}
/**
override func clean() { Constructs a map of key paths to animation values.
super.clean() - Parameter targetState state: A MotionTargetState.
overlayLayer = nil - Returns: A map of key paths to animation values.
} */
fileprivate func viewState(targetState ts: MotionTargetState) -> [String: Any?] {
override func startAnimations(isAppearing: Bool) { var ts = ts
if let beginState = targetState.beginState?.state { var values = [String: Any?]()
let appeared = viewState(targetState: beginState)
for (key, value) in appeared { if let size = ts.size {
snapshot.layer.setValue(value, forKeyPath: key) if ts.useScaleBasedSizeChange ?? targetState.useScaleBasedSizeChange ?? false {
} let currentSize = snapshot.bounds.size
if let (color, opacity) = beginState.overlay { ts.append(.scale(x: size.width / currentSize.width, y: size.height / currentSize.height))
let overlay = getOverlayLayer()
overlay.backgroundColor = color } else {
overlay.opacity = Float(opacity) values["bounds.size"] = NSValue(cgSize:size)
} }
}
if let position = ts.position {
values["position"] = NSValue(cgPoint:position)
}
if let opacity = ts.opacity, !(snapshot is UIVisualEffectView) {
values["opacity"] = NSNumber(value: opacity)
}
if let cornerRadius = ts.cornerRadius {
values["cornerRadius"] = NSNumber(value: cornerRadius.native)
}
if let backgroundColor = ts.backgroundColor {
values["backgroundColor"] = backgroundColor
}
if let zPosition = ts.zPosition {
values["zPosition"] = NSNumber(value: zPosition.native)
}
if let borderWidth = ts.borderWidth {
values["borderWidth"] = NSNumber(value: borderWidth.native)
}
if let borderColor = ts.borderColor {
values["borderColor"] = borderColor
}
if let masksToBounds = ts.masksToBounds {
values["masksToBounds"] = masksToBounds
}
if ts.displayShadow {
if let shadowColor = ts.shadowColor {
values["shadowColor"] = shadowColor
}
if let shadowRadius = ts.shadowRadius {
values["shadowRadius"] = NSNumber(value: shadowRadius.native)
}
if let shadowOpacity = ts.shadowOpacity {
values["shadowOpacity"] = NSNumber(value: shadowOpacity)
}
if let shadowPath = ts.shadowPath {
values["shadowPath"] = shadowPath
}
if let shadowOffset = ts.shadowOffset {
values["shadowOffset"] = NSValue(cgSize: shadowOffset)
}
}
if let contentsRect = ts.contentsRect {
values["contentsRect"] = NSValue(cgRect: contentsRect)
}
if let contentsScale = ts.contentsScale {
values["contentsScale"] = NSNumber(value: contentsScale.native)
}
if let transform = ts.transform {
values["transform"] = NSValue(caTransform3D: transform)
}
if let (color, opacity) = ts.overlay {
values["overlay.backgroundColor"] = color
values["overlay.opacity"] = NSNumber(value: opacity.native)
}
return values
} }
let disappeared = viewState(targetState: targetState) fileprivate func seek(layer: CALayer, elapsedTime: TimeInterval) {
let timeOffset = elapsedTime - targetState.delay
for (key, disappearedState) in disappeared { for (key, anim) in layer.animations {
let isAppearingState = currentValue(for: key) anim.speed = 0
let toValue = isAppearing ? isAppearingState : disappearedState anim.timeOffset = max(0, min(anim.duration - 0.01, timeOffset))
let fromValue = !isAppearing ? isAppearingState : disappearedState layer.removeAnimation(forKey: key)
transitionStates[key] = (fromValue, toValue) layer.add(anim, forKey: key)
}
} }
animate(delay: targetState.delay)
}
} }
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